Skip to content

Commit 244d59a

Browse files
committed
Move Layout To Own File
1 parent 616a48d commit 244d59a

File tree

2 files changed

+197
-187
lines changed

2 files changed

+197
-187
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//
2+
// File.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Khan Winter on 4/10/25.
6+
//
7+
8+
import AppKit
9+
10+
extension TextLayoutManager {
11+
/// Asserts that the caller is not in an active layout pass.
12+
/// See docs on ``isInLayout`` for more details.
13+
private func assertNotInLayout() {
14+
#if DEBUG // This is redundant, but it keeps the flag debug-only too which helps prevent misuse.
15+
assert(!isInLayout, "layoutLines called while already in a layout pass. This is a programmer error.")
16+
#endif
17+
}
18+
19+
// MARK: - Layout Lines
20+
21+
/// Lays out all visible lines
22+
func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
23+
assertNotInLayout()
24+
guard let visibleRect = rect ?? delegate?.visibleRect,
25+
!isInTransaction,
26+
let textStorage else {
27+
return
28+
}
29+
30+
// The macOS may call `layout` on the textView while we're laying out fragment views. This ensures the view
31+
// tree modifications caused by this method are atomic, so macOS won't call `layout` while we're already doing
32+
// that
33+
CATransaction.begin()
34+
#if DEBUG
35+
isInLayout = true
36+
#endif
37+
38+
let minY = max(visibleRect.minY - verticalLayoutPadding, 0)
39+
let maxY = max(visibleRect.maxY + verticalLayoutPadding, 0)
40+
let originalHeight = lineStorage.height
41+
var usedFragmentIDs = Set<UUID>()
42+
var forceLayout: Bool = needsLayout
43+
var newVisibleLines: Set<TextLine.ID> = []
44+
var yContentAdjustment: CGFloat = 0
45+
var maxFoundLineWidth = maxLineWidth
46+
47+
// Layout all lines, fetching lines lazily as they are laid out.
48+
for linePosition in lineStorage.linesStartingAt(minY, until: maxY).lazy {
49+
guard linePosition.yPos < maxY else { break }
50+
if forceLayout
51+
|| linePosition.data.needsLayout(maxWidth: maxLineLayoutWidth)
52+
|| !visibleLineIds.contains(linePosition.data.id) {
53+
let lineSize = layoutLine(
54+
linePosition,
55+
textStorage: textStorage,
56+
layoutData: LineLayoutData(minY: minY, maxY: maxY, maxWidth: maxLineLayoutWidth),
57+
laidOutFragmentIDs: &usedFragmentIDs
58+
)
59+
if lineSize.height != linePosition.height {
60+
lineStorage.update(
61+
atIndex: linePosition.range.location,
62+
delta: 0,
63+
deltaHeight: lineSize.height - linePosition.height
64+
)
65+
// If we've updated a line's height, force re-layout for the rest of the pass.
66+
forceLayout = true
67+
68+
if linePosition.yPos < minY {
69+
// Adjust the scroll position by the difference between the new height and old.
70+
yContentAdjustment += lineSize.height - linePosition.height
71+
}
72+
}
73+
if maxFoundLineWidth < lineSize.width {
74+
maxFoundLineWidth = lineSize.width
75+
}
76+
} else {
77+
// Make sure the used fragment views aren't dequeued.
78+
usedFragmentIDs.formUnion(linePosition.data.lineFragments.map(\.data.id))
79+
}
80+
newVisibleLines.insert(linePosition.data.id)
81+
}
82+
83+
#if DEBUG
84+
isInLayout = false
85+
#endif
86+
CATransaction.commit()
87+
88+
// Enqueue any lines not used in this layout pass.
89+
viewReuseQueue.enqueueViews(notInSet: usedFragmentIDs)
90+
91+
// Update the visible lines with the new set.
92+
visibleLineIds = newVisibleLines
93+
94+
// These are fine to update outside of `isInLayout` as our internal data structures are finalized at this point
95+
// so laying out again won't break our line storage or visible line.
96+
97+
if maxFoundLineWidth > maxLineWidth {
98+
maxLineWidth = maxFoundLineWidth
99+
}
100+
101+
if yContentAdjustment != 0 {
102+
delegate?.layoutManagerYAdjustment(yContentAdjustment)
103+
}
104+
105+
if originalHeight != lineStorage.height || layoutView?.frame.size.height != lineStorage.height {
106+
delegate?.layoutManagerHeightDidUpdate(newHeight: lineStorage.height)
107+
}
108+
109+
needsLayout = false
110+
}
111+
112+
// MARK: - Layout Single Line
113+
114+
/// Lays out a single text line.
115+
/// - Parameters:
116+
/// - position: The line position from storage to use for layout.
117+
/// - textStorage: The text storage object to use for text info.
118+
/// - layoutData: The information required to perform layout for the given line.
119+
/// - laidOutFragmentIDs: Updated by this method as line fragments are laid out.
120+
/// - Returns: A `CGSize` representing the max width and total height of the laid out portion of the line.
121+
private func layoutLine(
122+
_ position: TextLineStorage<TextLine>.TextLinePosition,
123+
textStorage: NSTextStorage,
124+
layoutData: LineLayoutData,
125+
laidOutFragmentIDs: inout Set<UUID>
126+
) -> CGSize {
127+
let lineDisplayData = TextLine.DisplayData(
128+
maxWidth: layoutData.maxWidth,
129+
lineHeightMultiplier: lineHeightMultiplier,
130+
estimatedLineHeight: estimateLineHeight()
131+
)
132+
133+
let line = position.data
134+
if let renderDelegate {
135+
renderDelegate.prepareForDisplay(
136+
textLine: line,
137+
displayData: lineDisplayData,
138+
range: position.range,
139+
stringRef: textStorage,
140+
markedRanges: markedTextManager.markedRanges(in: position.range),
141+
breakStrategy: lineBreakStrategy
142+
)
143+
} else {
144+
line.prepareForDisplay(
145+
displayData: lineDisplayData,
146+
range: position.range,
147+
stringRef: textStorage,
148+
markedRanges: markedTextManager.markedRanges(in: position.range),
149+
breakStrategy: lineBreakStrategy
150+
)
151+
}
152+
153+
if position.range.isEmpty {
154+
return CGSize(width: 0, height: estimateLineHeight())
155+
}
156+
157+
var height: CGFloat = 0
158+
var width: CGFloat = 0
159+
let relativeMinY = max(layoutData.minY - position.yPos, 0)
160+
let relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY)
161+
162+
for lineFragmentPosition in line.lineFragments.linesStartingAt(
163+
relativeMinY,
164+
until: relativeMaxY
165+
) {
166+
let lineFragment = lineFragmentPosition.data
167+
168+
layoutFragmentView(for: lineFragmentPosition, at: position.yPos + lineFragmentPosition.yPos)
169+
170+
width = max(width, lineFragment.width)
171+
height += lineFragment.scaledHeight
172+
laidOutFragmentIDs.insert(lineFragment.id)
173+
}
174+
175+
return CGSize(width: width, height: height)
176+
}
177+
178+
// MARK: - Layout Fragment
179+
180+
/// Lays out a line fragment view for the given line fragment at the specified y value.
181+
/// - Parameters:
182+
/// - lineFragment: The line fragment position to lay out a view for.
183+
/// - yPos: The y value at which the line should begin.
184+
private func layoutFragmentView(
185+
for lineFragment: TextLineStorage<LineFragment>.TextLinePosition,
186+
at yPos: CGFloat
187+
) {
188+
let view = viewReuseQueue.getOrCreateView(forKey: lineFragment.data.id)
189+
view.setLineFragment(lineFragment.data)
190+
view.renderDelegate = renderDelegate
191+
view.frame.origin = CGPoint(x: edgeInsets.left, y: yPos)
192+
layoutView?.addSubview(view)
193+
view.needsDisplay = true
194+
}
195+
}

0 commit comments

Comments
 (0)