fix(apple): drive macOS keyboard from NSEvent (GCKeyboard unreliable)
ci / rust (push) Has been cancelled

macOS GCKeyboard delivery is flaky — the same GameController quirk that
killed GCMouse motion (e414ec0). Keyboard input intermittently failed to
reach the host (e.g. typing in a gamescope game). Switch the macOS key
source to NSEvent, mirroring the mouse fix:

- StreamLayerView.keyDown/keyUp map NSEvent.keyCode (Carbon virtual
  keycode) → Windows VK via the new InputCapture.keyCodeToVK table and
  forward through InputCapture.sendKey, then consume the event (no beep).
- flagsChanged drives InputCapture.handleFlagsChanged, which diffs the raw
  modifier flags to recover each L/R modifier down/up (modifiers never fire
  keyDown/keyUp on macOS) and emits the same L/R VKs hidToVK already does.
- The macOS GCKeyboard keyChangedHandler is disabled (#if !os(macOS)) so it
  can't double-send; iOS keeps the GCKeyboard path unchanged.

sendKey honors the ⌘⎋ capture-toggle suppressedVK latch and tracks into
pressedVKs so releaseAll()/blur flushes anything still held. The emitted
VKs are identical to the existing HID path, so the host (vk_to_evdev)
needs no change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 18:10:13 +00:00
parent 6425f9995f
commit dcb2850c7c
2 changed files with 179 additions and 6 deletions
@@ -228,19 +228,44 @@ public final class StreamLayerView: NSView {
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.
// -combos arrive via performKeyEquivalent instead and stay fully functional (D).
// While captured, the view is first responder and SENDS key events to the host straight
// from NSEvent GCKeyboard delivery proved unreliable on macOS (the same GameController
// quirk that killed GCMouse motion, fixed in e414ec0), so the macOS GCKeyboard send path
// is disabled and NSEvent is the single source. We map NSEvent.keyCode (a Carbon virtual
// keycode) Windows VK and forward via InputCapture.sendKey, then CONSUME (return without
// super) to stop the responder chain's "unhandled keyDown" beep. Keys with no VK mapping
// are still consumed while captured so they don't beep either. The toggle's Esc is
// swallowed upstream by InputCapture's keyDown monitor (suppressedVK), so it never gets
// here as a send; -combos still arrive via performKeyEquivalent and stay functional (D).
// Modifier keys never fire keyDown/keyUp they come through flagsChanged below.
public override var acceptsFirstResponder: Bool { true }
public override func keyDown(with event: NSEvent) {
if captured { return }
if captured {
if let ic = inputCapture, let vk = InputCapture.keyCodeToVK[event.keyCode] {
ic.sendKey(vk, down: true) // autorepeat (event.isARepeat) passes through fine for VK
}
return // consume even unmapped keys while captured (no beep)
}
super.keyDown(with: event)
}
public override func keyUp(with event: NSEvent) {
if captured { return }
if captured {
if let ic = inputCapture, let vk = InputCapture.keyCodeToVK[event.keyCode] {
ic.sendKey(vk, down: false)
}
return
}
super.keyUp(with: event)
}
/// Modifier keys (shift/control/option/command) arrive ONLY as flagsChanged on macOS,
/// never keyDown/keyUp InputCapture diffs the raw flags to recover each L/R down/up.
public override func flagsChanged(with event: NSEvent) {
if captured, let inputCapture {
inputCapture.handleFlagsChanged(UInt(event.modifierFlags.rawValue))
return
}
super.flagsChanged(with: event)
}
private func requestAutoCapture() {
pendingAutoCapture = true