Skip to content

Commit 120e6fc

Browse files
committed
Add One Case To Layout Invalidation, Selection Drawing Uses LayoutManager
1 parent b1450b6 commit 120e6fc

File tree

8 files changed

+59
-19
lines changed

8 files changed

+59
-19
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,13 @@ extension TextLayoutManager {
5555

5656
// Layout all lines, fetching lines lazily as they are laid out.
5757
for linePosition in lineStorage.linesStartingAt(minY, until: maxY).lazy {
58-
guard linePosition.yPos < maxY else { break }
59-
if forceLayout
60-
|| linePosition.data.needsLayout(maxWidth: maxLineLayoutWidth)
61-
|| !visibleLineIds.contains(linePosition.data.id) {
58+
guard linePosition.yPos < maxY else { continue }
59+
// Three ways to determine if a line needs to be re-calculated.
60+
let changedWidth = linePosition.data.needsLayout(maxWidth: maxLineLayoutWidth)
61+
let wasNotVisible = !visibleLineIds.contains(linePosition.data.id)
62+
let lineNotEntirelyLaidOut = linePosition.height != linePosition.data.lineFragments.height
63+
64+
if forceLayout || changedWidth || wasNotVisible || lineNotEntirelyLaidOut {
6265
let lineSize = layoutLine(
6366
linePosition,
6467
textStorage: textStorage,

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,13 @@ extension TextLayoutManager {
137137
: (textStorage?.string as? NSString)?.rangeOfComposedCharacterSequence(at: offset)
138138
?? NSRange(location: offset, length: 0)
139139

140-
let minXPos = CTLineGetOffsetForStringIndex(
141-
fragmentPosition.data.ctLine,
142-
realRange.location - linePosition.range.location, // CTLines have the same relative range as the line
143-
nil
140+
let minXPos = characterXPosition(
141+
in: fragmentPosition.data,
142+
for: realRange.location - linePosition.range.location
144143
)
145-
let maxXPos = CTLineGetOffsetForStringIndex(
146-
fragmentPosition.data.ctLine,
147-
realRange.max - linePosition.range.location,
148-
nil
144+
let maxXPos = characterXPosition(
145+
in: fragmentPosition.data,
146+
for: realRange.max - linePosition.range.location
149147
)
150148

151149
return CGRect(
@@ -187,7 +185,7 @@ extension TextLayoutManager {
187185
var rects: [CGRect] = []
188186
for fragmentPosition in line.data.lineFragments.linesInRange(relativeRange) {
189187
guard let intersectingRange = fragmentPosition.range.intersection(relativeRange) else { continue }
190-
let fragmentRect = fragmentPosition.data.rectFor(range: intersectingRange)
188+
let fragmentRect = characterRect(in: fragmentPosition.data, for: intersectingRange)
191189
guard fragmentRect.width > 0 else { continue }
192190
rects.append(
193191
CGRect(
@@ -270,6 +268,28 @@ extension TextLayoutManager {
270268
return nil
271269
}
272270

271+
// MARK: - Line Fragment Rects
272+
273+
/// Finds the x position of the offset in the string the fragment represents.
274+
/// - Parameters:
275+
/// - lineFragment: The line fragment to calculate for.
276+
/// - offset: The offset, relative to the start of the *line*.
277+
/// - Returns: The x position of the character in the drawn line, from the left.
278+
public func characterXPosition(in lineFragment: LineFragment, for offset: Int) -> CGFloat {
279+
renderDelegate?.characterXPosition(in: lineFragment, for: offset) ?? lineFragment._xPos(for: offset)
280+
}
281+
282+
public func characterRect(in lineFragment: LineFragment, for range: NSRange) -> CGRect {
283+
let minXPos = characterXPosition(in: lineFragment, for: range.lowerBound)
284+
let maxXPos = characterXPosition(in: lineFragment, for: range.upperBound)
285+
return CGRect(
286+
x: minXPos,
287+
y: 0,
288+
width: maxXPos - minXPos,
289+
height: lineFragment.scaledHeight
290+
).pixelAligned
291+
}
292+
273293
// MARK: - Ensure Layout
274294

275295
/// Forces layout calculation for all lines up to and including the given offset.

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManagerRenderDelegate.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public protocol TextLayoutManagerRenderDelegate: AnyObject {
2424
func estimatedLineHeight() -> CGFloat?
2525

2626
func lineFragmentView(for lineFragment: LineFragment) -> LineFragmentView
27+
28+
func characterXPosition(in lineFragment: LineFragment, for offset: Int) -> CGFloat
2729
}
2830

2931
public extension TextLayoutManagerRenderDelegate {
@@ -51,4 +53,8 @@ public extension TextLayoutManagerRenderDelegate {
5153
func lineFragmentView(for lineFragment: LineFragment) -> LineFragmentView {
5254
LineFragmentView()
5355
}
56+
57+
func characterXPosition(in lineFragment: LineFragment, for offset: Int) -> CGFloat {
58+
lineFragment._xPos(for: offset)
59+
}
5460
}

Sources/CodeEditTextView/TextLine/LineFragment.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,17 @@ public final class LineFragment: Identifiable, Equatable {
4747
/// Finds the x position of the offset in the string the fragment represents.
4848
/// - Parameter offset: The offset, relative to the start of the *line*.
4949
/// - Returns: The x position of the character in the drawn line, from the left.
50-
public func xPos(for offset: Int) -> CGFloat {
50+
@available(*, deprecated, renamed: "layoutManager.characterXPosition(in:)", message: "Moved to layout manager")
51+
public func xPos(for offset: Int) -> CGFloat { _xPos(for: offset) }
52+
53+
/// Finds the x position of the offset in the string the fragment represents.
54+
///
55+
/// Underscored, because although this needs to be accessible outside this class, the relevant layout manager method
56+
/// should be used.
57+
///
58+
/// - Parameter offset: The offset, relative to the start of the *line*.
59+
/// - Returns: The x position of the character in the drawn line, from the left.
60+
func _xPos(for offset: Int) -> CGFloat {
5161
return CTLineGetOffsetForStringIndex(ctLine, offset, nil)
5262
}
5363

@@ -84,6 +94,7 @@ public final class LineFragment: Identifiable, Equatable {
8494
/// Calculates the drawing rect for a given range.
8595
/// - Parameter range: The range to calculate the bounds for, relative to the line.
8696
/// - Returns: A rect that contains the text contents in the given range.
97+
@available(*, deprecated, renamed: "layoutManager.characterRect(in:)", message: "Moved to layout manager")
8798
public func rectFor(range: NSRange) -> CGRect {
8899
let minXPos = CTLineGetOffsetForStringIndex(ctLine, range.lowerBound, nil)
89100
let maxXPos = CTLineGetOffsetForStringIndex(ctLine, range.upperBound, nil)

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import AppKit
1010
extension TextSelectionManager {
1111
/// Draws line backgrounds and selection rects for each selection in the given rect.
1212
/// - Parameter rect: The rect to draw in.
13-
func drawSelections(in rect: NSRect) {
13+
public func drawSelections(in rect: NSRect) {
1414
guard let context = NSGraphicsContext.current?.cgContext else { return }
1515
context.saveGState()
1616
var highlightedLines: Set<UUID> = []

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension TextSelectionManager {
1818
/// - Returns: An array of rects that the selection overlaps.
1919
func getFillRects(in rect: NSRect, for textSelection: TextSelection) -> [CGRect] {
2020
guard let layoutManager,
21-
let range = textSelection.range.intersection(textView?.visibleTextRange ?? .zero) else {
21+
let range = textSelection.range.intersection(delegate?.visibleTextRange ?? .zero) else {
2222
return []
2323
}
2424

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public class TextSelectionManager: NSObject {
5252
weak var delegate: TextSelectionManagerDelegate?
5353
var cursorTimer: CursorTimer
5454

55-
init(
55+
public init(
5656
layoutManager: TextLayoutManager,
5757
textStorage: NSTextStorage,
5858
textView: TextView?,

Sources/CodeEditTextView/TextView/DraggingTextRenderer.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class DraggingTextRenderer: NSView {
8181
// Clear text that's not selected
8282
if fragmentRange.contains(selectedRange.lowerBound) {
8383
let relativeOffset = selectedRange.lowerBound - line.range.lowerBound
84-
let selectionXPos = fragment.data.xPos(for: relativeOffset)
84+
let selectionXPos = layoutManager.characterXPosition(in: fragment.data, for: relativeOffset)
8585
context.clear(
8686
CGRect(
8787
x: 0.0,
@@ -94,7 +94,7 @@ class DraggingTextRenderer: NSView {
9494

9595
if fragmentRange.contains(selectedRange.upperBound) {
9696
let relativeOffset = selectedRange.upperBound - line.range.lowerBound
97-
let selectionXPos = fragment.data.xPos(for: relativeOffset)
97+
let selectionXPos = layoutManager.characterXPosition(in: fragment.data, for: relativeOffset)
9898
context.clear(
9999
CGRect(
100100
x: selectionXPos,

0 commit comments

Comments
 (0)