In this code sample, the TextDocumentView has a variable set to 5.0. Any idea where this 5.0 number is coming from? It is used internally in other calculations within TextDocumentLayer. While it works, I'm not sure what it signifies, and how to adapt it to our projects? Thanks
Post
Replies
Boosts
Views
Activity
This function on NSTextLayoutManager has the following signature
func enumerateTextSegments(
in textRange: NSTextRange,
type: NSTextLayoutManager.SegmentType,
options: NSTextLayoutManager.SegmentOptions = [],
using block: (NSTextRange?, CGRect, CGFloat, NSTextContainer) -> Bool
)
The documentation here doesn't define what the CGRect and CGFloat passed to block are. However, looking at sample code Using TextKit2 To Interact With Text, they seem to be the frame for the textsegment and baselineposition respectively.
But, the textSegmentFrame seems to start at origin.x = 5.0 when text is empty. Is this some starting offset for text segments? I don't seem to be able to find mention of this anywhere.
I have a custom subclass to a NSTextContentManager which provides NSTextParagrahs for layout. However, when I have a trailing newline in my content string, the NSTextLayoutFragment does not have a empty NSTextLineFragment indicating a new line. This is in contrast to using the standard NSTextContentStorage. NSTextContentStorage also uses NSTextParagraphs to represent it's text elements.
The code I have for both scenarios are
Using Default NSTextContentStorage
let layoutManager = NSTextLayoutManager()
let container = NSTextContainer(size: NSSize(width: 400, height: 400))
layoutManager.textContainer = container
let contentStorage = NSTextContentStorage()
contentStorage.textStorage?.replaceCharacters(in: NSRange(location: 0, length: 0), with: "It was the best of times.\n")
contentStorage.addTextLayoutManager(layoutManager)
layoutManager.enumerateTextLayoutFragments(from: contentStorage.documentRange.location, options: .ensuresLayout) { textLayoutFragment in
print("defaultTextLineFragments:")
for (index, textLineFragment) in textLayoutFragment.textLineFragments.enumerated() {
print("\(index): \(textLineFragment)")
}
print("\n")
return true
}
This outputs
defaultTextLineFragments:
0: <NSTextLineFragment: 0x123815a80 "It was the best of times.
">
1: <NSTextLineFragment: 0x123825b00 "">
Using custom subclass to NSTextContentManager
class CustomTextLocation: NSObject, NSTextLocation {
let offset: Int
init(offset: Int) {
self.offset = offset
}
func compare(_ location: NSTextLocation) -> ComparisonResult {
guard let location = location as? CustomTextLocation else {
return .orderedAscending
}
if offset < location.offset {
return .orderedAscending
} else if offset > location.offset {
return .orderedDescending
} else {
return .orderedSame
}
}
}
class CustomStorage: NSTextContentManager {
let content = "It was the best of times.\n"
override var documentRange: NSTextRange {
NSTextRange(location: CustomTextLocation(offset: 0), end: CustomTextLocation(offset: content.utf8.count))!
}
override func textElements(for range: NSTextRange) -> [NSTextElement] {
let paragraph = NSTextParagraph(attributedString: NSAttributedString(string: content))
paragraph.textContentManager = self
paragraph.elementRange = documentRange
return [paragraph]
}
override func enumerateTextElements(from textLocation: NSTextLocation?, options: NSTextContentManager.EnumerationOptions = [], using block: (NSTextElement) -> Bool) -> NSTextLocation? {
// Just assuming static text elements for this example
let elements = self.textElements(for: documentRange)
for element in elements {
block(element)
}
return elements.last?.elementRange?.endLocation
}
override func location(_ location: NSTextLocation, offsetBy offset: Int) -> NSTextLocation? {
guard let location = location as? CustomTextLocation,
let documentEnd = documentRange.endLocation as? CustomTextLocation else {
return nil
}
let offset = CustomTextLocation(offset: location.offset + offset)
if offset.compare(documentEnd) == .orderedDescending {
return nil
}
return offset
}
override func offset(from: NSTextLocation, to: NSTextLocation) -> Int {
guard let from = from as? CustomTextLocation,
let to = to as? CustomTextLocation else {
return 0
}
return to.offset - from.offset
}
}
let customLayoutManager = NSTextLayoutManager()
let customContainer = NSTextContainer(size: NSSize(width: 400, height: 400))
customLayoutManager.textContainer = customContainer
let customStorage = CustomStorage()
customStorage.addTextLayoutManager(customLayoutManager)
customLayoutManager.enumerateTextLayoutFragments(from: customStorage.documentRange.location, options: .ensuresLayout) { textLayoutFragment in
print("customStorage textLineFragments:")
for (index, textLineFragment) in textLayoutFragment.textLineFragments.enumerated() {
print("\(index): \(textLineFragment)")
}
print("\n")
return true
}
This output
customStorage textLineFragments:
0: <NSTextLineFragment: 0x13ff0c8d0 "It was the best of times.
">
I am expecting the two outputs to match (as it's impacting my position calculations for carets during text entry). How would I go about getting the text layout manager to add the extra line fragment while using a custom NSTextContentManager
With my continued experiments with TextKit2, I'm trying to figure out what sets the properties paragraphSeparatorRange and paragraphContentRange on a NSTextParagraph. These seem to be computed properties (or at least they cannot be directly set via public API).
var paragraph = NSTextParagraph(attributedString: NSAttributedString(string: "It was the best of times.\n"))
print("attributes: \(paragraph.attributedString.attributes(at: 0, effectiveRange: nil))")
print("paragraphSeparatorRange: \(String(describing: paragraph.paragraphSeparatorRange))")
print("paragraphContentRange: \(String(describing: paragraph.paragraphContentRange))")
In the above example, both paragraphSeparatorRange and paragraphContentRange are nil.
However, when using NSTextLayoutManager/NSTextContentStorage they are set.
let layoutManager = NSTextLayoutManager()
let container = NSTextContainer(size: NSSize(width: 400, height: 400))
layoutManager.textContainer = container
let contentStorage = NSTextContentStorage()
contentStorage.textStorage = NSTextStorage(string: "It was the best of times.\n")
contentStorage.addTextLayoutManager(layoutManager)
layoutManager.enumerateTextLayoutFragments(from: contentStorage.documentRange.location, options: .ensuresLayout) { textLayoutFragment in
print("layoutFragment: \(textLayoutFragment)")
print("textElement: \(String(describing: textLayoutFragment.textElement))")
print("textElement.range: \(String(describing: textLayoutFragment.textElement?.elementRange))")
let paragraph = textLayoutFragment.textElement as! NSTextParagraph
print("paragraphContentRange: \(String(describing: paragraph.paragraphContentRange))")
print("paragraphSeparatorRange: \(String(describing: paragraph.paragraphSeparatorRange))")
return true
}
Would appreciate any ideas on how these values are computed/set. Thanks
I've been struggling with this for a while now. To start off with, I am using a custom NSTextLocation as the element range on a NSTextParagraph. NSTextParagraph is a subclass of NSTextElement and both have limited public elements that can be set. From what I can see, the only thing we can set on NSTextParagraph is an NSAttributedString. It has a few other elements it inherits from NSTextElement - primarily textContentManager and elementRange. Seems simple enough...but, there is obviously something wrong with what I am doing.
If we use NSTextStorage as our backing store, then the text ranges it uses is a private type NSCountableTextLocation. Being a private type, I imagine we have to implement our own NSTextLocation to use in the ranges that are set on the NSTextElement.
But, for some reason, when I have multiple text elements, TextKit compares my custom NSTextLocation against a NSCountableTextLocation and crashes.
-[NSCountableTextLocation compare:] receiving unmatching type (3, 1)
(3,1) here being the debug description text of my custom text location (represented by line number and column). I am at a complete loss as to why this is being triggered. I have implemented isEqual, Comparable and compare: on my custom implementation.
Going with the following Text
"""
First
Second
T
"""
I create a NSTextParagraph for each paragraph. So, I have elements
'First\n' - with range (1,1) - (2,1) - length of this range is 6
'\n' - with range (2,1) - (3,1) - length of this range is 1
'Second\n' with range (3,1) - (4,1) with length being 7
'\n' - range (4,1) - (5,1) with length 1
'T' - range (5,1) - (5,2) length 1
This bit renders and lays out fine. If I append a character at the last location, I get the exception and crash.
"""
First
Second
Th
"""
Exception being -[NSCountableTextLocation compare:] receiving unmatching type (3, 1)
The number of text elements and the equivalent lengths match what I would get with NSTextStorage (executed in a different playground). There are a few subtle differences I see when debugging with lldb. When I use NSTextStorage, the value of _attributedString on NSTextParagraph of the first text element (and all) is "First\n\nSecond\n\nTh" - essentially the full text. The value of attributedString is "First\n" essentially the range of text that matches the paragraph. The generated text layout fragment has the correct line fragment "First\n" and not the full text. I don't see a public way to setup the NSTextParagraph this way (nor am I able to do it correctly using setValue:forKey either). I don't know if this difference really matters.
So, my questions really are
Does NSTextParagraph support custom NSTextLocation? If so, how do we set it up correctly?
If not, do we subclass NSTextElement and do the layout ourselves?
I have a full minimal project that reproduces this at https://github.com/georgemp/TextLocationCrash but it's a bit of an involved read :-)
I have also filed reports using Feedback Assistant FB13547274
P.S. This crash only seems to occur on Sonoma (not on Monterrey or Ventura)
P.P.S - Thank you for getting this far in this lengthy read :-)