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
+50 -1
View File
@@ -9,7 +9,8 @@
//! `--input-test` exercises the input plane: scripted mouse/keyboard datagrams during the
//! stream (watch them land in the host session, e.g. xev inside gamescope). `--mic-test`
//! exercises the mic uplink: a synthetic 440 Hz tone streamed as Opus (0xCB) → the host's
//! virtual microphone source (record it host-side to hear the tone).
//! virtual microphone source (record it host-side to hear the tone). `--touch-test` drags a
//! synthetic finger in a circle → host libei `ei_touchscreen` injection.
//!
//! `--pin <64-hex>` pins the host's certificate fingerprint (the host logs it at startup);
//! without it the client trusts on first use and prints the observed fingerprint to pin.
@@ -41,6 +42,8 @@ struct Args {
input_test: bool,
/// `--mic-test` — stream a synthetic 440 Hz tone as the mic uplink (proves the mic path).
mic_test: bool,
/// `--touch-test` — drag a synthetic finger in a circle (proves the touch path).
touch_test: bool,
pin: Option<[u8; 32]>,
/// `--remode WxHxFPS:SECS` — request this mode SECS seconds into the stream.
remode: Option<(Mode, u32)>,
@@ -142,6 +145,7 @@ fn parse_args() -> Args {
out: get("--out").map(String::from),
input_test: argv.iter().any(|a| a == "--input-test"),
mic_test: argv.iter().any(|a| a == "--mic-test"),
touch_test: argv.iter().any(|a| a == "--touch-test"),
pin,
remode,
pair: get("--pair").map(String::from),
@@ -396,6 +400,51 @@ async fn session(args: Args) -> Result<()> {
});
}
// Touch plane: drag a synthetic finger (touch id 0) in a circle on the client surface, so
// the host injects it via libei ei_touchscreen — proves the touch path end to end. `flags`
// packs the surface w/h; x/y are pixels (the host maps them into the device region).
if args.touch_test {
let conn2 = conn.clone();
let (w, h) = (args.mode.width, args.mode.height);
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let flags = (w << 16) | (h & 0xffff);
let (cx, cy, r) = (w as f32 / 2.0, h as f32 / 2.0, h as f32 / 4.0);
let touch = |kind, x: f32, y: f32| InputEvent {
kind,
_pad: [0; 3],
code: 0, // touch id 0
x: x as i32,
y: y as i32,
flags,
};
tracing::info!("touch-test: dragging a finger in a circle for ~6s");
for loop_i in 0..3u32 {
let _ = conn2.send_datagram(
touch(InputKind::TouchDown, cx + r, cy)
.encode()
.to_vec()
.into(),
);
for i in 0..60u32 {
let a = std::f32::consts::TAU * i as f32 / 60.0;
let mv = touch(InputKind::TouchMove, cx + r * a.cos(), cy + r * a.sin());
let _ = conn2.send_datagram(mv.encode().to_vec().into());
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
}
let _ = conn2.send_datagram(
touch(InputKind::TouchUp, cx + r, cy)
.encode()
.to_vec()
.into(),
);
let _ = loop_i;
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
tracing::info!("touch-test: done");
});
}
// Closed-flag for the blocking receive loop.
let closed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
{