Search code examples
objective-ccocoaappkitnstextviewnslayoutmanager

Drawing outside content insets with drawGlyphsForGlyphRange


I'm trying to show some extra symbols next to lines in NSTextView, based on text attributes.

I have successfully subclassed NSLayoutManager, but it seems that layout manager can't draw outside the area set by textContainerInset.

Because my text view can potentially have a very long strings, I'm hoping to keep the drawing connected to displaying glyphs. Is there a way to trick the layout manager to be able to draw inside the content insets — or is there another method I use instead of drawGlyphsForGlyphRange?

I have tried calling super before and after drawing, as well as storing and not storing graphics state. I also attempted setDrawsOutsideLineFragment:YES for the glyphs, but with no luck.

Things like Xcode editor itself uses change markers, so I know that this is somehow doable, but it's very possible I'm looking from the wrong place.

My drawing method, simplified:

- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
    [super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
    
    NSTextStorage *textStorage = self.textStorage;
    NSTextContainer *textContainer = self.textContainers[0];
    
    NSRange glyphRange = glyphsToShow;
    NSSize offset = self.textContainers.firstObject.textView.textContainerInset;
        
    while (glyphRange.length > 0) {
        NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL], attributeCharRange, attributeGlyphRange;
        
        id attribute = [textStorage attribute:@"Revision" atIndex:charRange.location longestEffectiveRange:&attributeCharRange inRange:charRange];
        attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharRange actualCharacterRange:NULL];
        attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);

        if (attribute != nil) {
            [NSGraphicsContext saveGraphicsState];
            
            NSRect boundingRect = [self boundingRectForGlyphRange:attributeGlyphRange
                                                  inTextContainer:textContainer];
            
            // Find the top of the revision
            NSPoint point = NSMakePoint(offset.width - 20, offset.height + boundingRect.origin.y + 1.0);
            
            NSString *marker = @"*";
            
            [marker drawAtPoint:point withAttributes:@{
                NSForegroundColorAttributeName: NSColor.blackColor;
            }];
                                   
            [NSGraphicsContext restoreGraphicsState];
        }
                
        glyphRange.length = NSMaxRange(glyphRange) - NSMaxRange(attributeGlyphRange);
        glyphRange.location = NSMaxRange(attributeGlyphRange);
    }
}

Solution

  • The answer was much more simple than I anticipated.

    You can set lineFragmentPadding for the associated NSTextContainer to make more room for drawing in the margins. This has to be taken into account when setting insets for the text container.