Search code examples
swiftmacosnstextviewnslayoutmanager

Accessing NSTextView metrics


I have an NSTextVIew in which I am only showing mono-spaced characters from the standard alphabet. So no numbers, special characters, emojis, etc. Each character equals one glyph. On top of the text I need to draw some shapes, and I am looking for a way to access some metrics from the text system:

  1. the distance from one character to the next one
  2. the distance from one line to the next one

See the picture for what I mean.

enter image description here

There don't seem to be any properties that I can use directly, or at least I haven't found them, so I am now use the text view's layoutManager to obtain these values:

For the first one I obtain the enclosing rects for two adjacent characters via the boundingRect(forGlyphRange glyphRange: NSRange, in container: NSTextContainer) -> NSRect method of the layoutmanager, and subtract the origin.x for both rects.

For the second one, I could use the same function, but then I need to know the range for the first character on the second line. Or iterate over all the characters and once the origin.y of the enclosing rect changes, I have the first character on the second line and I can calculate the distance between two lines.

EDIT : here's possible code using the layoutManager:

typealias TextMetrics = (distanceBetweenCharacters: CGFloat, distanceBetweenLines: CGFloat)

var metrics: TextMetrics = self.textMetrics() // need to update when text changes

    func textMetrics() -> TextMetrics {
        guard let lm = self.layoutManager,
        let tc = self.textContainer
        else { return (0,0)
        }

        var distanceBetweenCharacters: CGFloat = 0.0
        var distanceBetweenLines: CGFloat = 0.0

        if string.count > 2 {
            let firstRect = lm.boundingRect(forGlyphRange: NSRange(location: 0, length: 1), in: tc)
            let secondRect = lm.boundingRect(forGlyphRange: NSRange(location: 1, length: 1), in: tc)
            distanceBetweenCharacters = secondRect.maxX - firstRect.maxX

            for (index, _) in string.enumerated() {
                let rect = lm.boundingRect(forGlyphRange: NSRange(location: index, length: 1), in: tc)
                if rect.maxY > firstRect.maxY { // reached next line
                    distanceBetweenLines = rect.maxY - firstRect.maxY
                    break
                }
            }
        }

        return (distanceBetweenCharacters, distanceBetweenLines)
    }

I also looked at getting these from the defaultParagraphStyle, but if I access that, it is nil.

Is there maybe another, easier way to obtain these values?


Solution

  • After some more searching and trial and error, I found that distanceBetweenLines could be calculated from the font metrics and the lineHeightMultiple, which is a property from NSParagraphStyle, but it could also be defined outside the paragraph style, which is what I do.

    So in the end this works:

    let distanceBetweenLines = layoutManager.defaultLineHeight(for: myTextFont) * lineHeightMultiple
    

    For distanceBetweenCharacters, I have not found another solution.

    EDIT

    Based on the suggestion from @Willeke in the comment below, I now calculate distanceBetweenCharacters as follows:

    let distanceBetweenCharacters = myTextFont.advancement(forCGGlyph: layoutManager.cgGlyph(at: 0)).width + myKerning