Skip to content

Commit f7a5cc2

Browse files
committed
Add adaptive crisp zoom and harden multitouch input
Crisp zoom: raise the GL drawable density to match the host source while magnified so zoomed content stays sharp, gated on actual zoom scale so 1x keeps native density. Re-issue SetDimensions on scale change so the SDK fills the new drawable. Touch robustness: tap recognizers no longer delay/cancel touches so the overlay always sees the real touch lifecycle; flush touch state on resign-active; sweep an OS-orphaned ghost touch when another finger is actively moving.
1 parent cacc818 commit f7a5cc2

2 files changed

Lines changed: 87 additions & 17 deletions

File tree

OpenParsec/ParsecGLKRenderer.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class ParsecGLKRenderer: NSObject, GLKViewDelegate, GLKViewControllerDelegate {
66
var glkViewController: GLKViewController
77

88
var lastWidth: CGFloat = 1.0
9+
var lastScale: CGFloat = 0
910

1011
var lastImg: CGImage?
1112
let updateImage: () -> Void
@@ -30,9 +31,10 @@ class ParsecGLKRenderer: NSObject, GLKViewDelegate, GLKViewControllerDelegate {
3031

3132
func glkView(_ view: GLKView, drawIn rect: CGRect) {
3233
let deltaWidth: CGFloat = view.frame.size.width - lastWidth
33-
if deltaWidth > 0.1 || deltaWidth < -0.1 {
34-
CParsec.setFrame(view.frame.size.width, view.frame.size.height, view.contentScaleFactor)
35-
lastWidth = view.frame.size.width
34+
if deltaWidth > 0.1 || deltaWidth < -0.1 || abs(view.contentScaleFactor - lastScale) > 0.01 {
35+
CParsec.setFrame(view.frame.size.width, view.frame.size.height, view.contentScaleFactor)
36+
lastWidth = view.frame.size.width
37+
lastScale = view.contentScaleFactor
3638
}
3739

3840
// Calculate timeout based on configured/device frame rate

OpenParsec/ParsecViewController.swift

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ class ParsecViewController: UIViewController, UIScrollViewDelegate, ParsecTouchI
1919
var lastMouseX: Int32 = -1
2020
var lastMouseY: Int32 = -1
2121
var lastCursorHidden: Bool = false
22-
var isPinching = false
2322
var zoomEnabled = false
23+
var appliedRenderScale: CGFloat = 0 // last contentScaleFactor applied (avoids redundant reallocations)
24+
let maxRenderScale: CGFloat = 6.0 // cap on drawable density
2425
var clickHoldActive = false // left button held via long-press (file drag)
2526
// Flick-to-glide momentum (tuned constants, not user settings).
2627
var momentumLink: CADisplayLink?
@@ -40,6 +41,8 @@ class ParsecViewController: UIViewController, UIScrollViewDelegate, ParsecTouchI
4041
var cursorDidMoveThisTouch = false
4142
var singleTouchStartScreen: CGPoint = .zero
4243
let tapMoveSlop: CGFloat = 4.0 // finger movement (pts) above which a touch is a drag, not a tap
44+
let staleTouchThreshold: TimeInterval = 1.5 // a touch idle this long vs an actively-moving one is a ghost
45+
let staleMoveSlop: CGFloat = 24.0 // the moving finger must travel this far before a ghost is swept
4346
var twoFingerDidMove = false // true once a 2-finger gesture became a pinch/scroll (suppresses right-click)
4447

4548
var mouseSensitivity: Float = Float(SettingsHandler.mouseSensitivity)
@@ -54,6 +57,7 @@ class ParsecViewController: UIViewController, UIScrollViewDelegate, ParsecTouchI
5457
var isDragging = false
5558
var twoFingerResidual = false // leftover finger after a 2-finger gesture; inert until full lift
5659
var activeTouches: [UITouch] = [] // ordered; first two drive pinch / scroll
60+
var touchOrigins: [UITouch: CGPoint] = [:] // begin location per touch, for ghost-sweep movement test
5761

5862
enum TwoFingerMode { case undecided, zoom, scroll }
5963
var twoFingerMode: TwoFingerMode = .undecided
@@ -90,7 +94,31 @@ class ParsecViewController: UIViewController, UIScrollViewDelegate, ParsecTouchI
9094
fatalError("init(coder:) has not been implemented")
9195
}
9296

97+
func computeRenderScale() -> CGFloat {
98+
let nativeScale = UIScreen.main.nativeScale
99+
guard scrollView.zoomScale > 1.01, let glk = glkView as? ParsecGLKViewController else { return nativeScale }
100+
let vw = glk.glkView.frame.size.width
101+
let vh = glk.glkView.frame.size.height
102+
let sw = CGFloat(CParsec.hostWidth)
103+
let sh = CGFloat(CParsec.hostHeight)
104+
guard vw > 0, vh > 0, sw > 0, sh > 0 else { return nativeScale }
105+
// Match the drawable to the source so zoom stays crisp without rendering wasted pixels.
106+
let needed = max(sw / vw, sh / vh)
107+
return min(maxRenderScale, max(nativeScale, needed))
108+
}
109+
110+
func updateRenderScale() {
111+
let target = computeRenderScale()
112+
if abs(target - appliedRenderScale) > 0.01 {
113+
appliedRenderScale = target
114+
DispatchQueue.main.async { [weak self] in
115+
(self?.glkView as? ParsecGLKViewController)?.glkView.contentScaleFactor = target
116+
}
117+
}
118+
}
119+
93120
func updateImage() {
121+
updateRenderScale()
94122
// Optimization: Snap current valus
95123
let currentMouseX = CParsec.mouseInfo.mouseX
96124
let currentMouseY = CParsec.mouseInfo.mouseY
@@ -212,27 +240,30 @@ class ParsecViewController: UIViewController, UIScrollViewDelegate, ParsecTouchI
212240
// All gestures (cursor, pinch-zoom, two-finger scroll) are handled in the
213241
// ParsecTouchInputDelegate methods below, driven by touchOverlay's raw touches.
214242

215-
// Remove custom Pinch logic, ScrollView handles it.
216-
// But we might want to know isPinching status?
217-
// Let's rely on ScrollView delegate for updates.
218-
219243
// Add tap gesture recognizer for single-finger touch
220244
let singleFingerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleSingleFingerTap(_:)))
221245
singleFingerTapGestureRecognizer.numberOfTouchesRequired = 1
222246
singleFingerTapGestureRecognizer.allowedTouchTypes = [0, 2]
247+
// Let the overlay see the real touch end so no phantom finger lingers.
248+
singleFingerTapGestureRecognizer.cancelsTouchesInView = false
249+
singleFingerTapGestureRecognizer.delaysTouchesEnded = false
223250
view.addGestureRecognizer(singleFingerTapGestureRecognizer)
224251

225252
// Add tap gesture recognizer for two-finger touch
226253
let twoFingerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTwoFingerTap(_:)))
227254
twoFingerTapGestureRecognizer.numberOfTouchesRequired = 2
228255
twoFingerTapGestureRecognizer.allowedTouchTypes = [0]
256+
twoFingerTapGestureRecognizer.cancelsTouchesInView = false
257+
twoFingerTapGestureRecognizer.delaysTouchesEnded = false
229258
view.addGestureRecognizer(twoFingerTapGestureRecognizer)
230259
// view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
231260
// view.backgroundColor = UIColor(red: 0x66, green: 0xcc, blue: 0xff, alpha: 1.0)
232261

233262
let threeFingerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleThreeFinderTap(_:)))
234263
threeFingerTapGestureRecognizer.numberOfTouchesRequired = 3
235264
threeFingerTapGestureRecognizer.allowedTouchTypes = [0]
265+
threeFingerTapGestureRecognizer.cancelsTouchesInView = false
266+
threeFingerTapGestureRecognizer.delaysTouchesEnded = false
236267
view.addGestureRecognizer(threeFingerTapGestureRecognizer)
237268

238269
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
@@ -256,6 +287,14 @@ class ParsecViewController: UIViewController, UIScrollViewDelegate, ParsecTouchI
256287
object: nil
257288
)
258289

290+
// Backgrounding / Control Center / system alerts can orphan touches; flush on resign.
291+
NotificationCenter.default.addObserver(
292+
self,
293+
selector: #selector(appWillResignActive),
294+
name: UIApplication.willResignActiveNotification,
295+
object: nil
296+
)
297+
259298
}
260299

261300
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@@ -300,6 +339,7 @@ class ParsecViewController: UIViewController, UIScrollViewDelegate, ParsecTouchI
300339
}
301340
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
302341
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
342+
NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
303343
}
304344

305345

@@ -465,9 +505,31 @@ extension ParsecViewController: UIGestureRecognizerDelegate {
465505
let oldCount = activeTouches.count
466506
// Rebuild from the event each time (keeping order) so a cancelled touch can't leave a stale one.
467507
var ordered = activeTouches.filter { touches.contains($0) }
468-
for t in touches where !ordered.contains(t) { ordered.append(t) }
508+
for t in touches where !ordered.contains(t) {
509+
ordered.append(t)
510+
touchOrigins[t] = t.location(in: view)
511+
}
512+
// Sweep a ghost (an OS-dropped lift lingering as .stationary) only while another finger actively
513+
// moves - a resting or just-tapped finger is never swept, so taps and held fingers stay safe.
514+
var sweptGhost = false
515+
if ordered.count >= 2 && !clickHoldActive,
516+
let newest = ordered.max(by: { $0.timestamp < $1.timestamp }) {
517+
let origin = touchOrigins[newest] ?? newest.location(in: view)
518+
let here = newest.location(in: view)
519+
if hypot(here.x - origin.x, here.y - origin.y) > staleMoveSlop {
520+
let fresh = ordered.filter { newest.timestamp - $0.timestamp < staleTouchThreshold }
521+
if fresh.count < ordered.count { ordered = fresh; sweptGhost = true }
522+
}
523+
}
469524
activeTouches = ordered
470-
if activeTouches.count != oldCount {
525+
touchOrigins = touchOrigins.filter { activeTouches.contains($0.key) }
526+
if sweptGhost && activeTouches.count == 1 {
527+
// Recover the surviving finger as a live cursor instead of inert two-finger residual.
528+
stopMomentum()
529+
twoFingerResidual = false
530+
twoFingerMode = .undecided
531+
startCursor()
532+
} else if activeTouches.count != oldCount {
471533
handleTouchCountChange(old: oldCount, new: activeTouches.count)
472534
}
473535
if moved { handleActiveTouchesMoved() }
@@ -509,6 +571,19 @@ extension ParsecViewController: UIGestureRecognizerDelegate {
509571
}
510572
}
511573

574+
@objc private func appWillResignActive() { flushAllTouches() }
575+
576+
// Drop all touch state and release any held button so nothing sticks when backgrounded.
577+
private func flushAllTouches() {
578+
stopMomentum()
579+
releaseClickHoldIfNeeded()
580+
endCursorIfNeeded()
581+
isDragging = false
582+
twoFingerResidual = false
583+
twoFingerMode = .undecided
584+
activeTouches = []
585+
}
586+
512587
private func startCursor() {
513588
isDragging = true
514589
twoFingerResidual = false
@@ -815,14 +890,7 @@ extension ParsecViewController: UIGestureRecognizerDelegate {
815890
// Centering is handled on cursor movement (updateImage) and at the end of a pinch.
816891
}
817892

818-
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
819-
// A pinch-to-zoom started: pause two-finger host scrolling until it ends so the
820-
// same two-finger gesture isn't interpreted as both a zoom and a scroll.
821-
isPinching = true
822-
}
823-
824893
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
825-
isPinching = false
826894
centerViewportOnCursorPos()
827895
}
828896

0 commit comments

Comments
 (0)