0

So I am trying to write a custom NSView with text in it but not using NSTextView (since it will be more than just text). I can use Core Text to draw text, but now I also need to be able to do hit-testing and drawing of the caret and selection rects.

The naive way of drawing carets and selections, as shown by various Apple samples, is to get the typographic bounds of the various CTLines encompassing the desired string range (using CTLineGetBoundsWithOptions(0) instead of CTLineGetTypographicBounds() due to various errors in the latter) and filling the resultant rects. Unfortunately, as soon as there is a paragraph style of any sort (be it paragraph spacing, line spacing, or line height) this stops working because paragraph styles are not included in the typographic bounds. If you select text that spans multiple lines, you'll see that somewhat iconic white gap between the lines! And this still does not answer the question of how to know what line to use if I am clicking on the whitespace between lines.

After fighting over typographic bounds rects overlapping or having gaps between them due to how Core Text does snapping metrics points to integers by default, I noticed that if I call CTFrameGetLineOrigins() I can determine the height of line i + 1 by computing origins[i].y - origins[i + 1].y; the height of line 0 is the height of the frame minus the height of the combined other lines. The use of i + 1 here is important; it deals with odd scenarios caused by fonts that have the space height different from other character heights.

However, with this technique, I cannot differentiate between spacing above a line and spacing below a line, whether that space is paragraph or line space. All spacing is treated as being above the line, and if multiple spacing modes are used, they just combine. I have a small program that you can use to experiment with this for yourself; click the "Baseline Diffs" checkbox to shade in line heights.

So the question is: what am I missing that I can't figure out how tall a line really is and where its baseline is relative to its height, regardless of the paragraph style? Or do I need to reimplement the internal logic of Core Text myself if I want to do this? I do have a lot of the pieces together, but a lot of it is highly conditional...

And what happens on the last paragraph? Should I simulate the trailing space there?

The solution needs to run on OS X 10.8 or newer.

Thanks.

andlabs
  • 11,290
  • 1
  • 31
  • 52

1 Answers1

1

The big question you need to ask yourself is what you want the results of these things to be. The hight of the line really is just the hight of the line. It doesn't include the spacing between the lines. The typesetter doesn't have to make all the lines stack. Consider two-column layout situations where a line may be higher than the prior line. Or if there are page breaks; do you consider the page's margins to be part of the line-height? Core Text doesn't just handle "TextView-like things." It handles quite arbitrary layouts situations.

You're running into the hard question yourself of what line the space between the lines should belong to. That's a question for you to answer. Core Text doesn't know what you want. It doesn't know the meaning of any of these rectangles or how you mean them to work together.

So for the selection question, yes, you'd need to compute the full box by creating a union of all the line rectangles that are fully selected (plus the extra bits on the first and last line). That's how it's usually done for simple, single-column text; obviously it gets a bit more complicated if text can span multiple columns. You may also have to consider situations where text flows "backwards" (i.e. right-to-left inside generally left-to-right text).

It's a lot of work using only low-level Core Text primitives. I would first try to use NSLayoutManager instead and see if it's powerful enough for your purposes. If your problem is just that you need to wrap around (or skip over) graphics or other non-text elements, then NSLayoutManager may be able to do a lot of the work for you and let you still use a NSTextView. NSLayoutManager is highly subclassable to customize its behavior, and using it you can often keep Cocoa's built-in selection capabilities without reimplementing them.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Seconded. You should be more specific about what you're doing and in what way it is "more than just text". NSTextView can accommodate all sorts of things that are "more than just text", by modifying the text flow, or by using attachments in various ways. You're probably making a whole lot of work for yourself unnecessarily. – bhaller Jan 12 '17 at 00:39
  • All right, I see how I was misled; thanks. Though I do wonder why Core Text handles paragraph styles at all if this is the case… I did consider using Cocoa's APIs, but I'm not fully sure how to translate from Core Text to NSLayoutManager. And as for NSTextView, the view is an existing pure-graphics view that I want to add text editing to, rather than the other way around. I also know in my test program that evaluates the effort needed operates more like Slack Pages than like Word, so not strictly free-form rich text. I do wonder how malleable NSTextView is, though, since everything uses it... – andlabs Jan 12 '17 at 01:04
  • And admittedly whatever comes out of this will eventually become part of libui as well (though I do have a few personal things I want to make with it, as hinted at). – andlabs Jan 12 '17 at 03:05
  • I think I have a handle on how to use NSLayoutManager now. Not sure how kosher it would be to mix this and Core Graphics. Does AppKit provide a way to match individual trait values for font descriptor matching, or do I still need to do that manually? And I still wonder why Core Text has CTParagraphStyle if the only part that uses it is the line positioning and the semnatics are slightly different (it seems you can have a new CTParagraphStye anywhere on a line instead of paragraph-wide like NSParagraphStyle is)? – andlabs Jan 13 '17 at 18:26
  • s/anywhere on a line/anywhere on a paragraph (so long as that spot is the beginning of a CTLine)/ – andlabs Jan 13 '17 at 19:05