From a730ca8557b58dedb29bbad94cadb51539db7518 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 10 Jun 2026 23:17:23 +0200 Subject: [PATCH] =?UTF-8?q?fix(apple):=20scroll=20from=20trackpads/Magic?= =?UTF-8?q?=20Mouse=20=E2=80=94=20forward=20NSEvent=20scrollWheel,=20drop?= =?UTF-8?q?=20GC=20scroll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scroll was wired to GCMouse's scroll dpad, which only fires for plain HID wheel deltas — trackpad and Magic Mouse scrolling are gesture events that never reach GameController, so scrolling was dead on the default Mac setups. The stream view now overrides scrollWheel (while captured the cursor is parked mid-view, so it receives every scroll event) and feeds InputCapture.sendScroll: precise gesture deltas are pixels (~0.1 notch/px, SDL's factor → ×12 for WHEEL_DELTA(120)), classic wheels are lines (×120), fractional remainders accumulate, and the GC scroll handler is gone so wheel mice can't double-deliver. Signs pass through as-is, preserving the local (natural-)scrolling preference. Co-Authored-By: Claude Fable 5 --- clients/apple/README.md | 5 +-- .../Sources/PunktfunkKit/InputCapture.swift | 32 ++++++++++++------- .../Sources/PunktfunkKit/StreamView.swift | 17 ++++++++++ 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/clients/apple/README.md b/clients/apple/README.md index e0a8bc3..187dde3 100644 --- a/clients/apple/README.md +++ b/clients/apple/README.md @@ -37,8 +37,9 @@ What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3): thread per view, token-cancelled so reconnects can't double-pump. - `InputCapture.swift` — `GCMouse` raw deltas + `GCKeyboard` HID→VK mapping (the host's `vk_to_evdev` consumes Windows VKs), with fractional-delta accumulation so sub-pixel - motion isn't truncated away. Buttons use GameStream ids (1=left … 5=X2); scroll is - WHEEL_DELTA(120)-scaled. + motion isn't truncated away. Buttons use GameStream ids (1=left … 5=X2). Scroll + arrives via the stream view's `scrollWheel` override instead of GC (trackpad/Magic + Mouse gestures never reach GCMouse's scroll dpad), WHEEL_DELTA(120)-scaled. - **`PunktfunkClient`** (the app): hosts grid (saved in UserDefaults), "+" toolbar sheet to add hosts, stream mode in Settings (⌘,), two trust flows — the trust-on-first-use fingerprint prompt over the live-but-blurred stream, and SPAKE2 PIN diff --git a/clients/apple/Sources/PunktfunkKit/InputCapture.swift b/clients/apple/Sources/PunktfunkKit/InputCapture.swift index 73aedb6..6461c0e 100644 --- a/clients/apple/Sources/PunktfunkKit/InputCapture.swift +++ b/clients/apple/Sources/PunktfunkKit/InputCapture.swift @@ -237,18 +237,26 @@ public final class InputCapture { } } } - input.scroll.valueChangedHandler = { [weak self] _, x, y in - guard let self, self.forwarding else { return } - // WHEEL_DELTA(120) per notch; positive = up / right (Moonlight's convention). - let fy = y * 120 + self.residualScrollY - let fx = x * 120 + self.residualScrollX - let iy = fy.rounded(.towardZero) - let ix = fx.rounded(.towardZero) - self.residualScrollY = fy - iy - self.residualScrollX = fx - ix - if iy != 0 { self.connection.send(.scroll(Int32(iy))) } - if ix != 0 { self.connection.send(.scroll(Int32(ix), horizontal: true)) } - } + // NOTE: no scroll handler here. GCMouse's scroll dpad only fires for plain HID + // wheel deltas — trackpad/Magic Mouse scrolling is gesture-based and never + // reaches GameController. Scroll arrives via the stream view's scrollWheel + // override (NSEvent covers wheels too) → sendScroll(). + } + + /// Forward a scroll gesture, WHEEL_DELTA(120)-scaled (positive = up / right, + /// Moonlight's convention). Fed by StreamLayerView.scrollWheel — the only delivery + /// path that covers trackpad/Magic Mouse gestures (GCMouse never reports them). + /// Fractional remainders accumulate so slow two-finger scrolling isn't truncated away. + public func sendScroll(dx: Float, dy: Float) { + guard forwarding else { return } + let fy = dy + residualScrollY + let fx = dx + residualScrollX + let iy = fy.rounded(.towardZero) + let ix = fx.rounded(.towardZero) + residualScrollY = fy - iy + residualScrollX = fx - ix + if iy != 0 { connection.send(.scroll(Int32(iy))) } + if ix != 0 { connection.send(.scroll(Int32(ix), horizontal: true)) } } private func attach(keyboard: GCKeyboard) { diff --git a/clients/apple/Sources/PunktfunkKit/StreamView.swift b/clients/apple/Sources/PunktfunkKit/StreamView.swift index 5907ae7..7902b0d 100644 --- a/clients/apple/Sources/PunktfunkKit/StreamView.swift +++ b/clients/apple/Sources/PunktfunkKit/StreamView.swift @@ -215,6 +215,23 @@ public final class StreamLayerView: NSView { super.mouseUp(with: event) } + /// Scroll is forwarded from here, not from GCMouse: trackpad/Magic Mouse gestures + /// never reach GameController's scroll dpad. While captured the cursor is parked + /// mid-view, so this view receives every scroll event. Precise (gesture) deltas are + /// pixels — ~0.1 wheel notch per pixel (SDL's factor) → ×12 for WHEEL_DELTA(120); + /// classic wheels report lines, one notch = ±1 → ×120. Signs pass through as-is, + /// preserving the user's local (natural-)scrolling preference. + public override func scrollWheel(with event: NSEvent) { + guard captured, let inputCapture else { + super.scrollWheel(with: event) + return + } + let scale: Float = event.hasPreciseScrollingDeltas ? 12 : 120 + inputCapture.sendScroll( + dx: Float(event.scrollingDeltaX) * scale, + dy: Float(event.scrollingDeltaY) * scale) + } + // While captured, the view is first responder and consumes key events — GC delivers // them to the host independently, and consuming here stops the responder chain's // "unhandled keyDown" beep without touching the event stream GC may rely on.