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 outputs
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
Update
Based on additional testing, it looks like if NSTextParagraph has paragraphSeparatorRange set on it, then NSTextLayoutManager generates the additional text line fragment. There seem to be a couple of issues here.
NSTextParagraph.paragraphSeparatorRangeis not settable by any public method. It has to be via unsupportedsetValue(_:forKey:). I have a separate thread here on this.By the looks of it, the type of
NSTextRangehas to beNSCountableTextRange. Any other kind ofNSTextRangedoes not generate theNSTextLayoutFragment. Unfortunately,NSCountableTextRangeis private as well. Even if we could use it, I guess there would be a mismatch between theNSTextParagraph's elementRange type and that being used in the parent document.