From 8c2e245c8baa1319725e6f4ff0072c4389b76962 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 14 Jun 2026 17:14:57 +0000 Subject: [PATCH] fix(apple/cursor): disable the client-side cursor (gamescope traps input) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client-side cursor positions the host pointer with ABSOLUTE events, but gamescope's input socket (EIS) grants only a relative pointer — the host drops the absolute events (libei.rs: no PointerAbsolute → not emitted), so the pointer never moves and clicks/scroll land on the stuck position. Auto-mode enabled exactly this on gamescope, making all input appear dead until toggled off. Force `cursorVisible = false`, neuter the ⌘⇧C toggle, and hide the now-inert Settings picker. The resolution logic + handlers are kept (commented) for when per-compositor gating (KWin/GNOME/Sway have an absolute pointer) or a synthetic-cursor-over-relative path lands. Relative capture (the working path) is now always used. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../PunktfunkClient/SettingsView.swift | 20 +++----------- .../Sources/PunktfunkKit/StreamView.swift | 27 +++++++++---------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/clients/apple/Sources/PunktfunkClient/SettingsView.swift b/clients/apple/Sources/PunktfunkClient/SettingsView.swift index 02a48bb..47cc2bb 100644 --- a/clients/apple/Sources/PunktfunkClient/SettingsView.swift +++ b/clients/apple/Sources/PunktfunkClient/SettingsView.swift @@ -18,7 +18,6 @@ struct SettingsView: View { @AppStorage(DefaultsKey.gamepadType) private var gamepadType = 0 @AppStorage(DefaultsKey.bitrateKbps) private var bitrateKbps = 0 @AppStorage(DefaultsKey.presenter) private var presenter = "stage1" - @AppStorage(DefaultsKey.cursorMode) private var cursorMode = "auto" @AppStorage(DefaultsKey.libraryEnabled) private var libraryEnabled = false @AppStorage(DefaultsKey.fullscreenWhileStreaming) private var fullscreenWhileStreaming = true @AppStorage(DefaultsKey.micEnabled) private var micEnabled = true @@ -385,22 +384,9 @@ struct SettingsView: View { .font(.caption) .foregroundStyle(.secondary) } - Section { - Picker("Cursor in stream", selection: $cursorMode) { - Text("Auto (gamescope)").tag("auto") - Text("Always").tag("always") - Text("Never").tag("never") - } - } header: { - Text("Cursor") - } footer: { - Text("Show the local system cursor over the stream instead of capturing it. " - + "gamescope's capture carries no cursor, so the client draws its own — " - + "Auto turns this on only for gamescope sessions. ⌘⇧C toggles it live " - + "during a session.") - .font(.caption) - .foregroundStyle(.secondary) - } + // The client-side cursor picker is hidden while the feature is disabled (gamescope's + // input is relative-only, so absolute cursor positioning traps input — see StreamView). + // Restore this Section when per-compositor gating / a synthetic cursor lands. #endif Section { Picker("Presenter", selection: $presenter) { diff --git a/clients/apple/Sources/PunktfunkKit/StreamView.swift b/clients/apple/Sources/PunktfunkKit/StreamView.swift index fd73c37..653afe9 100644 --- a/clients/apple/Sources/PunktfunkKit/StreamView.swift +++ b/clients/apple/Sources/PunktfunkKit/StreamView.swift @@ -532,23 +532,22 @@ public final class StreamLayerView: NSView { // guard as the ⌘⎋ capture toggle). Re-engage capture in the new mode so disassociation // and the absolute/relative forwarding choice swap atomically — releaseCapture restores // the old mode's grab (if any), engageCapture installs the new one. - capture.onToggleCursor = { [weak self] in - guard let self, self.window?.isKeyWindow == true else { return } - self.cursorVisible.toggle() - let wasCaptured = self.captured - self.releaseCapture() - if wasCaptured { self.engageCapture(fromClick: false) } - } + // ⌘⇧C would flip the client-side cursor live — NEUTERED while the feature is disabled + // (see the cursorVisible resolution below): toggling it on under gamescope's relative-only + // input traps the pointer. Restore this body when absolute/synthetic-cursor support lands. + capture.onToggleCursor = {} capture.start() inputCapture = capture - // Resolve the client-side-cursor mode for this session: Auto → on iff the host - // resolved gamescope (whose capture carries no cursor); Always → on; Never → off. - switch UserDefaults.standard.string(forKey: DefaultsKey.cursorMode) ?? "auto" { - case "always": cursorVisible = true - case "never": cursorVisible = false - default: cursorVisible = connection.resolvedCompositor == .gamescope - } + // Client-side cursor is TEMPORARILY DISABLED. It positions the host cursor with ABSOLUTE + // events, but gamescope's input socket (EIS) grants only a relative pointer, so those are + // silently dropped — the pointer never moves and clicks/scroll land on the stuck position + // (looks like "all input dead"). gamescope is exactly the compositor Auto enabled it for. + // Forced off until per-compositor gating (KWin/GNOME/Sway have absolute) or a synthetic- + // cursor-over-relative path lands; the resolution logic below is kept for that. See the + // ⌘⇧C handler (also neutered) and the cursorMode setting (hidden). + cursorVisible = false + _ = connection.resolvedCompositor // (was: Auto → gamescope; kept to document intent) // Presenter choice — default stage-1 (the known-good AVSampleBufferDisplayLayer). Stage-2 // (`punktfunk.presenter == "stage2"`) takes explicit VTDecompressionSession decode + a