1- import Foundation
21import CoreGraphics
2+ import Foundation
33
44/// Manages gesture recognition from touch input
55class GestureRecognizer {
6-
6+
77 // MARK: - Properties
8-
8+
99 /// Configuration for gesture detection
1010 var configuration = GestureConfiguration ( )
11-
11+
1212 /// Current gesture state
1313 private( set) var state : GestureState = . idle
14-
14+
1515 /// Delegate for gesture events
1616 weak var delegate : GestureRecognizerDelegate ?
17-
17+
1818 // Position tracking
1919 private var lastFingerPositions : [ MTPoint ] = [ ]
2020 private var gestureStartTime : Double = 0
2121 private var gestureStartPosition : MTPoint ?
2222 private var lastCentroid : MTPoint ?
2323 private var frameCount : Int = 0
24-
24+
2525 // Stability tracking - prevents false gesture ends during brief state transitions
2626 private var stableFrameCount : Int = 0
27-
27+
28+ // Cooldown after 4-finger cancellation
29+ // Prevents accidental gesture triggers when lifting one finger during Mission Control
30+ private var isInCancellationCooldown : Bool = false
31+
2832 // MARK: - Public Interface
29-
33+
3034 /// Process new touch data from the multitouch device
3135 /// - Parameters:
3236 /// - touches: Raw pointer to touch data array
3337 /// - count: Number of touches in the array
3438 /// - timestamp: Timestamp of the touch frame
3539 func processTouches( _ touches: UnsafeMutableRawPointer , count: Int , timestamp: Double ) {
3640 let touchArray = touches. bindMemory ( to: MTTouch . self, capacity: count)
37-
41+
3842 // Collect only valid touching fingers (state 3 = touching down, state 4 = active)
3943 // Skip state 5 (lifting), 6 (lingering), 7 (gone)
4044 var validFingers : [ MTPoint ] = [ ]
41-
45+
4246 for i in 0 ..< count {
4347 let touch = touchArray [ i]
4448 if touch. state == 3 || touch. state == 4 {
4549 validFingers. append ( touch. normalizedVector. position)
4650 }
4751 }
48-
52+
4953 let fingerCount = validFingers. count
50-
51- if fingerCount >= 3 {
54+
55+ // ALWAYS cancel on 4+ fingers regardless of configuration
56+ // This ensures Mission Control and other system gestures always work
57+ if fingerCount >= 4 {
58+ if state != . idle {
59+ handleGestureCancel ( )
60+ }
61+ // Enter cooldown to prevent restart when finger is briefly lifted
62+ isInCancellationCooldown = true
63+ return
64+ }
65+
66+ // Clear cooldown when finger count drops to 0-2,
67+ // or when finger count is 3 and we're idle (so user can start a new gesture)
68+ if fingerCount <= 2 || ( fingerCount == 3 && state == . idle) {
69+ isInCancellationCooldown = false
70+ }
71+
72+ // Process exactly 3-finger gestures only
73+ // 4+ fingers are cancelled above, 0-2 fingers end the gesture below
74+ // Note: requiresExactlyThreeFingers no longer has an effect since we always
75+ // require exactly 3 to preserve Mission Control - could be deprecated
76+ let isValidThreeFingerGesture = !isInCancellationCooldown && fingerCount == 3
77+
78+ if isValidThreeFingerGesture {
5279 handleThreeFingerGesture ( fingers: validFingers, timestamp: timestamp)
5380 } else if state != . idle {
54- // Only end gesture if we've been below 3 fingers for multiple frames
55- // This prevents ending during brief state transitions
81+ // Gesture state changed - finger count dropped below 3
82+ // (4+ fingers case is handled by cancellation above before we get here)
83+ // Use stable frame count to prevent false ends during brief transitions
5684 stableFrameCount += 1
5785 if stableFrameCount >= 2 {
5886 handleGestureEnd ( timestamp: timestamp)
5987 }
6088 }
61-
89+
6290 frameCount += 1
6391 }
64-
92+
6593 /// Reset gesture recognition state
6694 func reset( ) {
6795 state = . idle
@@ -71,15 +99,16 @@ class GestureRecognizer {
7199 gestureStartTime = 0
72100 frameCount = 0
73101 stableFrameCount = 0
102+ isInCancellationCooldown = false // Clear cooldown on reset
74103 }
75-
104+
76105 // MARK: - Private Methods
77-
106+
78107 private func handleThreeFingerGesture( fingers: [ MTPoint ] , timestamp: Double ) {
79108 stableFrameCount = 0 // Reset since we have 3 fingers
80-
109+
81110 let centroid = calculateCentroid ( fingers: fingers)
82-
111+
83112 // Check for large centroid jumps (finger added/removed causing position shift)
84113 if let last = lastCentroid {
85114 let jump = centroid. distance ( to: last)
@@ -90,7 +119,7 @@ class GestureRecognizer {
90119 return
91120 }
92121 }
93-
122+
94123 switch state {
95124 case . idle:
96125 // Start new gesture
@@ -100,27 +129,27 @@ class GestureRecognizer {
100129 lastCentroid = centroid
101130 lastFingerPositions = fingers
102131 delegate? . gestureRecognizerDidStart ( self , at: centroid)
103-
132+
104133 case . possibleTap:
105134 // Check if we should transition to drag
106135 guard let startPos = gestureStartPosition else { return }
107136 let movement = startPos. distance ( to: centroid)
108137 let elapsed = timestamp - gestureStartTime
109-
138+
110139 if movement > configuration. moveThreshold || elapsed > configuration. tapThreshold {
111140 state = . dragging
112141 lastCentroid = centroid
113142 delegate? . gestureRecognizerDidBeginDragging ( self )
114143 } else {
115144 lastCentroid = centroid
116145 }
117-
146+
118147 case . dragging:
119148 // Calculate delta from last frame
120149 if let last = lastCentroid {
121150 let deltaX = centroid. x - last. x
122151 let deltaY = centroid. y - last. y
123-
152+
124153 // Only process small deltas (real movement, not jumps)
125154 if abs ( deltaX) < 0.03 && abs ( deltaY) < 0.03 {
126155 if abs ( deltaX) > 0.0001 || abs ( deltaY) > 0.0001 {
@@ -137,17 +166,17 @@ class GestureRecognizer {
137166 }
138167 }
139168 lastCentroid = centroid
140-
169+
141170 case . waitingForRelease:
142171 break
143172 }
144-
173+
145174 lastFingerPositions = fingers
146175 }
147-
176+
148177 private func handleGestureEnd( timestamp: Double ) {
149178 let elapsed = timestamp - gestureStartTime
150-
179+
151180 switch state {
152181 case . possibleTap:
153182 if elapsed < configuration. tapThreshold {
@@ -158,10 +187,26 @@ class GestureRecognizer {
158187 default :
159188 break
160189 }
161-
190+
191+ reset ( )
192+ }
193+
194+ /// Cancel gesture without completing it (e.g., when 4th finger detected)
195+ private func handleGestureCancel( ) {
196+ switch state {
197+ case . possibleTap:
198+ // Cancel the possible tap - notify delegate so it can reset state
199+ delegate? . gestureRecognizerDidCancel ( self )
200+ case . dragging:
201+ // Cancel the drag - don't complete it normally
202+ delegate? . gestureRecognizerDidCancelDragging ( self )
203+ default :
204+ break
205+ }
206+
162207 reset ( )
163208 }
164-
209+
165210 private func calculateCentroid( fingers: [ MTPoint ] ) -> MTPoint {
166211 let sumX = fingers. reduce ( 0 ) { $0 + $1. x }
167212 let sumY = fingers. reduce ( 0 ) { $0 + $1. y }
@@ -179,17 +224,17 @@ struct GestureData {
179224 let fingerCount : Int
180225 let startPosition : MTPoint ?
181226 let lastPosition : MTPoint
182-
227+
183228 /// Calculate frame-to-frame delta with sensitivity applied
184229 func frameDelta( from configuration: GestureConfiguration ) -> ( x: CGFloat , y: CGFloat ) {
185230 let deltaX = CGFloat ( centroid. x - lastPosition. x)
186231 let deltaY = CGFloat ( centroid. y - lastPosition. y)
187-
232+
188233 // Reject large deltas (likely jumps from finger changes)
189234 if abs ( deltaX) > 0.03 || abs ( deltaY) > 0.03 {
190235 return ( 0 , 0 )
191236 }
192-
237+
193238 let sensitivity = CGFloat ( configuration. effectiveSensitivity ( for: velocity) )
194239 return ( deltaX * sensitivity, deltaY * sensitivity)
195240 }
@@ -201,16 +246,22 @@ struct GestureData {
201246protocol GestureRecognizerDelegate : AnyObject {
202247 /// Called when a gesture starts (3 fingers detected)
203248 func gestureRecognizerDidStart( _ recognizer: GestureRecognizer , at position: MTPoint )
204-
249+
205250 /// Called when a tap gesture is recognized
206251 func gestureRecognizerDidTap( _ recognizer: GestureRecognizer )
207-
252+
208253 /// Called when dragging begins
209254 func gestureRecognizerDidBeginDragging( _ recognizer: GestureRecognizer )
210-
255+
211256 /// Called during drag with movement data
212257 func gestureRecognizerDidUpdateDragging( _ recognizer: GestureRecognizer , with data: GestureData )
213-
214- /// Called when dragging ends
258+
259+ /// Called when dragging ends normally (user lifted fingers)
215260 func gestureRecognizerDidEndDragging( _ recognizer: GestureRecognizer )
261+
262+ /// Called when gesture is cancelled from early state (e.g., possibleTap when 4th finger added)
263+ func gestureRecognizerDidCancel( _ recognizer: GestureRecognizer )
264+
265+ /// Called when dragging is cancelled (e.g., 4th finger added for Mission Control)
266+ func gestureRecognizerDidCancelDragging( _ recognizer: GestureRecognizer )
216267}
0 commit comments