From cd7ae2f3723348a9f04708bfb16a06aa15b9da5a Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sat, 8 Aug 2026 22:27:51 -0700 Subject: [PATCH] Pin the pointer while focus is armed, not just the drawing of it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hiding the cursor stops it being drawn; it keeps moving underneath. Every window it crossed still got mouseMoved, so hover states lit up in other apps while the trackpad was being played as a drum head. Disassociating the pointer leaves the deltas flowing, which is all focused FX and pitch bend ever read, while the pointer itself stops travelling. This is the second pointer-ownership behavior written for the direct build and lost to `#if !MAC_APP_STORE` in the fork that actually ships with the TrackDrum helper — the cursor hide was the first. Both now happen on arm, after the mode branch, since local FX idle shows and unlocks on its way in. Unlocking gets the more careful treatment of the pair. A pointer left disassociated is a mouse that does not move, so it is released on every capture exit and again on terminate, beside the cursor restore that already existed there for the reference-counted hide. Known gap: prepareFocusedLocalFXIdle still unlocks mid-session, and the watchdog only watches the hide, so that path can leave focus armed over a moving pointer. --- .../Sources/MenuBand/AppDelegate.swift | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/slab/menuband/Sources/MenuBand/AppDelegate.swift b/slab/menuband/Sources/MenuBand/AppDelegate.swift index 89e8c47cce..9f264eeea7 100644 --- a/slab/menuband/Sources/MenuBand/AppDelegate.swift +++ b/slab/menuband/Sources/MenuBand/AppDelegate.swift @@ -2049,10 +2049,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #else prepareFocusedLocalFXIdle() #endif - // Focus owns the pointer in every mode, and the hidden cursor is what - // says so. This lands after the mode branch because local FX idles - // with an ordinary pointer when unfocused and shows it on the way in. + // Focus owns the pointer in every mode, and the hidden, pinned cursor + // is what says so. This lands after the mode branch because local FX + // idles with an ordinary pointer when unfocused, showing and + // unlocking on the way in, and would otherwise undo both. hideSystemCursorIfNeeded() + lockSystemCursorIfNeeded() startFocusCursorWatchdog() updateIcon() updatePianoWaveformWindow() @@ -2060,6 +2062,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private func finishLocalCapture(reason: LocalKeyCapture.EndReason) { stopFocusCursorWatchdog() + unlockSystemCursorIfNeeded() showSystemCursorIfNeeded() debugLog("local capture ended: \(reason)") let shouldRestoreFocus = keyboardPerformanceFocusActive && reason == .cancelled @@ -2860,7 +2863,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // If we crashed (or were killed) mid-pitch-bend, the system // cursor stays hidden across sessions because CGDisplayHide/Show // are reference-counted globally. Restore here so the user never - // ends up with an invisible cursor on next launch. + // ends up with an invisible cursor on next launch. The pointer + // association matters more on the way out than the drawing does: + // a pointer left disassociated is a mouse that does not move. + unlockSystemCursorIfNeeded() showSystemCursorIfNeeded() pitchBendOverlay?.dismiss() } @@ -5959,6 +5965,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate { pitchBendSystemCursorHidden = false } + /// Disassociate the pointer from the mouse so the trackpad drives only + /// this app. Hiding the cursor stops it being *drawn*; it keeps moving + /// underneath, and every app it crosses still gets mouseMoved — hover + /// states light up in other windows while the user is playing a drum. + /// Deltas still arrive while disassociated, so focused FX is unaffected. + private func lockSystemCursorIfNeeded() { + guard !pitchBendCursorLocked else { return } + pitchBendLockScreenPoint = NSEvent.mouseLocation + CGAssociateMouseAndMouseCursorPosition(0) + pitchBendCursorLocked = true + } + + private func unlockSystemCursorIfNeeded() { + guard pitchBendCursorLocked else { return } + CGAssociateMouseAndMouseCursorPosition(1) + pitchBendCursorLocked = false + } + /// The hidden cursor is the proof Menu Band owns the pointer. If it comes /// back while focus is armed the focus is a lie: keys and the trackpad are /// still being read out from under a pointer the user can see moving. -- 2.51.2