NSAttributedString + NSLayoutManager versus Core Text


Graham Cox
 

Hi all,

I’m wrestling with annoying inconsistencies in the various options for text rendering.

Here’s what I want to do - I want to draw overbars above runs of characters in a text string. This is so that I can label electronic components according to the usual convention that an active low I/O port has an overbar, either above certain characters, or above a whole string.

An obvious way to do this is to use NSAttributedString with custom attributes for the overbar attribute, and then override the glyph drawing method in a subclass of NSLayoutManager to get the overbar attribute, compute where the line goes and draw it. That works really well.

However, the way NSAttributedString actually draws is strange - text is always offset to the right of the origin of the rectangle that you want to draw it into. It’s only a few pixels, but it’s more than enough to misplace the text especially with small font sizes. The offset seems to vary with font also. Because the text container has the size of the rectangle, this offset means that text is typically clipped or wrapped by the right edge when it should not be, because the whole string was drawn too far right. I can fudge the origin, but then I change the font and those numbers are wrong.

I also built a solution using Core Text. This does not exhibit the same behaviour - lines draw exactly where you tell them, not offset to the right. However, I can’t see a way to use Core Text to draw my overbars - there’s no override point for drawing additional lines after rendering the glyphs. (Unless I’m just not seeing it).

Another oddity with NSAttributedString is that when there is a paragraph style with left/centre/right alignment variations, this has no effect on the text positioning. Since for the actual glyph rendering I’m simply calling super’s implementation, it should surely work properly. 

My question is why does NSAttributedString render like this, and what should I be doing about it? Or alternatively, is there a Core Text way to do this?

Here’s the code for drawing the string, in a category on NSAttributedString:

- (void) drawWithOverbarInRect:(CGRect) rect inContext:(CGContextRef) context
{
// to draw the string, a subclass of NSLayoutManager is required


NSTextStorage* tempStorage = [[NSTextStorage alloc] initWithAttributedString:self];


GCOverbarLayoutManager* layManager = [GCOverbarLayoutManager sharedOverbarLayoutManager];


NSTextContainer* container = [[NSTextContainer alloc] initWithSize:rect.size];
container.lineBreakMode = NSLineBreakByClipping;
[layManager addTextContainer:container];
[container release];


[tempStorage addLayoutManager:layManager];

[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithCGContext:context flipped:YES]];


NSRange glyphRange = [layManager glyphRangeForTextContainer:container];
NSPoint textOrigin = rect.origin;


// textOrigin is the top,left of the rect - but in fact this positions glyphs too far to the right and clips off characters at the right edge


if( glyphRange.length > 0 )
{
[layManager drawBackgroundForGlyphRange:glyphRange atPoint:textOrigin];
[layManager drawGlyphsForGlyphRange:glyphRange atPoint:textOrigin];
}

[NSGraphicsContext restoreGraphicsState];
[layManager removeTextContainerAtIndex:0];
[tempStorage removeLayoutManager:layManager];
[tempStorage release];
}

And here’s the code in the NSLayoutManager subclass that actually renders the overbars:

- (void) showCGGlyphs:(const CGGlyph*) glyphs 
positions:(const CGPoint*) positions 
count:(NSInteger) glyphCount 
font:(NSFont*) font 
  textMatrix:(CGAffineTransform) textMatrix 
  attributes:(NSDictionary<NSAttributedStringKey,id>*) attributes 
inContext:(CGContextRef) CGContext
{
// this overrides the layout manager's standard method to add the overbar if the attribute indicates it


[super showCGGlyphs:glyphs
  positions:positions
  count:glyphCount
  font:font
textMatrix:textMatrix
attributes:attributes
  inContext:CGContext];


// attributes for overbar?


CGFloat overBarWidth = [[attributes objectForKey:GCOverbarAttributeName] doubleValue];


if( overBarWidth > 0.0 )
{
// there's an overbar to draw here - custom colour? If not, use text colour, or black, whatever we have info about


NSColor* overbarColour = [attributes objectForKey:GCOverbarColorAttributeName];

if( overbarColour == nil )
overbarColour = [attributes objectForKey:NSForegroundColorAttributeName];


if( overbarColour == nil )
overbarColour = [NSColor blackColor];


// need to compute the horizontal length of the bar. This can be deduced from the positions array
// the last glyph rendered also needs to be considered - we need its width


NSRect firstGlyphRect = [font boundingRectForCGGlyph:glyphs[0]];
NSRect lastGlyphRect = [font boundingRectForCGGlyph:glyphs[glyphCount - 1]];
CGPoint obPoints[2];


obPoints[0].x = NSMinX( firstGlyphRect ) + positions[0].x - 0.5;
obPoints[0].y = obPoints[1].y = positions[0].y - font.ascender - overBarWidth * font.xHeight;
obPoints[1].x = NSMaxX( lastGlyphRect ) + positions[glyphCount -1].x + 0.5;


CGContextSaveGState( CGContext );
CGContextSetTextMatrix( CGContext, textMatrix );


CGContextSetLineWidth( CGContext, overBarWidth * font.xHeight );
CGContextSetStrokeColorWithColor( CGContext, overbarColour.CGColor );
CGContextSetLineCap( CGContext, kCGLineCapButt );


CGContextStrokeLineSegments( CGContext, obPoints, 2 );
CGContextRestoreGState( CGContext );
}
}


—Graham



Join cocoa@apple-dev.groups.io to automatically receive all group messages.