mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-11-10 10:34:52 +03:00
Refactor line number view
This commit is contained in:
parent
532f0b4ebb
commit
e9118292d2
@ -268,28 +268,32 @@ 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
|
||||
|
||||
// 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 }
|
||||
|
||||
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)
|
||||
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: -CGFloat($0 + 1) * drawingInfo.charWidth) }
|
||||
.map { basePosition.offsetBy(dx: -Double($0 + 1) * drawingInfo.charWidth) }
|
||||
let glyphs: [CGGlyph] = digits
|
||||
.map { drawingInfo.digitGlyphs[$0] }
|
||||
|
||||
// draw
|
||||
// draw number
|
||||
if isSelected {
|
||||
context.setFillColor(self.foregroundColor(.bold).cgColor)
|
||||
context.setFont(self.boldLineNumberFont)
|
||||
@ -300,17 +304,6 @@ final class LineNumberView: NSView {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
body(lineRect, lineNumber, isSelected)
|
||||
|
||||
if options.contains(.bySkippingWrappedLine), isWrapped { break }
|
||||
|
||||
let line: Line = isWrapped ? .wrapped : .new(isSelected)
|
||||
|
||||
body(lineRect, line, lineNumber)
|
||||
|
||||
wrappedLineGlyphIndex = fragmentGlyphRange.upperBound
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user