What is the recommended approach to rendering text with line numbers in TextKit 2?
Text with line numbers in TextKit 2
I don't know if this is recommended, however since nobody picked up this questions, this is what I do:
I assume you use NSRulerView
as an NSScrollView.verticalRulerView
property. What I do, is override drawHashMarksAndLabels(in:)
and use CoreText to draw numbers in the positions from NSTextLayoutManager
class LineNumberRulerView: NSRulerView {
private weak var textView: NSTextView?
init(textView: NSTextView) {
self.textView = textView
super.init(scrollView: textView.enclosingScrollView!, orientation: .verticalRuler)
clientView = textView.enclosingScrollView!.documentView
NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: textView, queue: nil) { [weak self] _ in
self?.needsDisplay = true
}
NotificationCenter.default.addObserver(forName: NSText.didChangeNotification, object: textView, queue: nil) { [weak self] _ in
self?.needsDisplay = true
}
}
public override func drawHashMarksAndLabels(in rect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext,
let textView = textView,
let textLayoutManager = textView.textLayoutManager
else {
return
}
let relativePoint = self.convert(NSZeroPoint, from: textView)
context.saveGState()
context.textMatrix = CGAffineTransform(scaleX: 1, y: isFlipped ? -1 : 1)
let attributes: [NSAttributedString.Key: Any] = [
.font: textView.font!,
.foregroundColor: NSColor.secondaryLabelColor
]
var lineNum = 1
textLayoutManager.enumerateTextLayoutFragments(from: nil, options: .ensuresLayout) { fragment in
let fragmentFrame = fragment.layoutFragmentFrame
for (subLineIdx, textLineFragment) in fragment.textLineFragments.enumerated() where subLineIdx == 0 {
let locationForFirstCharacter = textLineFragment.locationForCharacter(at: 0)
let ctline = CTLineCreateWithAttributedString(CFAttributedStringCreate(nil, "\(lineNum)" as CFString, attributes as CFDictionary))
context.textPosition = fragmentFrame.origin.applying(.init(translationX: 4, y: locationForFirstCharacter.y + relativePoint.y))
CTLineDraw(ctline, context)
}
lineNum += 1
return true
}
context.restoreGState()
}
}
Hi krzyzanowskim,
thanks for the answer, It worked fine for me. It encounters correctly line wraps due to the width of the view and scrolling works as well.
Thanks very much!
Hi krzyzanowskim,
Thanks for posting this. I was struggling to make the right associations between the notions of text storage vs. layout fragments, redraw notifications, etc., and also hadn't thought of using an NSRulerView for the numbers. Your approach here is much more concise and easy to implement than the one I was pursuing, and provided a lot of clarity.