fix(gamepad/host): keep Steam Deck trackpad clicks across a button frame (G2)

SteamControllerManager::handle rebuilds `SteamState.buttons` from the gamepad
frame every tick via from_gamepad, preserving only the rich-plane TOUCH bits —
so a held trackpad CLICK (set on the rich plane by apply_rich, stored in
`buttons`) was wiped on the very next button/stick frame and only flickered
back on the next rich event. This is the exact trap the DualSense backend
already dodges by keeping click in a separate `touch_click` field.

Mirror that: add persisted `lpad_click`/`rpad_click` bools to SteamState set by
apply_rich (instead of pressing LPAD_CLICK/RPAD_CLICK into `buttons`), OR them
into the report's click bits in serialize_deck_state, and preserve them across
the rebuild in handle() like touch/coords/motion. RPAD_CLICK's other owner —
the DualSense touchpad-click wire button via from_gamepad — stays in `buttons`
and is OR'd at serialize, so the two sources release independently (a released
BTN_TOUCHPAD can't strand a rich click, and vice-versa).

Adds a regression test (rich_click_survives_a_buttons_rebuild). All 17
inject::{steam,dualsense,dualshock4}_proto tests pass on Linux (.21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 12:04:59 +02:00
parent 236c59754b
commit 2642ba6ad0
2 changed files with 63 additions and 4 deletions
@@ -415,6 +415,12 @@ impl SteamControllerManager {
s.gyro = prev.gyro;
s.accel = prev.accel;
s.buttons |= prev.buttons & (btn::RPAD_TOUCH | btn::LPAD_TOUCH);
// Trackpad CLICK arrives on the rich plane too and must survive a button-only frame,
// exactly like touch/coords/motion above. It lives in its own fields (not `buttons`,
// which `from_gamepad` just rebuilt) so preserving it can't strand the BTN_TOUCHPAD
// wire-button's RPAD_CLICK — the two are OR'd only at serialize.
s.lpad_click = prev.lpad_click;
s.rpad_click = prev.rpad_click;
self.state[idx] = s;
self.write(idx);
}