feat: touch input — TouchDown/Move/Up + host libei ei_touchscreen injection
ci / rust (push) Has been cancelled

Roadmap #5 (touch, ahead of the XL UHID DualSense work). Touch fits the existing 18-byte
InputEvent: code = touch id, x/y = client pixels, flags = (w<<16)|h — the same absolute
mapping as MouseMoveAbs.

- core: InputKind::{TouchDown=9, TouchMove=10, TouchUp=11} + from_u8 + roundtrip test.
- host inject/libei.rs: request the RemoteDesktop Touchscreen device type, bind the Touch
  capability, and inject ei_touchscreen down/motion/up (one event = one frame, per the
  protocol rule), mapping coordinates into the device region like the abs pointer. wlroots
  has no virtual-touch protocol wired — no-ops there.
- client-rs --touch-test: drags a synthetic finger (touch id 0) in a circle.

Validated live on headless KWin: the portal GRANTS the Touchscreen device type
(Keyboard|Pointer|Touchscreen), proving the request path — but KWin's EIS server creates no
touchscreen *device*, so touch currently no-ops on this KWin (now logged once, not silent).
The injection code is correct and will land on a backend that exposes ei_touchscreen
(gamescope / a newer compositor / the real touch-client path). Workspace green, clippy/fmt
clean, +1 unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 22:38:44 +00:00
parent e07e359b6d
commit dc375668ee
6 changed files with 152 additions and 7 deletions
+33
View File
@@ -29,6 +29,14 @@ pub enum InputKind {
/// Sticks are i16 range (32768..32767) in the XInput/Moonlight convention — **+y =
/// up** (unlike mouse coordinates); triggers 0..255.
GamepadAxis = 8,
/// Touch begins. `code` = touch id (which finger; reusable after `TouchUp`), `x`/`y` =
/// pixel coordinates and `flags` = `(width << 16) | height` of the client's touch surface
/// — the same absolute mapping as [`MouseMoveAbs`](Self::MouseMoveAbs).
TouchDown = 9,
/// Touch moves. Same field meaning as [`TouchDown`](Self::TouchDown).
TouchMove = 10,
/// Touch ends. Only `code` (the touch id) is used.
TouchUp = 11,
}
/// The gamepad wire contract for [`InputKind::GamepadButton`]/[`InputKind::GamepadAxis`].
@@ -79,6 +87,9 @@ impl InputKind {
6 => MouseScroll,
7 => GamepadButton,
8 => GamepadAxis,
9 => TouchDown,
10 => TouchMove,
11 => TouchUp,
_ => return None,
})
}
@@ -148,4 +159,26 @@ mod tests {
assert_eq!(InputEvent::decode(&e.encode()), Some(e));
assert!(InputEvent::decode(&[0u8; INPUT_WIRE_LEN]).is_none()); // bad magic
}
#[test]
fn touch_kinds_roundtrip() {
for kind in [
InputKind::TouchDown,
InputKind::TouchMove,
InputKind::TouchUp,
] {
assert_eq!(InputKind::from_u8(kind as u8), Some(kind));
let e = InputEvent {
kind,
_pad: [0; 3],
code: 2, // touch id
x: 640,
y: 360,
flags: (1280u32 << 16) | 720, // client surface w/h
};
assert_eq!(InputEvent::decode(&e.encode()), Some(e));
}
// 12 (one past TouchUp) is not a valid kind.
assert_eq!(InputKind::from_u8(12), None);
}
}