Skip to content

Commit 082ba3b

Browse files
committed
🎨 Organize code more
1 parent 44303f9 commit 082ba3b

7 files changed

Lines changed: 474 additions & 424 deletions

‎Loop/Core/LoopManager.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ final class LoopManager {
154154
}
155155

156156
gestureToggleTask = Task(priority: .background) { [weak self] in
157-
for await enabled in Defaults.updates(.enableGestures) {
157+
for await enabled in Defaults.updates(.enableGestures, initial: false) {
158158
guard let self, !Task.isCancelled else { break }
159159

160160
if enabled, AccessibilityManager.shared.isGranted {

‎Loop/Core/Multitouch/MultitouchGestureBlocker.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ final class MultitouchGestureBlocker {
2828
CGEventType(rawValue: UInt32(NSEvent.EventType.gesture.rawValue)),
2929
CGEventType(rawValue: UInt32(NSEvent.EventType.magnify.rawValue)),
3030
CGEventType(rawValue: UInt32(NSEvent.EventType.rotate.rawValue)),
31+
CGEventType(rawValue: UInt32(NSEvent.EventType.swipe.rawValue)),
3132
CGEventType(rawValue: UInt32(NSEvent.EventType.smartMagnify.rawValue))
3233
].compactMap(\.self)
3334

‎Loop/Core/Multitouch/MultitouchGestureSession.swift‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ final class MultitouchGestureSession {
2626
private var lastCommittedAction: ActionKey?
2727
private var lastCommitSwipeDistance: CGFloat = 0
2828
private var lastCommitMagnifyDistance: CGFloat = 0
29-
private var magnificationDirection: Int = 0
29+
private var magnificationKind: GestureBinding.Kind?
3030

3131
func reset() {
3232
didOpenLoopWithThisGesture = false
@@ -38,7 +38,7 @@ final class MultitouchGestureSession {
3838
lastCommittedAction = nil
3939
lastCommitSwipeDistance = 0
4040
lastCommitMagnifyDistance = 0
41-
magnificationDirection = 0
41+
magnificationKind = nil
4242
}
4343

4444
func begin(
@@ -120,24 +120,29 @@ final class MultitouchGestureSession {
120120
}
121121

122122
func commitMagnify(
123+
gesture: GestureBinding,
123124
distance: CGFloat,
124-
originDistance: CGFloat,
125-
newKey: ActionKey,
126125
step: CGFloat,
127126
canRepeat: Bool,
128127
fire: (_ reverse: Bool) -> ()
129128
) {
129+
let newKey = ActionKey.gesture(gesture.id)
130+
130131
if lastCommittedAction != newKey {
131-
magnificationDirection = distance >= originDistance ? 1 : -1
132+
magnificationKind = gesture.kind
132133
lastCommittedAction = newKey
133134
lastCommitMagnifyDistance = distance
134135
fire(false)
135136
return
136137
}
137138

138-
guard canRepeat else { return }
139+
guard canRepeat,
140+
let magnificationDirection = magnificationKind?.magnificationDirection
141+
else {
142+
return
143+
}
139144

140-
let delta = (distance - lastCommitMagnifyDistance) * CGFloat(magnificationDirection)
145+
let delta = (distance - lastCommitMagnifyDistance) * magnificationDirection
141146
if delta >= step {
142147
lastCommitMagnifyDistance = distance
143148
fire(false)
@@ -157,10 +162,10 @@ final class MultitouchGestureSession {
157162
lastCommitSwipeDistance = distance
158163
}
159164

160-
func switchMagnifyGesture(to gesture: GestureBinding, distance: CGFloat, direction: Int) {
165+
func switchMagnifyGesture(to gesture: GestureBinding, distance: CGFloat) {
161166
resolvedGesture = gesture
162167
lastCommittedAction = .gesture(gesture.id)
163-
magnificationDirection = direction
168+
magnificationKind = gesture.kind
164169
lastCommitMagnifyDistance = distance
165170
}
166171

@@ -172,3 +177,16 @@ final class MultitouchGestureSession {
172177
lastCommitMagnifyDistance = distance
173178
}
174179
}
180+
181+
private extension GestureBinding.Kind {
182+
var magnificationDirection: CGFloat? {
183+
switch self {
184+
case .magnifyIn:
185+
-1
186+
case .magnifyOut:
187+
1
188+
default:
189+
nil
190+
}
191+
}
192+
}

‎Loop/Core/Multitouch/MultitouchTargetResolver.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ final class MultitouchTargetResolver {
5555
minimumTitlebarHeight
5656
}
5757

58-
log.info("Detected titlebar height of \(titlebarHeight)")
58+
log.debug("Detected titlebar height of \(titlebarHeight)")
5959

6060
let titlebarMinY = window.frame.minY
6161
let titlebarMaxY = window.frame.minY + titlebarHeight
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//
2+
// MultitouchTrigger+Magnify.swift
3+
// Loop
4+
//
5+
// Created by Kai Azim on 2026-01-30.
6+
//
7+
8+
import CoreGraphics
9+
import Subsurface
10+
11+
/// Magnify handling lives separately from the main trigger as it has its own
12+
/// activation threshold and reversal model, including direction changes between
13+
/// magnify-in and magnify-out gestures and radial-menu center commits
14+
extension MultitouchTrigger {
15+
func handleMagnify(_ magnify: SubsurfaceGestureEvent.MagnifyEvent, fingerCount: Int) async {
16+
guard let entry = recognizerRegistry.entry(for: fingerCount) else { return }
17+
18+
// Radial menu magnify triggers the center action regardless of direction.
19+
if let radialMenuGesture = entry.radialMenuGesture {
20+
await handleRadialMenuMagnify(magnify, fingerCount: fingerCount, gesture: radialMenuGesture)
21+
return
22+
}
23+
24+
switch magnify.phase {
25+
case .began, .changed:
26+
if magnify.phase == .began,
27+
recognizerRegistry.session(for: fingerCount)?.hasGestureBegun != true {
28+
recognizerRegistry.session(for: fingerCount)?.reset()
29+
}
30+
guard let session = recognizerRegistry.session(for: fingerCount), !session.isGestureRejected else { return }
31+
32+
if !session.hasGestureBegun {
33+
let initialGesture = magnify.distance >= magnify.originDistance ? entry.magnifyOutGesture : entry.magnifyInGesture
34+
guard let initialGesture else {
35+
return
36+
}
37+
handleGestureBegan(fingerCount: fingerCount, gesture: initialGesture)
38+
recognizerRegistry.session(for: fingerCount)?.setResolvedGesture(initialGesture)
39+
}
40+
41+
guard let session = recognizerRegistry.session(for: fingerCount), !session.isGestureRejected,
42+
let activeGesture = session.resolvedGesture
43+
else {
44+
return
45+
}
46+
47+
guard await activateGestureIfNeeded(
48+
fingerCount: fingerCount,
49+
magnifyDisplacement: magnify.distance - magnify.originDistance,
50+
magnifyActivationThreshold: magnifyCycleStepSize
51+
),
52+
let session = recognizerRegistry.session(for: fingerCount),
53+
!session.isGestureRejected
54+
else {
55+
return
56+
}
57+
58+
if magnifyReversalDetected(currentGesture: activeGesture, magnify: magnify) {
59+
let opposite = activeGesture.kind == .magnifyIn ? entry.magnifyOutGesture : entry.magnifyInGesture
60+
handleMagnifyReversal(
61+
fingerCount: fingerCount,
62+
currentGesture: activeGesture,
63+
oppositeGesture: opposite,
64+
distance: magnify.distance
65+
)
66+
return
67+
}
68+
69+
let canRepeatAction = resolvedWindowAction(from: activeGesture).map {
70+
$0.canRepeat || $0.direction == .cycle
71+
} ?? false
72+
73+
session.commitMagnify(
74+
gesture: activeGesture,
75+
distance: magnify.distance,
76+
step: magnifyCycleStepSize,
77+
canRepeat: canRepeatAction
78+
) { reverse in
79+
triggerSingleAction(from: activeGesture, reverse: reverse)
80+
}
81+
82+
if let window = session.pendingTargetWindow,
83+
resolvedWindowAction(from: activeGesture)?.canRepeat == true {
84+
targetResolver.rememberRepeatableWindow(window, canRepeat: true)
85+
}
86+
87+
case .ended, .cancelled:
88+
resetLoopState(for: fingerCount)
89+
90+
default:
91+
break
92+
}
93+
}
94+
95+
/// Magnify within a radial menu gesture triggers the center (last) radial menu action.
96+
private func handleRadialMenuMagnify(
97+
_ magnify: SubsurfaceGestureEvent.MagnifyEvent,
98+
fingerCount: Int,
99+
gesture: GestureBinding
100+
) async {
101+
switch magnify.phase {
102+
case .began, .changed:
103+
if magnify.phase == .began, recognizerRegistry.session(for: fingerCount)?.hasGestureBegun != true {
104+
handleGestureBegan(fingerCount: fingerCount, gesture: gesture)
105+
}
106+
guard await activateGestureIfNeeded(
107+
fingerCount: fingerCount,
108+
magnifyDisplacement: magnify.distance - magnify.originDistance
109+
) else { return }
110+
guard let session = recognizerRegistry.session(for: fingerCount), !session.isGestureRejected else { return }
111+
112+
let actions = radialMenuActions
113+
guard !actions.isEmpty else { return }
114+
let centerActionIndex = actions.count - 1
115+
116+
session.commitRadialMagnify(
117+
distance: magnify.distance,
118+
originDistance: magnify.originDistance,
119+
step: magnifyCycleStepSize
120+
) { reverse in
121+
triggerRadialMenuAction(at: centerActionIndex, from: actions[...], reverse: reverse)
122+
}
123+
124+
case .ended, .cancelled:
125+
resetLoopState(for: fingerCount)
126+
127+
default:
128+
break
129+
}
130+
}
131+
132+
private func magnifyReversalDetected(
133+
currentGesture: GestureBinding,
134+
magnify: SubsurfaceGestureEvent.MagnifyEvent
135+
) -> Bool {
136+
switch currentGesture.kind {
137+
case .magnifyIn:
138+
magnify.distance >= magnify.originDistance
139+
case .magnifyOut:
140+
magnify.distance <= magnify.originDistance
141+
default:
142+
false
143+
}
144+
}
145+
146+
private func handleMagnifyReversal(
147+
fingerCount: Int,
148+
currentGesture: GestureBinding,
149+
oppositeGesture: GestureBinding?,
150+
distance: CGFloat
151+
) {
152+
if let oppositeGesture {
153+
guard let session = recognizerRegistry.session(for: fingerCount) else { return }
154+
session.switchMagnifyGesture(to: oppositeGesture, distance: distance)
155+
triggerSingleAction(from: oppositeGesture, reverse: false)
156+
157+
if let window = session.pendingTargetWindow,
158+
resolvedWindowAction(from: oppositeGesture)?.canRepeat == true {
159+
targetResolver.rememberRepeatableWindow(window, canRepeat: true)
160+
}
161+
} else if isCycleAction(currentGesture) {
162+
triggerSingleAction(from: currentGesture, reverse: true)
163+
recognizerRegistry.session(for: fingerCount)?.updateLastCommitMagnifyDistance(distance)
164+
} else {
165+
resetLoopState(for: fingerCount, forceClose: true)
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)