Skip to content

Commit d6babde

Browse files
committed
Track Mouse Drag Outside View
1 parent 7d63a64 commit d6babde

File tree

1 file changed

+92
-36
lines changed

1 file changed

+92
-36
lines changed

Sources/CodeEditTextView/TextView/TextView+Mouse.swift

Lines changed: 92 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -86,50 +86,29 @@ extension TextView {
8686
return
8787
}
8888

89+
// We receive global events because our view received the drag event, but we need to clamp the potentially
90+
// out-of-bounds positions to a position our layout manager can deal with.
91+
let locationInWindow = convert(event.locationInWindow, from: nil)
92+
let locationInView = CGPoint(
93+
x: max(0.0, min(locationInWindow.x, frame.width)),
94+
y: max(0.0, min(locationInWindow.y, frame.height))
95+
)
96+
8997
if mouseDragAnchor == nil {
90-
mouseDragAnchor = convert(event.locationInWindow, from: nil)
98+
mouseDragAnchor = locationInView
9199
super.mouseDragged(with: event)
92100
} else {
93101
guard let mouseDragAnchor,
94102
let startPosition = layoutManager.textOffsetAtPoint(mouseDragAnchor),
95-
let endPosition = layoutManager.textOffsetAtPoint(convert(event.locationInWindow, from: nil)) else {
103+
let endPosition = layoutManager.textOffsetAtPoint(locationInView) else {
96104
return
97105
}
98106

99-
switch cursorSelectionMode {
100-
case .character:
101-
selectionManager.setSelectedRange(
102-
NSRange(
103-
location: min(startPosition, endPosition),
104-
length: max(startPosition, endPosition) - min(startPosition, endPosition)
105-
)
106-
)
107-
108-
case .word:
109-
let startWordRange = findWordBoundary(at: startPosition)
110-
let endWordRange = findWordBoundary(at: endPosition)
111-
112-
selectionManager.setSelectedRange(
113-
NSRange(
114-
location: min(startWordRange.location, endWordRange.location),
115-
length: max(startWordRange.location + startWordRange.length,
116-
endWordRange.location + endWordRange.length) -
117-
min(startWordRange.location, endWordRange.location)
118-
)
119-
)
120-
121-
case .line:
122-
let startLineRange = findLineBoundary(at: startPosition)
123-
let endLineRange = findLineBoundary(at: endPosition)
124-
125-
selectionManager.setSelectedRange(
126-
NSRange(
127-
location: min(startLineRange.location, endLineRange.location),
128-
length: max(startLineRange.location + startLineRange.length,
129-
endLineRange.location + endLineRange.length) -
130-
min(startLineRange.location, endLineRange.location)
131-
)
132-
)
107+
let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
108+
if modifierFlags.contains(.option) {
109+
dragColumnSelection(mouseDragAnchor: mouseDragAnchor, locationInView: locationInView)
110+
} else {
111+
dragSelection(startPosition: startPosition, endPosition: endPosition, mouseDragAnchor: mouseDragAnchor)
133112
}
134113

135114
setNeedsDisplay()
@@ -182,4 +161,81 @@ extension TextView {
182161
mouseDragTimer?.invalidate()
183162
mouseDragTimer = nil
184163
}
164+
165+
private func dragSelection(startPosition: Int, endPosition: Int, mouseDragAnchor: CGPoint) {
166+
switch cursorSelectionMode {
167+
case .character:
168+
selectionManager.setSelectedRange(
169+
NSRange(
170+
location: min(startPosition, endPosition),
171+
length: max(startPosition, endPosition) - min(startPosition, endPosition)
172+
)
173+
)
174+
175+
case .word:
176+
let startWordRange = findWordBoundary(at: startPosition)
177+
let endWordRange = findWordBoundary(at: endPosition)
178+
179+
selectionManager.setSelectedRange(
180+
NSRange(
181+
location: min(startWordRange.location, endWordRange.location),
182+
length: max(startWordRange.location + startWordRange.length,
183+
endWordRange.location + endWordRange.length) -
184+
min(startWordRange.location, endWordRange.location)
185+
)
186+
)
187+
188+
case .line:
189+
let startLineRange = findLineBoundary(at: startPosition)
190+
let endLineRange = findLineBoundary(at: endPosition)
191+
192+
selectionManager.setSelectedRange(
193+
NSRange(
194+
location: min(startLineRange.location, endLineRange.location),
195+
length: max(startLineRange.location + startLineRange.length,
196+
endLineRange.location + endLineRange.length) -
197+
min(startLineRange.location, endLineRange.location)
198+
)
199+
)
200+
}
201+
}
202+
203+
private func dragColumnSelection(mouseDragAnchor: CGPoint, locationInView: CGPoint) {
204+
// Drag the selection and select in columns
205+
let start = CGPoint(
206+
x: min(mouseDragAnchor.x, locationInView.x),
207+
y: min(mouseDragAnchor.y, locationInView.y)
208+
)
209+
let end = CGPoint(
210+
x: max(mouseDragAnchor.x, locationInView.x),
211+
y: max(mouseDragAnchor.y, locationInView.y)
212+
)
213+
214+
// Collect all overlapping text ranges
215+
var selectedRanges: [NSRange] = layoutManager.linesStartingAt(start.y, until: end.y).flatMap { textLine in
216+
// Collect fragment ranges
217+
return textLine.data.lineFragments.compactMap { lineFragment -> NSRange? in
218+
let startOffset = self.layoutManager.textOffsetAtPoint(
219+
start,
220+
fragmentPosition: lineFragment,
221+
linePosition: textLine
222+
)
223+
let endOffset = self.layoutManager.textOffsetAtPoint(
224+
end,
225+
fragmentPosition: lineFragment,
226+
linePosition: textLine
227+
)
228+
guard let startOffset, let endOffset else { return nil }
229+
230+
return NSRange(start: startOffset, end: endOffset)
231+
}
232+
}
233+
234+
// If we have some non-cursor selections, filter out any cursor selections
235+
if selectedRanges.contains(where: { !$0.isEmpty }) {
236+
selectedRanges = selectedRanges.filter({ !$0.isEmpty })
237+
}
238+
239+
selectionManager.setSelectedRanges(selectedRanges)
240+
}
185241
}

0 commit comments

Comments
 (0)