fix(client-linux): Deck trackpad clicks — bind to the correct pad, stop riding the button plane
apple / swift (push) Successful in 1m6s
ci / rust (push) Successful in 1m27s
ci / web (push) Successful in 50s
android / android (push) Successful in 3m19s
ci / docs-site (push) Successful in 1m6s
apple / screenshots (push) Successful in 5m29s
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
apple / swift (push) Successful in 1m6s
ci / rust (push) Successful in 1m27s
ci / web (push) Successful in 50s
android / android (push) Successful in 3m19s
ci / docs-site (push) Successful in 1m6s
apple / screenshots (push) Successful in 5m29s
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
SDL's Steam Deck mapping delivers the pad clicks as gamepad BUTTONS with no surface identity: the generic `touchpad` button is the LEFT pad's click and `misc2` the RIGHT's (SDL_gamepad_db.h `touchpad:b17,misc2:b16`). The client forwarded `touchpad` as wire BTN_TOUCHPAD — which the host maps to the RIGHT pad click (DualSense convention) — and dropped `misc2` entirely: a left-pad click registered on the right pad, a right-pad click nowhere, and the mis-routed state could stick. Clicks from a multi-touchpad pad now ride the rich plane as TouchpadEx with their surface, reusing the surface's live contact point (click buttons carry no position). forward_touch carries the held click through motion frames so a touch update can't clear a click mid-press, and the flush lifts held clicks on detach/pad-switch. A DualSense's single touchpad button stays on the button plane unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+102
-10
@@ -551,6 +551,14 @@ struct Worker<'a> {
|
|||||||
/// switch / detach so a contact held at that moment doesn't stick. surface 0 = the legacy single
|
/// switch / detach so a contact held at that moment doesn't stick. surface 0 = the legacy single
|
||||||
/// touchpad, 1/2 = a Steam left/right pad.
|
/// touchpad, 1/2 = a Steam left/right pad.
|
||||||
held_touches: std::collections::HashSet<(u8, u8)>,
|
held_touches: std::collections::HashSet<(u8, u8)>,
|
||||||
|
/// Per Steam-pad surface (index 0 = left/surface 1, 1 = right/surface 2): the last wire
|
||||||
|
/// coordinates + whether a finger is on it. Pad CLICKS arrive as buttons with no position,
|
||||||
|
/// so the click forward reuses the surface's live contact point.
|
||||||
|
surface_last: [(i16, i16, bool); 2],
|
||||||
|
/// Steam-pad clicks currently held (surface−1 indexed): keeps the click bit asserted
|
||||||
|
/// through touch-motion frames (which would otherwise clear it host-side) and lets the
|
||||||
|
/// flush lift a click held across detach/pad-switch.
|
||||||
|
held_clicks: [bool; 2],
|
||||||
last_accel: [i16; 3],
|
last_accel: [i16; 3],
|
||||||
/// Raises the UI escape signal; the escape chord fires it once per press.
|
/// Raises the UI escape signal; the escape chord fires it once per press.
|
||||||
escape_tx: async_channel::Sender<()>,
|
escape_tx: async_channel::Sender<()>,
|
||||||
@@ -681,6 +689,24 @@ impl Worker<'_> {
|
|||||||
}
|
}
|
||||||
*v = i32::MIN;
|
*v = i32::MIN;
|
||||||
}
|
}
|
||||||
|
// Lift any Steam-pad click held at this moment — a click that survives a
|
||||||
|
// detach/pad-switch would leave the host's pad pressed forever.
|
||||||
|
for i in 0..2usize {
|
||||||
|
if std::mem::take(&mut self.held_clicks[i]) {
|
||||||
|
let (x, y, _) = self.surface_last[i];
|
||||||
|
let _ = c.send_rich_input(RichInput::TouchpadEx {
|
||||||
|
pad: 0,
|
||||||
|
surface: (i as u8) + 1,
|
||||||
|
finger: 0,
|
||||||
|
touch: false,
|
||||||
|
click: false,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
pressure: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.surface_last = [(0, 0, false); 2];
|
||||||
// Lift any touchpad contact the host still believes is down (surface 0 = legacy pad).
|
// Lift any touchpad contact the host still believes is down (surface 0 = legacy pad).
|
||||||
for (surface, finger) in self.held_touches.drain() {
|
for (surface, finger) in self.held_touches.drain() {
|
||||||
let rich = if surface == 0 {
|
let rich = if surface == 0 {
|
||||||
@@ -709,6 +735,8 @@ impl Worker<'_> {
|
|||||||
self.held_buttons.clear();
|
self.held_buttons.clear();
|
||||||
self.last_axis = [i32::MIN; 6];
|
self.last_axis = [i32::MIN; 6];
|
||||||
self.held_touches.clear();
|
self.held_touches.clear();
|
||||||
|
self.held_clicks = [false; 2];
|
||||||
|
self.surface_last = [(0, 0, false); 2];
|
||||||
}
|
}
|
||||||
// A held chord doesn't survive a flush (detach / pad-switch) — clear its latches too.
|
// A held chord doesn't survive a flush (detach / pad-switch) — clear its latches too.
|
||||||
self.reset_chord();
|
self.reset_chord();
|
||||||
@@ -789,26 +817,29 @@ impl Worker<'_> {
|
|||||||
y: f32,
|
y: f32,
|
||||||
active: bool,
|
active: bool,
|
||||||
) {
|
) {
|
||||||
let Some(c) = self.attached.as_ref() else {
|
let Some(c) = self.attached.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let multi = self
|
let multi = self.is_multi_touchpad(which);
|
||||||
.open
|
|
||||||
.as_ref()
|
|
||||||
.filter(|(id, _)| *id == which)
|
|
||||||
.map(|(_, p)| p.touchpads_count() >= 2)
|
|
||||||
.unwrap_or(false);
|
|
||||||
let (cx, cy) = (x.clamp(0.0, 1.0), y.clamp(0.0, 1.0));
|
let (cx, cy) = (x.clamp(0.0, 1.0), y.clamp(0.0, 1.0));
|
||||||
let surface = if multi { (touchpad as u8) + 1 } else { 0 };
|
let surface = if multi { (touchpad as u8) + 1 } else { 0 };
|
||||||
let rich = if multi {
|
let rich = if multi {
|
||||||
|
let (wx, wy) = (
|
||||||
|
(cx * 65535.0 - 32768.0) as i16,
|
||||||
|
(cy * 65535.0 - 32768.0) as i16,
|
||||||
|
);
|
||||||
|
let i = (surface - 1).min(1) as usize;
|
||||||
|
self.surface_last[i] = (wx, wy, active);
|
||||||
RichInput::TouchpadEx {
|
RichInput::TouchpadEx {
|
||||||
pad: 0,
|
pad: 0,
|
||||||
surface,
|
surface,
|
||||||
finger,
|
finger,
|
||||||
touch: active,
|
touch: active,
|
||||||
click: false,
|
// The pad's physical click is a separate BUTTON event (see forward_click) —
|
||||||
x: (cx * 65535.0 - 32768.0) as i16,
|
// carry the held state so a motion frame can't clear a click mid-press.
|
||||||
y: (cy * 65535.0 - 32768.0) as i16,
|
click: self.held_clicks[i],
|
||||||
|
x: wx,
|
||||||
|
y: wy,
|
||||||
pressure: 0,
|
pressure: 0,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -828,6 +859,57 @@ impl Worker<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The open pad has two touchpads (Steam Deck / Steam Controller) — the gate for the
|
||||||
|
/// `TouchpadEx` surface encoding and the pad-click button re-route.
|
||||||
|
fn is_multi_touchpad(&self, which: u32) -> bool {
|
||||||
|
self.open
|
||||||
|
.as_ref()
|
||||||
|
.filter(|(id, _)| *id == which)
|
||||||
|
.map(|(_, p)| p.touchpads_count() >= 2)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SDL's Steam Deck mapping delivers the pad CLICKS as gamepad buttons — the generic
|
||||||
|
/// `touchpad` button is the LEFT pad's click and `misc2` the RIGHT's (SDL_gamepad_db.h
|
||||||
|
/// `touchpad:b17,misc2:b16`). They must NOT ride the button plane: it has no surface
|
||||||
|
/// identity, and the host maps `BTN_TOUCHPAD` to the RIGHT pad (DualSense convention) —
|
||||||
|
/// which is exactly "a left-pad click registers on the right pad". Only for the open
|
||||||
|
/// multi-touchpad pad; a DualSense's single `touchpad` button stays a wire button.
|
||||||
|
fn steam_click_surface(&self, which: u32, button: sdl3::gamepad::Button) -> Option<u8> {
|
||||||
|
use sdl3::gamepad::Button;
|
||||||
|
if !self.is_multi_touchpad(which) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match button {
|
||||||
|
Button::Touchpad => Some(1),
|
||||||
|
Button::Misc2 => Some(2),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forward a Steam-pad click on the rich plane, bound to its surface. Click events carry
|
||||||
|
/// no position, so reuse the surface's live contact point; a physical click implies
|
||||||
|
/// contact, so `touch` stays asserted while the click is down even if the touch event
|
||||||
|
/// hasn't arrived yet (event-order safety).
|
||||||
|
fn forward_click(&mut self, surface: u8, down: bool) {
|
||||||
|
let Some(c) = self.attached.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let i = (surface - 1).min(1) as usize;
|
||||||
|
self.held_clicks[i] = down;
|
||||||
|
let (x, y, touching) = self.surface_last[i];
|
||||||
|
let _ = c.send_rich_input(RichInput::TouchpadEx {
|
||||||
|
pad: 0,
|
||||||
|
surface,
|
||||||
|
finger: 0,
|
||||||
|
touch: touching || down,
|
||||||
|
click: down,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
pressure: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Publish the pad list, active pad, and pin to the UI-facing mutexes.
|
/// Publish the pad list, active pad, and pin to the UI-facing mutexes.
|
||||||
fn publish(&self) {
|
fn publish(&self) {
|
||||||
let mut list: Vec<PadInfo> = self
|
let mut list: Vec<PadInfo> = self
|
||||||
@@ -935,6 +1017,10 @@ impl Worker<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::ControllerButtonDown { which, button, .. } if active == Some(which) => {
|
Event::ControllerButtonDown { which, button, .. } if active == Some(which) => {
|
||||||
|
if let Some(surface) = self.steam_click_surface(which, button) {
|
||||||
|
self.forward_click(surface, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(c) = self.attached.clone() else {
|
let Some(c) = self.attached.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -945,6 +1031,10 @@ impl Worker<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::ControllerButtonUp { which, button, .. } if active == Some(which) => {
|
Event::ControllerButtonUp { which, button, .. } if active == Some(which) => {
|
||||||
|
if let Some(surface) = self.steam_click_surface(which, button) {
|
||||||
|
self.forward_click(surface, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(c) = self.attached.clone() else {
|
let Some(c) = self.attached.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1158,6 +1248,8 @@ fn run(
|
|||||||
last_axis: [i32::MIN; 6],
|
last_axis: [i32::MIN; 6],
|
||||||
held_buttons: Vec::new(),
|
held_buttons: Vec::new(),
|
||||||
held_touches: std::collections::HashSet::new(),
|
held_touches: std::collections::HashSet::new(),
|
||||||
|
surface_last: [(0, 0, false); 2],
|
||||||
|
held_clicks: [false; 2],
|
||||||
last_accel: [0; 3],
|
last_accel: [0; 3],
|
||||||
escape_tx: escape_tx.clone(),
|
escape_tx: escape_tx.clone(),
|
||||||
disconnect_tx: disconnect_tx.clone(),
|
disconnect_tx: disconnect_tx.clone(),
|
||||||
|
|||||||
Reference in New Issue
Block a user