Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Control spacing around custom text attributes in NSLayoutManager

I’ve got a custom NSLayoutManager subclass I’m using to draw pill-shaped tokens. I draw these tokens for substrings with a custom attribute (TokenAttribute). I can draw no problem.

However, I need to add a little bit of “padding” around the ranges with my TokenAttribute (so that the round rectangle background of the token won’t intersect with the text).

enter image description here

In the above image, I’m drawing my token’s background with an orange colour, but I want extra padding around 469 so the background isn’t right up against the text.

I’m not really sure how to do this. I tried overriding -boundingRectForGlyphRange:inTextContainer: to return a bounding rect with more horizontal padding, but it appears the layout of glyphs isn’t actually affected by this.

How do I give more spacing around certain glyphs / ranges of glyphs?


Here’s the code I use to draw the background, in my layout manager subclass:

- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {

    NSTextStorage *textStorage = self.textStorage;
    NSRange glyphRange = glyphsToShow;

    while (glyphRange.length > 0) {

        NSRange characterRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
        NSRange attributeCharacterRange;
        NSRange attributeGlyphRange;

        id attribute = [textStorage attribute:LAYScrubbableParameterAttributeName 
                                      atIndex:characterRange.location 
                        longestEffectiveRange:&attributeCharacterRange 
                                      inRange:characterRange];

        attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharacterRange 
                                           actualCharacterRange:NULL];
        attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);

        if (attribute != nil) {
            CGContextRef context = UIGraphicsGetCurrentContext();
            CGContextSaveGState(context);

            UIColor *backgroundColor = [UIColor orangeColor];
            NSTextContainer *textContainer = self.textContainers[0];
            CGRect boundingRect = [self boundingRectForGlyphRange:attributeGlyphRange inTextContainer:textContainer];

            // Offset this bounding rect by the `origin` passed in above
            // `origin` is the origin of the text container!
            // if we don't do this, then bounding rect is incorrectly placed (too high, in my case).
            boundingRect.origin.x += origin.x;
            boundingRect.origin.y += origin.y;

            [backgroundColor setFill];
            UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:boundingRect cornerRadius:boundingRect.size.height / 2.0];
            [path fill];

            [super drawGlyphsForGlyphRange:attributeGlyphRange atPoint:origin];
            CGContextRestoreGState(context);

        } else {
            [super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
        }

        glyphRange.length = NSMaxRange(glyphRange) - NSMaxRange(attributeGlyphRange);
        glyphRange.location = NSMaxRange(attributeGlyphRange);
    }
}
like image 708
jbrennan Avatar asked Dec 29 '15 03:12

jbrennan


1 Answers

There are methods defined in NSLayoutManagerDelegate, that serve as glyph-based customisation points.

Use

func layoutManager(_ layoutManager: NSLayoutManager, shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>, properties props: UnsafePointer<NSLayoutManager.GlyphProperty>, characterIndexes charIndexes: UnsafePointer<Int>, font aFont: NSFont, forGlyphRange glyphRange: NSRange) -> Int

to identify the glyphs associated with the whitespace surrounding your range-of-interest and mark those by altering their value in the props array to NSLayoutManager.GlyphProperty.controlCharacter. Then pass this altered array to

NSLayoutManager.setGlyphs(_:properties:characterIndexes:font:forGlyphRange:)

Afterwards, you may implement

func layoutManager(_ layoutManager: NSLayoutManager, shouldUse action: NSLayoutManager.ControlCharacterAction, forControlCharacterAt charIndex: Int) -> NSLayoutManager.ControlCharacterAction

to again identify the glyphs of interest and return the predefined action:

NSLayoutManager.ControlCharacterAction.whitespace

This, at the end, lets you implement

func layoutManager(_ layoutManager: NSLayoutManager, boundingBoxForControlGlyphAt glyphIndex: Int, for textContainer: NSTextContainer, proposedLineFragment proposedRect: NSRect, glyphPosition: NSPoint, characterIndex charIndex: Int) -> NSRect

to alter the bounding box used for the glyphs. Simply return the appropriated dimensions. This will have effect on the following layout-mechanism.

Good luck!

like image 80
Peer Benderson Avatar answered Nov 03 '22 04:11

Peer Benderson