New lines not generating a textLineFragment when using a custom NSTextContentManager

93 Views Asked by At

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

  1. 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 "">
  1. 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.

  1. NSTextParagraph.paragraphSeparatorRange is not settable by any public method. It has to be via unsupported setValue(_:forKey:). I have a separate thread here on this.

  2. By the looks of it, the type of NSTextRange has to be NSCountableTextRange. Any other kind of NSTextRange does not generate the NSTextLayoutFragment. Unfortunately, NSCountableTextRange is private as well. Even if we could use it, I guess there would be a mismatch between the NSTextParagraph's elementRange type and that being used in the parent document.

0

There are 0 best solutions below