fix(client-linux): absolute mouse was dropped — pack the surface size in flags
ci / web (push) Failing after 45s
ci / rust (push) Successful in 1m1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
apple / swift (push) Successful in 1m18s
ci / docs-site (push) Failing after 42s
docker / deploy-docs (push) Successful in 17s

The MouseMoveAbs wire contract packs the client coordinate-space size
as (width << 16) | height in `flags` (same as touch); injectors
normalize against it and drop the event when it is zero. The GTK
client sent flags=0, so KWin's libei path refused every motion
(`emitted=false`) — found via the first real user test from
home-worker-3.

- ui_stream: send_abs() packs the negotiated mode into flags for
  motion + click-position events.
- core input.rs: document the contract on MouseMoveAbs itself (it was
  only implied by TouchDown's doc).
- client-rs --input-test: add a MouseMoveAbs sweep so the absolute
  path stays covered — Moonlight and the Mac client only send relative
  motion, which is why this gap survived every prior live test.

Validated live against serve --native: kind=MouseMoveAbs emitted=true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 20:50:53 +00:00
parent f09def4138
commit 5f088c6f56
4 changed files with 31 additions and 12 deletions
+11 -10
View File
@@ -43,18 +43,21 @@ fn send(connector: &NativeClient, kind: InputKind, code: u32, x: i32, y: i32, fl
}); });
} }
/// Widget coordinates → video pixel coordinates through the Contain-fit letterbox. /// Forward an absolute pointer position: widget coordinates → video pixels through the
fn map_xy(widget: &impl IsA<gtk::Widget>, connector: &NativeClient, x: f64, y: f64) -> (i32, i32) { /// Contain-fit letterbox. `flags` packs the coordinate-space size (`(w << 16) | h`, the
/// same contract as touch) — the host normalizes against it before mapping into the EIS
/// region; without it the event is dropped.
fn send_abs(widget: &impl IsA<gtk::Widget>, connector: &NativeClient, x: f64, y: f64) {
let w = widget.as_ref(); let w = widget.as_ref();
let mode = connector.mode(); let mode = connector.mode();
let (ww, wh) = (w.width().max(1) as f64, w.height().max(1) as f64); let (ww, wh) = (w.width().max(1) as f64, w.height().max(1) as f64);
let (vw, vh) = (mode.width.max(1) as f64, mode.height.max(1) as f64); let (vw, vh) = (mode.width.max(1) as f64, mode.height.max(1) as f64);
let scale = (ww / vw).min(wh / vh); let scale = (ww / vw).min(wh / vh);
let (ox, oy) = ((ww - vw * scale) / 2.0, (wh - vh * scale) / 2.0); let (ox, oy) = ((ww - vw * scale) / 2.0, (wh - vh * scale) / 2.0);
( let px = (((x - ox) / scale).round()).clamp(0.0, vw - 1.0) as i32;
(((x - ox) / scale).round()).clamp(0.0, vw - 1.0) as i32, let py = (((y - oy) / scale).round()).clamp(0.0, vh - 1.0) as i32;
(((y - oy) / scale).round()).clamp(0.0, vh - 1.0) as i32, let flags = (mode.width << 16) | (mode.height & 0xffff);
) send(connector, InputKind::MouseMoveAbs, 0, px, py, flags);
} }
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
@@ -193,8 +196,7 @@ pub fn new(
let target = overlay.downgrade(); let target = overlay.downgrade();
motion.connect_motion(move |_, x, y| { motion.connect_motion(move |_, x, y| {
if let Some(w) = target.upgrade() { if let Some(w) = target.upgrade() {
let (px, py) = map_xy(&w, &conn, x, y); send_abs(&w, &conn, x, y);
send(&conn, InputKind::MouseMoveAbs, 0, px, py, 0);
} }
}); });
overlay.add_controller(motion); overlay.add_controller(motion);
@@ -206,8 +208,7 @@ pub fn new(
click.connect_pressed(move |g, _n, x, y| { click.connect_pressed(move |g, _n, x, y| {
if let Some(w) = target.upgrade() { if let Some(w) = target.upgrade() {
w.grab_focus(); w.grab_focus();
let (px, py) = map_xy(&w, &conn, x, y); send_abs(&w, &conn, x, y);
send(&conn, InputKind::MouseMoveAbs, 0, px, py, 0);
} }
if let Some(gs) = keymap::gdk_button_to_gs(g.current_button()) { if let Some(gs) = keymap::gdk_button_to_gs(g.current_button()) {
send(&conn, InputKind::MouseButtonDown, gs, 0, 0, 0); send(&conn, InputKind::MouseButtonDown, gs, 0, 0, 0);
+12
View File
@@ -528,6 +528,7 @@ async fn session(args: Args) -> Result<()> {
// low-latency input path without a real input device. // low-latency input path without a real input device.
if args.input_test { if args.input_test {
let conn2 = conn.clone(); let conn2 = conn.clone();
let (mw, mh) = (args.mode.width, args.mode.height);
tokio::spawn(async move { tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(2)).await; tokio::time::sleep(std::time::Duration::from_secs(2)).await;
tracing::info!("input-test: sending scripted datagrams for ~6s"); tracing::info!("input-test: sending scripted datagrams for ~6s");
@@ -547,6 +548,17 @@ async fn session(args: Args) -> Result<()> {
flags: 0, flags: 0,
}; };
let _ = conn2.send_datagram(mv.encode().to_vec().into()); let _ = conn2.send_datagram(mv.encode().to_vec().into());
// Absolute motion too (the GTK client's path): a diagonal sweep, with the
// coordinate-space size packed in `flags` — the contract injectors require.
let abs = InputEvent {
kind: InputKind::MouseMoveAbs,
_pad: [0; 3],
code: 0,
x: ((i * mw) / 160) as i32,
y: ((i * mh) / 160) as i32,
flags: (mw << 16) | (mh & 0xffff),
};
let _ = conn2.send_datagram(abs.encode().to_vec().into());
if i % 20 == 0 { if i % 20 == 0 {
for kind in [InputKind::KeyDown, InputKind::KeyUp] { for kind in [InputKind::KeyDown, InputKind::KeyUp] {
let key = InputEvent { let key = InputEvent {
+4 -1
View File
@@ -17,7 +17,10 @@ pub enum InputKind {
KeyUp = 1, KeyUp = 1,
/// Relative motion: `x`/`y` carry `dx`/`dy`. /// Relative motion: `x`/`y` carry `dx`/`dy`.
MouseMove = 2, MouseMove = 2,
/// Absolute motion: `x`/`y` carry pixel coordinates. /// Absolute motion: `x`/`y` carry pixel coordinates and `flags` packs the client's
/// coordinate-space size as `(width << 16) | height` (the same contract as
/// [`TouchDown`](Self::TouchDown)) — injectors normalize against it before mapping
/// into the output region and **drop the event when it is zero**.
MouseMoveAbs = 3, MouseMoveAbs = 3,
MouseButtonDown = 4, MouseButtonDown = 4,
MouseButtonUp = 5, MouseButtonUp = 5,
+4 -1
View File
@@ -273,7 +273,10 @@ enum PunktfunkInputKind
PUNKTFUNK_INPUT_KIND_KEY_UP = 1, PUNKTFUNK_INPUT_KIND_KEY_UP = 1,
// Relative motion: `x`/`y` carry `dx`/`dy`. // Relative motion: `x`/`y` carry `dx`/`dy`.
PUNKTFUNK_INPUT_KIND_MOUSE_MOVE = 2, PUNKTFUNK_INPUT_KIND_MOUSE_MOVE = 2,
// Absolute motion: `x`/`y` carry pixel coordinates. // Absolute motion: `x`/`y` carry pixel coordinates and `flags` packs the client's
// coordinate-space size as `(width << 16) | height` (the same contract as
// [`TouchDown`](Self::TouchDown)) — injectors normalize against it before mapping
// into the output region and **drop the event when it is zero**.
PUNKTFUNK_INPUT_KIND_MOUSE_MOVE_ABS = 3, PUNKTFUNK_INPUT_KIND_MOUSE_MOVE_ABS = 3,
PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_DOWN = 4, PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_DOWN = 4,
PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_UP = 5, PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_UP = 5,