Refactor line number view

This commit is contained in:
1024jp 2024-04-06 16:53:53 +09:00
parent 532f0b4ebb
commit e9118292d2
3 changed files with 57 additions and 84 deletions

View File

@ -268,47 +268,40 @@ final class LineNumberView: NSView {
}
// draw labels
let options: NSTextView.LineEnumerationOptions = isVerticalText ? [.bySkippingWrappedLine] : []
textView.enumerateLineFragments(in: textView.visibleRect, options: options) { (lineRect, line, lineNumber) in
let y = scale * -lineRect.minY
textView.enumerateLineFragments(in: textView.visibleRect) { (lineRect, lineNumber, isSelected) in
let y = (scale * -lineRect.minY) - lineOffset
switch line {
case .new(let isSelected):
// draw line number
if !isVerticalText || isSelected || lineNumber.isMultiple(of: 5) || lineNumber == 1 || lineNumber == self.numberOfLines {
let digits = lineNumber.digits
// calculate base position
let basePosition: CGPoint = isVerticalText
? CGPoint(x: y - lineOffset + drawingInfo.charWidth * CGFloat(digits.count) / 2, y: 3 * drawingInfo.tickLength)
: CGPoint(x: -drawingInfo.padding, y: y - lineOffset)
// get glyphs and positions
let positions: [CGPoint] = digits.indices
.map { basePosition.offsetBy(dx: -CGFloat($0 + 1) * drawingInfo.charWidth) }
let glyphs: [CGGlyph] = digits
.map { drawingInfo.digitGlyphs[$0] }
// draw
if isSelected {
context.setFillColor(self.foregroundColor(.bold).cgColor)
context.setFont(self.boldLineNumberFont)
}
context.showGlyphs(glyphs, at: positions)
if isSelected {
context.setFillColor(self.foregroundColor().cgColor)
context.setFont(Self.lineNumberFont)
}
}
// draw tick
if isVerticalText {
let rect = CGRect(x: (y - lineOffset).rounded() + 0.5, y: 1, width: 0, height: drawingInfo.tickLength)
context.stroke(rect, width: scale)
}
case .wrapped:
break
// draw tick
if isVerticalText {
let rect = CGRect(x: y.rounded() + 0.5, y: 1, width: 0, height: drawingInfo.tickLength)
context.stroke(rect, width: scale)
}
// skip intermediate lines by vertical orientation
guard !isVerticalText || isSelected || lineNumber.isMultiple(of: 5) || lineNumber == 1 || lineNumber == self.numberOfLines else { return }
let digits = lineNumber.digits
// calculate base position
let basePosition = isVerticalText
? CGPoint(x: y + drawingInfo.charWidth * Double(digits.count) / 2, y: 3 * drawingInfo.tickLength)
: CGPoint(x: -drawingInfo.padding, y: y)
// get glyphs and positions
let positions: [CGPoint] = digits.indices
.map { basePosition.offsetBy(dx: -Double($0 + 1) * drawingInfo.charWidth) }
let glyphs: [CGGlyph] = digits
.map { drawingInfo.digitGlyphs[$0] }
// draw number
if isSelected {
context.setFillColor(self.foregroundColor(.bold).cgColor)
context.setFont(self.boldLineNumberFont)
}
context.showGlyphs(glyphs, at: positions)
if isSelected {
context.setFillColor(self.foregroundColor().cgColor)
context.setFont(Self.lineNumberFont)
}
}
}

View File

@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2018-2023 1024jp
// © 2018-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -27,19 +27,11 @@ import AppKit
extension NSTextView {
enum Line {
case new(_ isSelected: Bool)
case wrapped
}
struct LineEnumerationOptions: OptionSet {
let rawValue: Int
static let bySkippingWrappedLine = Self(rawValue: 1 << 0)
static let bySkippingExtraLine = Self(rawValue: 1 << 1)
static let bySkippingExtraLine = Self(rawValue: 1 << 0)
}
@ -64,72 +56,60 @@ extension NSTextView {
/// - options: The options to skip invoking `body` in some specific fragments.
/// - body: The closure executed for each line in the enumeration.
/// - lineRect: The line fragment rect.
/// - line: The information of the line.
/// - lineNumber: The number of logical line (1-based).
final func enumerateLineFragments(in rect: NSRect, for range: NSRange? = nil, options: LineEnumerationOptions = [], body: (_ lineRect: NSRect, _ line: Line, _ lineNumber: Int) -> Void) {
/// - isSelected: Whether the line is selected.
final func enumerateLineFragments(in rect: NSRect, for range: NSRange? = nil, options: LineEnumerationOptions = [], body: (_ lineRect: NSRect, _ lineNumber: Int, _ isSelected: Bool) -> Void) {
guard
let layoutManager = self.layoutManager,
let textContainer = self.textContainer
else { return assertionFailure() }
// get glyph range of which line number should be drawn
// get range of which line number should be drawn
// -> Requires additionalLayout to obtain glyphRange for markedText. (2018-12 macOS 10.14 SDK)
guard let glyphRangeToDraw: NSRange = {
guard let rangeToDraw: NSRange = {
let layoutRect = rect.offset(by: -self.textContainerOrigin)
let rectGlyphRange = layoutManager.glyphRange(forBoundingRect: layoutRect, in: textContainer)
let rectRange = layoutManager.characterRange(forGlyphRange: rectGlyphRange, actualGlyphRange: nil)
guard let range else { return rectGlyphRange }
guard let range else { return rectRange }
return layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil).intersection(rectGlyphRange)
return rectRange.intersection(range)
}() else { return }
let string = self.string as NSString
let selectedRanges = (self.rangesForUserTextChange ?? self.selectedRanges).map(\.rangeValue)
// count up lines until the interested area
let firstIndex = layoutManager.characterIndexForGlyph(at: glyphRangeToDraw.lowerBound)
var lineNumber = self.lineNumber(at: firstIndex)
var index = rangeToDraw.lowerBound
var lineNumber = self.lineNumber(at: index)
// enumerate visible line numbers
var glyphIndex = glyphRangeToDraw.lowerBound
while glyphIndex < glyphRangeToDraw.upperBound { // process logical lines
let characterIndex = layoutManager.characterIndexForGlyph(at: glyphIndex)
let lineRange = self.lineRange(at: characterIndex)
let lineGlyphRange = layoutManager.glyphRange(forCharacterRange: lineRange, actualCharacterRange: nil)
while index < rangeToDraw.upperBound { // process logical lines
let lineRange = self.lineRange(at: index)
let lineGlyphIndex = layoutManager.glyphIndexForCharacter(at: lineRange.lowerBound)
let lineRect = layoutManager.lineFragmentRect(forGlyphAt: lineGlyphIndex, effectiveRange: nil, withoutAdditionalLayout: true)
let isSelected = selectedRanges.contains { $0.intersects(lineRange) }
|| (lineRange.upperBound == string.length &&
lineRange.upperBound == selectedRanges.last?.upperBound &&
layoutManager.extraLineFragmentRect.isEmpty)
glyphIndex = lineGlyphRange.upperBound
var wrappedLineGlyphIndex = max(lineGlyphRange.lowerBound, glyphRangeToDraw.lowerBound)
while wrappedLineGlyphIndex < min(glyphIndex, glyphRangeToDraw.upperBound) { // process visually wrapped lines
var fragmentGlyphRange = NSRange.notFound
let lineRect = layoutManager.lineFragmentRect(forGlyphAt: wrappedLineGlyphIndex, effectiveRange: &fragmentGlyphRange, withoutAdditionalLayout: true)
let isWrapped = fragmentGlyphRange.lowerBound > lineGlyphRange.lowerBound
if options.contains(.bySkippingWrappedLine), isWrapped { break }
let line: Line = isWrapped ? .wrapped : .new(isSelected)
body(lineRect, line, lineNumber)
wrappedLineGlyphIndex = fragmentGlyphRange.upperBound
}
body(lineRect, lineNumber, isSelected)
index = lineRange.upperBound
lineNumber += 1
}
guard
!options.contains(.bySkippingExtraLine),
(!layoutManager.isValidGlyphIndex(glyphRangeToDraw.upperBound) || lineNumber == 1),
(rangeToDraw.upperBound == string.length || lineNumber == 1),
layoutManager.extraLineFragmentTextContainer != nil
else { return }
let lastLineNumber = (lineNumber > 1) ? lineNumber : self.lineNumber(at: string.length)
lineNumber = (lineNumber > 1) ? lineNumber : self.lineNumber(at: string.length)
let isSelected = (selectedRanges.last?.lowerBound == string.length)
body(layoutManager.extraLineFragmentRect, .new(isSelected), lastLineNumber)
body(layoutManager.extraLineFragmentRect, lineNumber, isSelected)
}

View File

@ -9,7 +9,7 @@
// ---------------------------------------------------------------------------
//
// © 2004-2007 nakamuxu
// © 2014-2023 1024jp
// © 2014-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -226,13 +226,13 @@ final class PrintTextView: NSTextView, Themable {
}
let range = (self.layoutManager as? PrintLayoutManager)?.visibleRange
self.enumerateLineFragments(in: dirtyRect, for: range, options: [.bySkippingWrappedLine, .bySkippingExtraLine]) { (lineRect, _, lineNumber) in
self.enumerateLineFragments(in: dirtyRect, for: range, options: .bySkippingExtraLine) { (lineRect, lineNumber, _) in
// draw number only every 5 times
let numberString = (!isVerticalText || lineNumber == 1 || lineNumber.isMultiple(of: 5)) ? String(lineNumber) : "·"
// adjust position to draw
let width = CGFloat(numberString.count) * numberSize.width
let point: NSPoint = isVerticalText
let point = isVerticalText
? NSPoint(x: -lineRect.midY - width / 2,
y: horizontalOrigin - numberSize.height)
: NSPoint(x: horizontalOrigin - width, // - width to align to right