Skip to content

Commit 37499b9

Browse files
committed
Optimize insertion point rendering by reusing views and fixing layout issues
Reuse existing STInsertionPointView instances to avoid unnecessary allocations when drawing multiple insertion points. Ensure correct sizing of text insertion indicators by overriding setFrameSize and disabling reliance on autoresizingMask. Improve blink control behavior for both modern and legacy indicators.
1 parent e657ab4 commit 37499b9

File tree

2 files changed

+49
-16
lines changed

2 files changed

+49
-16
lines changed

Sources/STTextViewAppKit/STInsertionPointView.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import AppKit
1010
/// |---textInsertionIndicator (STInsertionPointIndicatorProtocol)
1111
///
1212
internal class STInsertionPointView: NSView {
13-
private var textInsertionIndicator: any STInsertionPointIndicatorProtocol
13+
private let textInsertionIndicator: any STInsertionPointIndicatorProtocol
1414

1515
override var isFlipped: Bool {
1616
true
@@ -37,6 +37,13 @@ internal class STInsertionPointView: NSView {
3737
addSubview(textInsertionIndicator)
3838
}
3939

40+
override func setFrameSize(_ newSize: NSSize) {
41+
super.setFrameSize(newSize)
42+
// Manually reset size because `NSTextInsertionIndicator`
43+
// does not react to its autoresizingMask
44+
textInsertionIndicator.frame.size = newSize
45+
}
46+
4047
func blinkStart() {
4148
textInsertionIndicator.blinkStart()
4249
}

Sources/STTextViewAppKit/STTextView+InsertionPoint.swift

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,34 +89,50 @@ extension STTextView {
8989
}
9090
}
9191

92-
removeInsertionPointView()
92+
var existingViews = contentView.subviews.filter { view in
93+
type(of: view) == STInsertionPointView.self
94+
}
9395

9496
for selectionFrame in textSelectionFrames where !selectionFrame.isNull && !selectionFrame.isInfinite {
9597
let insertionViewFrame = CGRect(origin: selectionFrame.origin, size: CGSize(width: max(2, selectionFrame.width), height: selectionFrame.height)).pixelAligned
96-
97-
var textInsertionIndicator: any STInsertionPointIndicatorProtocol
98-
if let customTextInsertionIndicator = self.delegateProxy.textViewInsertionPointView(self, frame: CGRect(origin: .zero, size: insertionViewFrame.size)) {
99-
textInsertionIndicator = customTextInsertionIndicator
98+
let insertionView: STInsertionPointView
99+
// re-use existing insertion views
100+
if !existingViews.isEmpty {
101+
// reuse existing insertion view
102+
insertionView = existingViews.removeFirst() as! STInsertionPointView
103+
insertionView.frame = insertionViewFrame
100104
} else {
101-
if #available(macOS 14, *) {
102-
textInsertionIndicator = STTextInsertionIndicatorNew(frame: CGRect(origin: .zero, size: insertionViewFrame.size))
105+
// add new views that exedes existing views
106+
var textInsertionIndicator: any STInsertionPointIndicatorProtocol
107+
if let customTextInsertionIndicator = self.delegateProxy.textViewInsertionPointView(self, frame: CGRect(origin: .zero, size: insertionViewFrame.size)) {
108+
textInsertionIndicator = customTextInsertionIndicator
103109
} else {
104-
textInsertionIndicator = STTextInsertionIndicatorOld(frame: CGRect(origin: .zero, size: insertionViewFrame.size))
110+
if #available(macOS 14, *) {
111+
textInsertionIndicator = STTextInsertionIndicatorNew(frame: CGRect(origin: .zero, size: insertionViewFrame.size))
112+
} else {
113+
textInsertionIndicator = STTextInsertionIndicatorOld(frame: CGRect(origin: .zero, size: insertionViewFrame.size))
114+
}
105115
}
106-
}
107116

108-
let insertionView = STInsertionPointView(frame: insertionViewFrame, textInsertionIndicator: textInsertionIndicator)
109-
insertionView.clipsToBounds = false
110-
insertionView.insertionPointColor = insertionPointColor
117+
insertionView = STInsertionPointView(frame: insertionViewFrame, textInsertionIndicator: textInsertionIndicator)
118+
insertionView.clipsToBounds = false
119+
insertionView.insertionPointColor = insertionPointColor
120+
contentView.addSubview(insertionView)
121+
}
111122

112123
if isFirstResponder {
113124
insertionView.blinkStart()
114125
} else {
115126
insertionView.blinkStop()
116127
}
128+
}
117129

118-
contentView.addSubview(insertionView)
130+
// remove unused insertion points (unused)
131+
for v in existingViews {
132+
v.removeFromSuperview()
119133
}
134+
existingViews.removeAll()
135+
120136
} else if !shouldDrawInsertionPoint {
121137
removeInsertionPointView()
122138
}
@@ -132,9 +148,12 @@ extension STTextView {
132148

133149
@available(macOS 14.0, *)
134150
private class STTextInsertionIndicatorNew: NSTextInsertionIndicator, STInsertionPointIndicatorProtocol {
151+
// NSTextInsertionIndicator start as visible (blinking)
152+
private var _isVisible: Bool = true
135153

136154
override init(frame frameRect: CGRect) {
137155
super.init(frame: frameRect)
156+
autoresizingMask = [.width, .height]
138157
}
139158

140159
@available(*, unavailable)
@@ -153,11 +172,17 @@ private class STTextInsertionIndicatorNew: NSTextInsertionIndicator, STInsertion
153172
}
154173

155174
func blinkStart() {
156-
displayMode = .automatic
175+
if !_isVisible {
176+
_isVisible = true
177+
displayMode = .automatic
178+
}
157179
}
158180

159181
func blinkStop() {
160-
displayMode = .hidden
182+
if _isVisible {
183+
_isVisible = false
184+
displayMode = .hidden
185+
}
161186
}
162187

163188
open override var isFlipped: Bool {
@@ -180,6 +205,7 @@ private class STTextInsertionIndicatorOld: NSView, STInsertionPointIndicatorProt
180205
wantsLayer = true
181206
layer?.backgroundColor = insertionPointColor.cgColor
182207
layer?.cornerRadius = 1
208+
autoresizingMask = [.width, .height]
183209
}
184210

185211
@available(*, unavailable)

0 commit comments

Comments
 (0)