Cocoa Text System everywhere…

By uliwitness

Sometimes, you need to draw text with more control than an NSTextField or NSTextView will let you do, and sometimes you need better performance than the NSStringDrawing category will provide. And maybe you need to draw text into a CGContext or even inside a Carbon application.

You may be thinking about CoreText right now, and how unfortunate it is that you still have to maintain compatibility with Mac OS X 10.4 “Tiger”, but there’s an easier way:

The Cocoa Text System

Just use the Cocoa Text System. The Cocoa Text System is a group of classes that NSTextView, NSTextField and NSStringDrawing use to actually draw strings. Now, if you look at Apple’s docs, you’ll first be frightened by how complex this whole system seems to be: there are NSLayoutManagers, NSTextStorages, NSTextContainers, NSGlyphGenerators, NSTypesetters… but don’t fear, it’s much easier than it looks.

Apple actually provides a great introduction in the Drawing Text with NSLayoutManager entry of the Drawing Strings Task Documentation.

If you read that, you’ll see that you actually only need to deal with three of these classes to draw any styled Unicode string wrapped to a text box: NSLayoutManager is kind of the main controller. The string to be drawn and its attributes are stored in an NSTextStorage, and the extents of the area to draw into is specified by the NSTextContainer. What’s more, once you’ve created these objects, you can cache them, and thus speed up repeated drawing of the same string significantly.

You create these objects using +alloc/-init as usual, then tell the layout manager to take ownership of the text container, and the text storage to take ownership of the layout manager. Here’s essentially Apple’s example:

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:@"This is the text string."];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] init];
[layoutManager addTextContainer:textContainer];
[textContainer release];
[textStorage addLayoutManager:layoutManager];
[layoutManager release];

// Use the objects.

[textStorage release];

To actually draw this example string, you simply specify the size of the area to draw in, then ask the layout manager for the range of glyphs to draw, and then tell it to draw those at whatever position you want:

[textContainer setContainerSize: rect.size];
NSRange glyphRange = [layoutManager glyphRangeForTextContainer: textContainer];
[layoutManager drawGlyphsForGlyphRange: glyphRange atPoint: rect.origin];

Pretty simple, isn’t it? Two things to watch out for here: Whenever you change the size of the text container, the text needs to be re-wrapped (“re-layouted”), which is a sort of expensive operation that can be sped up by caching. Second, to actually trigger such a re-layout, you call -glyphRangeForTextContainer:.

For drawing styles and images, you simply take advantage of the fact that the NSTextStorage is actually a subclass of NSMutableAttributedString. So, it’s trivial to assign styles, fonts, colors and add text attachments.

Measuring Text

For example, if you want to measure how much space text will use, set the text container to have the height or width of the fixed side, and set the other side of the rect to some huge value, like FLT_MAX. Then, you call -glyphRangeForTextContainer: to make sure the text has been layouted, and then call -usedRectForTextContainer: to get the actual dimensions of the text:

[textContainer setContainerSize: NSMakeSize([self bounds].size.width, FLT_MAX)];
(NSRange) [layoutManager glyphRangeForTextContainer: textContainer]; // Cause re-layout.
NSRect neededBox = [layoutManager usedRectForTextContainer: textContainer];

The Cocoa Text System in Quartz or Carbon

Carbon uses the straight Quartz APIs. People who want to draw into OpenGL textures, PDF contexts and other custom locations often also use Quartz directly. So, what if you want to use the above text drawing methods in Quartz? You don’t have a an NSGraphicsContext, you only have a CGContext. What to do? Well, easy. Behind every NSGraphicsContext, there is a CGContext, and for every CGContext, you can create an NSGraphicsContext. So, if you have the CGContextRef in a variable named inContext, you can easily do:

[NSGraphicsContext saveGraphicsState];
NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithGraphicsPort: inContext flipped: true];
[NSGraphicsContext setCurrentContext: context];

// Do Cocoa drawing here.

[NSGraphicsContext restoreGraphicsState];

Of course, if you’re doing this in Carbon, take care to catch any NSExceptions and create an autorelease pool around calls like these, you wouldn’t want to leak Cocoa objects or have a Cocoa exception waltz through the Carbon system libraries.

The neat part about this is not only that the Cocoa Text System is much simpler than ATSUI, but also that the Cocoa Text system is very similar in design to CoreText. So, if you want to move to CoreText a couple releases from now, you’ll just have to swap out a few API calls, but the general workings will stay pretty much the same.

Share your thoughts