1

I am trying to render text in a NSView canvas. I need to write three lines of text and ignore what's beyond. String.draw(in:withAttributes) with a provided rect seems perfect to do it. My code looks like this:

func renderText(_ string:String, x:Double, y:Double, numberOfLines: Int, withColor color:Color) -> Double {
    let font = NSFont.boldSystemFont(ofSize: 11)
    let lineHeight = Double(font.ascender + abs(font.descender) + font.leading)
    let textHeight = lineHeight * Double(numberOfLines) + font.leading // three lines
    let textRect = NSRect(x: x, y: y, width: 190, height: textHeight)
    string.draw(in: textRect, withAttributes: [NSFontAttributeName: font, NSForegroundColorAttributeName: color])
    return textHeight
}

renderText("Lorem ipsum...", x: 100, y: 100, numberOfLines: 3, withColor: NSColor.white)

Without adjustments, I get only two lines of text rendered:

enter image description here

I am following these guidelines: https://developer.apple.com/library/content/documentation/TextFonts/Conceptual/CocoaTextArchitecture/FontHandling/FontHandling.html#//apple_ref/doc/uid/TP40009459-CH5-SW18

I am missing something?

Béatrice Cassistat
  • 1,048
  • 12
  • 37

3 Answers3

2

Ultimately your text makes it to the screen by calling upon the classes that comprise Cocoa's text architecture, so it makes sense to get information about line height directly from these classes. In the code below I've created an NSLayoutManager instance, and set its typesetter behaviour property to match the value of the typesetter that is ultimately used by the machinery created by the function drawInRect:withAttributes:. Calling the layout manager's defaultLineHeight method then gives you the height value you're after.

lazy var layoutManager: NSLayoutManager = {
    var layoutManager = NSLayoutManager()
    layoutManager.typesetterBehavior = .behavior_10_2_WithCompatibility
    return layoutManager
}()

func renderText(_ string:String, x:Double, y:Double, numberOfLines: Int, withColor color:NSColor) -> Double {
    let font = NSFont.boldSystemFont(ofSize: 11)
    let textHeight = Double(layoutManager.defaultLineHeight(for: font)) * Double(numberOfLines)
    let textRect = NSRect(x: x, y: y, width: 190, height: textHeight)
    string.draw(in: textRect, withAttributes: [NSFontAttributeName: font, NSForegroundColorAttributeName: color])
    return textHeight
}
Paul Patterson
  • 6,840
  • 3
  • 42
  • 56
2

You are writing your text into a precise typographic bounds, but the system may adjust the size of the text for on-screen presentation to make the text more legible (e.g. substitute screen fonts for for vector fonts). Using an exact typographic bounds can also cause problems for characters with ascenders or descenders that fall outside of bounds. For example the "A-ring" character or a capital E with an grave accent.

To find the bounds of text using rules of the CGContext that it will be drawn in I suggest boundingRectWithSize:options:context: (for NSAttributedString) and boundingRectWithSize:options:attributes:context: (for NSString)

Scott Thompson
  • 22,629
  • 4
  • 32
  • 34
1

I would try adding a small delta to textHeight -- Doubles are perfectly accurate.

Lou Franco
  • 87,846
  • 14
  • 132
  • 192
  • I need to add about 4 points (`textHeight = ... + 4`) to make the third line appear. This is a huge difference, I would like to know if my lineHeight calculation are correct. On the iOS version of the app, the same code without the +4 adjustment seems to be working in the iOS simulator. – Béatrice Cassistat May 03 '17 at 15:41
  • 1
    You are writing your text into a precise typographic bounds, but the system may adjust the size of the text for on-screen presentation to make the text more legible (e.g. substitute screen fonts for for vector fonts). Using an exact typographic bounds can also cause problems for characters with ascenders or descenders that fall outside of bounds. For example the "A-ring" character or a capital E with an grave accent. You need to find the routine that can measure the height of the text for a particular context... I will see if I can rack my brain from my way-back machine to recall it. – Scott Thompson May 03 '17 at 15:57
  • 2
    Try boundingRectWithSize:options:context: (for NSAttributedString) and boundingRectWithSize:options:attributes:context: (for NSString) – Scott Thompson May 03 '17 at 16:02
  • @ScottThompson Thank you, it helped. I may accept if you write it as an answer. I was expecting font metrics to provide an accurate height with more performances, but maybe I could use some cache. – Béatrice Cassistat May 03 '17 at 18:02