Skip to content

Commit 7d2c81c

Browse files
committed
Update Selections, Remove Demo Menu Item
1 parent d86b59d commit 7d2c81c

File tree

6 files changed

+58
-25
lines changed

6 files changed

+58
-25
lines changed

Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/ContentView.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ struct ContentView: View {
1717
HStack {
1818
Toggle("Wrap Lines", isOn: $wrapLines)
1919
Toggle("Inset Edges", isOn: $enableEdgeInsets)
20-
Button {
21-
22-
} label: {
23-
Text("Insert Attachment")
24-
}
25-
2620
}
2721
Divider()
2822
SwiftUITextView(

Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachmentManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public final class TextAttachmentManager {
2828
layoutManager?.lineStorage.update(atOffset: $0.range.location, delta: 0, deltaHeight: -$0.height)
2929
}
3030
}
31-
layoutManager?.invalidateLayoutForRange(range)
31+
layoutManager?.setNeedsLayout()
3232
}
3333

3434
public func remove(atOffset offset: Int) {

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,12 @@ extension TextLayoutManager {
6161
/// to re-enter.
6262
/// - Warning: This is probably not what you're looking for. If you need to invalidate layout, or update lines, this
6363
/// is not the way to do so. This should only be called when macOS performs layout.
64-
public func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
64+
@discardableResult
65+
public func layoutLines(in rect: NSRect? = nil) -> Set<TextLine.ID> { // swiftlint:disable:this function_body_length
6566
guard let visibleRect = rect ?? delegate?.visibleRect,
6667
!isInTransaction,
6768
let textStorage else {
68-
return
69+
return []
6970
}
7071

7172
// The macOS may call `layout` on the textView while we're laying out fragment views. This ensures the view
@@ -83,6 +84,10 @@ extension TextLayoutManager {
8384
var yContentAdjustment: CGFloat = 0
8485
var maxFoundLineWidth = maxLineWidth
8586

87+
#if DEBUG
88+
var laidOutLines: Set<TextLine.ID> = []
89+
#endif
90+
8691
// Layout all lines, fetching lines lazily as they are laid out.
8792
for linePosition in linesStartingAt(minY, until: maxY).lazy {
8893
guard linePosition.yPos < maxY else { continue }
@@ -115,6 +120,9 @@ extension TextLayoutManager {
115120
if maxFoundLineWidth < lineSize.width {
116121
maxFoundLineWidth = lineSize.width
117122
}
123+
#if DEBUG
124+
laidOutLines.insert(linePosition.data.id)
125+
#endif
118126
} else {
119127
// Make sure the used fragment views aren't dequeued.
120128
usedFragmentIDs.formUnion(linePosition.data.lineFragments.map(\.data.id))
@@ -147,6 +155,12 @@ extension TextLayoutManager {
147155
if originalHeight != lineStorage.height || layoutView?.frame.size.height != lineStorage.height {
148156
delegate?.layoutManagerHeightDidUpdate(newHeight: lineStorage.height)
149157
}
158+
159+
#if DEBUG
160+
return laidOutLines
161+
#else
162+
return []
163+
#endif
150164
}
151165

152166
// MARK: - Layout Single Line

Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/TextSelectionManager+SelectionManipulation.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,46 @@ public extension TextSelectionManager {
2525
decomposeCharacters: Bool = false,
2626
suggestedXPos: CGFloat? = nil
2727
) -> NSRange {
28+
var range: NSRange
2829
switch direction {
2930
case .backward:
3031
guard offset > 0 else { return NSRange(location: offset, length: 0) } // Can't go backwards beyond 0
31-
return extendSelectionHorizontal(
32+
range = extendSelectionHorizontal(
3233
from: offset,
3334
destination: destination,
3435
delta: -1,
3536
decomposeCharacters: decomposeCharacters
3637
)
3738
case .forward:
38-
return extendSelectionHorizontal(
39+
range = extendSelectionHorizontal(
3940
from: offset,
4041
destination: destination,
4142
delta: 1,
4243
decomposeCharacters: decomposeCharacters
4344
)
4445
case .up:
45-
return extendSelectionVertical(
46+
range = extendSelectionVertical(
4647
from: offset,
4748
destination: destination,
4849
up: true,
4950
suggestedXPos: suggestedXPos
5051
)
5152
case .down:
52-
return extendSelectionVertical(
53+
range = extendSelectionVertical(
5354
from: offset,
5455
destination: destination,
5556
up: false,
5657
suggestedXPos: suggestedXPos
5758
)
5859
}
60+
61+
// Extend ranges to include attachments.
62+
if let attachments = layoutManager?.attachments.get(overlapping: range) {
63+
attachments.forEach { textAttachment in
64+
range.formUnion(textAttachment.range)
65+
}
66+
}
67+
68+
return range
5969
}
6070
}

Sources/CodeEditTextView/TextView/TextView+Menu.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,9 @@ extension TextView {
2525
menu.items = [
2626
NSMenuItem(title: "Cut", action: #selector(cut(_:)), keyEquivalent: "x"),
2727
NSMenuItem(title: "Copy", action: #selector(copy(_:)), keyEquivalent: "c"),
28-
NSMenuItem(title: "Paste", action: #selector(paste(_:)), keyEquivalent: "v"),
29-
NSMenuItem(title: "Attach", action: #selector(buh), keyEquivalent: "b")
28+
NSMenuItem(title: "Paste", action: #selector(paste(_:)), keyEquivalent: "v")
3029
]
3130

3231
return menu
3332
}
34-
35-
@objc func buh() {
36-
if layoutManager.attachments.get(
37-
startingIn: selectedRange()
38-
).first?.range.location == selectedRange().location {
39-
layoutManager.attachments.remove(atOffset: selectedRange().location)
40-
} else {
41-
layoutManager.attachments.add(Buh(), for: selectedRange())
42-
}
43-
}
4433
}

Tests/CodeEditTextViewTests/LayoutManager/TextLayoutManagerTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ struct TextLayoutManagerTests {
4343
init() throws {
4444
textView = TextView(string: "A\nB\nC\nD")
4545
textView.frame = NSRect(x: 0, y: 0, width: 1000, height: 1000)
46+
textView.updateFrameIfNeeded()
4647
textStorage = textView.textStorage
4748
layoutManager = try #require(textView.layoutManager)
4849
}
@@ -181,4 +182,29 @@ struct TextLayoutManagerTests {
181182
lastLineIndex = lineIndex
182183
}
183184
}
185+
186+
@Test
187+
func afterLayoutDoesntNeedLayout() {
188+
layoutManager.layoutLines(in: NSRect(x: 0, y: 0, width: 1000, height: 1000))
189+
#expect(layoutManager.needsLayout == false)
190+
}
191+
192+
@Test
193+
func invalidatingRangeLaysOutLines() {
194+
layoutManager.layoutLines(in: NSRect(x: 0, y: 0, width: 1000, height: 1000))
195+
196+
let lineIds = Set(layoutManager.linesInRange(NSRange(start: 2, end: 4)).map { $0.data.id })
197+
layoutManager.invalidateLayoutForRange(NSRange(start: 2, end: 4))
198+
199+
#expect(layoutManager.needsLayout == false) // No forced layout
200+
#expect(
201+
layoutManager
202+
.linesInRange(NSRange(start: 2, end: 4))
203+
.allSatisfy({ $0.data.needsLayout(maxWidth: .infinity) })
204+
)
205+
206+
let invalidatedLineIds = layoutManager.layoutLines()
207+
208+
#expect(invalidatedLineIds == lineIds, "Invalidated lines != lines that were laid out in next pass.")
209+
}
184210
}

0 commit comments

Comments
 (0)