fix(apple/gamepad): reclaim the PS/Home button from the macOS system gesture
ci / docs-site (push) Successful in 31s
ci / rust (push) Successful in 6m30s
deb / build-publish (push) Successful in 3m58s
ci / web (push) Successful in 27s
apple / swift (push) Successful in 1m16s
ci / bench (push) Successful in 1m34s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m13s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m18s
docker / deploy-docs (push) Successful in 17s
ci / docs-site (push) Successful in 31s
ci / rust (push) Successful in 6m30s
deb / build-publish (push) Successful in 3m58s
ci / web (push) Successful in 27s
apple / swift (push) Successful in 1m16s
ci / bench (push) Successful in 1m34s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m13s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m18s
docker / deploy-docs (push) Successful in 17s
The earlier buttonHome handler wasn't enough: on macOS the SYSTEM grabs the DualSense Home/PS button by default (opens Launchpad's Games folder), so it never reached the app. The fix is to disable the system gesture on the element — `physicalInputProfile.buttons[GCInputButtonHome].preferredSystemGestureState = .disabled` (Apple's documented mechanism) — which hands the button to us. Then drive `guide` DIRECTLY from that element's pressedChangedHandler instead of via buttonMask: the legacy `extendedGamepad.buttonHome` is unreliable/often nil even when the physical element exists, so reading it in the mask dropped presses. `sendGuide` folds the bit into `buttons` so a held PS button still releases on focus loss. On tvOS the element is reserved (nil) → the block no-ops. The host already maps BTN_GUIDE → the DualSense PS bit, so this completes the chain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -169,13 +169,16 @@ public final class GamepadCapture {
|
|||||||
ext.valueChangedHandler = { [weak self] g, _ in
|
ext.valueChangedHandler = { [weak self] g, _ in
|
||||||
MainActor.assumeIsolated { self?.sync(g) }
|
MainActor.assumeIsolated { self?.sync(g) }
|
||||||
}
|
}
|
||||||
// The Home/PS button (→ guide; the host maps it to the DualSense PS / Xbox guide bit) does
|
// The Home/PS button (→ guide; the host maps it to the DualSense PS / Xbox guide bit). On
|
||||||
// NOT reliably fire the gamepad's valueChangedHandler on macOS, so its presses were dropped.
|
// macOS the SYSTEM grabs it by default (opens Launchpad's Games folder), so it never reached
|
||||||
// A dedicated handler re-syncs on every Home transition.
|
// the app — `preferredSystemGestureState = .disabled` on the element is what hands it to us.
|
||||||
ext.buttonHome?.pressedChangedHandler = { [weak self] _, _, _ in
|
// We drive `guide` DIRECTLY from this handler's pressed value (not via buttonMask), because
|
||||||
MainActor.assumeIsolated {
|
// the legacy `extendedGamepad.buttonHome` is unreliable/often nil even when the physical
|
||||||
guard let self, let g = self.bound?.extendedGamepad else { return }
|
// element exists. On tvOS the element is absent (reserved) → nil, the whole block no-ops.
|
||||||
self.sync(g)
|
if let home = c.physicalInputProfile.buttons[GCInputButtonHome] {
|
||||||
|
home.preferredSystemGestureState = .disabled
|
||||||
|
home.pressedChangedHandler = { [weak self] _, _, pressed in
|
||||||
|
MainActor.assumeIsolated { self?.sendGuide(down: pressed) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Wake the host pad immediately (pads are created lazily from the first event;
|
// Wake the host pad immediately (pads are created lazily from the first event;
|
||||||
@@ -224,6 +227,18 @@ public final class GamepadCapture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forward the guide (Home/PS) transition directly — it's kept out of `buttonMask` (the legacy
|
||||||
|
/// `buttonHome` element is unreliable). Folds into `buttons` so a held PS button is released by
|
||||||
|
/// `releaseAll` on focus loss just like the others.
|
||||||
|
private func sendGuide(down: Bool) {
|
||||||
|
guard !suspended else { return }
|
||||||
|
let bit = GamepadWire.guide
|
||||||
|
let now = down ? (buttons | bit) : (buttons & ~bit)
|
||||||
|
guard now != buttons else { return }
|
||||||
|
connection.send(.gamepadButton(bit, down: down, pad: 0))
|
||||||
|
buttons = now
|
||||||
|
}
|
||||||
|
|
||||||
private static func buttonMask(_ g: GCExtendedGamepad) -> UInt32 {
|
private static func buttonMask(_ g: GCExtendedGamepad) -> UInt32 {
|
||||||
var b: UInt32 = 0
|
var b: UInt32 = 0
|
||||||
if g.dpad.up.isPressed { b |= GamepadWire.dpadUp }
|
if g.dpad.up.isPressed { b |= GamepadWire.dpadUp }
|
||||||
@@ -236,7 +251,8 @@ public final class GamepadCapture {
|
|||||||
if g.rightThumbstickButton?.isPressed == true { b |= GamepadWire.rightStickClick }
|
if g.rightThumbstickButton?.isPressed == true { b |= GamepadWire.rightStickClick }
|
||||||
if g.leftShoulder.isPressed { b |= GamepadWire.leftShoulder }
|
if g.leftShoulder.isPressed { b |= GamepadWire.leftShoulder }
|
||||||
if g.rightShoulder.isPressed { b |= GamepadWire.rightShoulder }
|
if g.rightShoulder.isPressed { b |= GamepadWire.rightShoulder }
|
||||||
if g.buttonHome?.isPressed == true { b |= GamepadWire.guide }
|
// guide (Home/PS) is NOT read here — it's forwarded directly by the Home button's
|
||||||
|
// pressedChangedHandler (the legacy `buttonHome` element is unreliable). See `rebind`.
|
||||||
if g.buttonA.isPressed { b |= GamepadWire.a }
|
if g.buttonA.isPressed { b |= GamepadWire.a }
|
||||||
if g.buttonB.isPressed { b |= GamepadWire.b }
|
if g.buttonB.isPressed { b |= GamepadWire.b }
|
||||||
if g.buttonX.isPressed { b |= GamepadWire.x }
|
if g.buttonX.isPressed { b |= GamepadWire.x }
|
||||||
|
|||||||
Reference in New Issue
Block a user