cocoanstextviewtextkit

Exclusion rects not shifting text properly with NSTextView/UITextView


I have an NSTextView with some exclusion rects so I can shift the text vertically without having to resort to adding newlines to the string. However, I've noticed that the exclusion rect is rather limited or perhaps even buggy because I cannot shift the text vertically more than ~45% of the textview's height. I have found a workaround where I increase the textview's height by 2x but this feels gross and I'd rather do it "properly"

enter image description here

In the image above, I have three text views (from left to right)

  1. Programmatically without encapsulating it inside an NSScrollView, 2x height
  2. Programmatically with NSScrollView encapsulation. 2x height
  3. Using interface builder, regular height.

The exclusion rect is drawn with a CAShapeLayer and you can see that "hello world new world" isn't properly positioned outside of the exclusion rect.

I tried all three examples to make sure I wasn't missing something with regards to IB default settings or the dynamics of the text view when encapsulated in an NSScrollView (Im new to AppKit and TextKit), however all 3 examples exhibit the same bug.

Code to update the text exclusion rect

(Each time the slider moves on the bottom right, it will update their text rect)

label.stringValue = "excl: \(sender.integerValue): height: \(view.frame.height)"
print(sender.integerValue)

let exclHeight: CGFloat = CGFloat(slider.integerValue)


[aTextView, bTextView, cTextView]
  .compactMap { $0 }
  .forEach {
    let rect = CGRect(
      x: 5,
      y: 5,
      width: aTextView.frame.width - 10,
      height: exclHeight)
    $0.wantsLayer = true
    $0.textContainer?.exclusionPaths.removeAll()
    $0.textContainer?.exclusionPaths.append(.init(rect: rect))
    $0.layer?.sublayers?.forEach {
      $0.removeFromSuperlayer()
    }

    let layer = CAShapeLayer()
    layer.frame = rect
    layer.backgroundColor = NSColor.blue.withAlphaComponent(0.2).cgColor
    layer.borderWidth = 1
    layer.borderColor = NSColor.white.cgColor
    $0.layer?.addSublayer(layer)
  }
}

Solution

  • The problem seems to be that the exclusionPath is before the first line.

    Just playing around with the parameters, a two line sample text with the rect y positioned after the first line works without any problems.

    So it looks like the issue is calculating the container height when it starts with a exclusionPaths in -[NSLayoutManager usedRectForTextContainer:]

    @interface LOLayoutManager : NSLayoutManager
    @end
    
    @implementation LOLayoutManager
    
    - (NSRect)usedRectForTextContainer:(NSTextContainer *)container
    {
        NSRect rect = [super usedRectForTextContainer:container];
        NSRect newRect = NSMakeRect(0, 0, NSMaxX(rect), NSMaxY(rect));
        return newRect;
    }
    
    @end
    

    Instead of returning y position from the exclusionPaths and the line fragments height, this returns a big rect starting at 0, 0. This should work as long as the NSTextView only contains one text container.