From 5f088c6f56fd6c7f10551f93d04c78f8afac2dfe Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 12 Jun 2026 20:50:53 +0000 Subject: [PATCH] =?UTF-8?q?fix(client-linux):=20absolute=20mouse=20was=20d?= =?UTF-8?q?ropped=20=E2=80=94=20pack=20the=20surface=20size=20in=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../punktfunk-client-linux/src/ui_stream.rs | 21 ++++++++++--------- crates/punktfunk-client-rs/src/main.rs | 12 +++++++++++ crates/punktfunk-core/src/input.rs | 5 ++++- include/punktfunk_core.h | 5 ++++- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/crates/punktfunk-client-linux/src/ui_stream.rs b/crates/punktfunk-client-linux/src/ui_stream.rs index bed7108..3ac8dd9 100644 --- a/crates/punktfunk-client-linux/src/ui_stream.rs +++ b/crates/punktfunk-client-linux/src/ui_stream.rs @@ -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. -fn map_xy(widget: &impl IsA, connector: &NativeClient, x: f64, y: f64) -> (i32, i32) { +/// Forward an absolute pointer position: widget coordinates → video pixels through the +/// 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, connector: &NativeClient, x: f64, y: f64) { let w = widget.as_ref(); let mode = connector.mode(); 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 scale = (ww / vw).min(wh / vh); let (ox, oy) = ((ww - vw * scale) / 2.0, (wh - vh * scale) / 2.0); - ( - (((x - ox) / scale).round()).clamp(0.0, vw - 1.0) as i32, - (((y - oy) / scale).round()).clamp(0.0, vh - 1.0) as i32, - ) + let px = (((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; + let flags = (mode.width << 16) | (mode.height & 0xffff); + send(connector, InputKind::MouseMoveAbs, 0, px, py, flags); } #[allow(clippy::too_many_lines)] @@ -193,8 +196,7 @@ pub fn new( let target = overlay.downgrade(); motion.connect_motion(move |_, x, y| { if let Some(w) = target.upgrade() { - let (px, py) = map_xy(&w, &conn, x, y); - send(&conn, InputKind::MouseMoveAbs, 0, px, py, 0); + send_abs(&w, &conn, x, y); } }); overlay.add_controller(motion); @@ -206,8 +208,7 @@ pub fn new( click.connect_pressed(move |g, _n, x, y| { if let Some(w) = target.upgrade() { w.grab_focus(); - let (px, py) = map_xy(&w, &conn, x, y); - send(&conn, InputKind::MouseMoveAbs, 0, px, py, 0); + send_abs(&w, &conn, x, y); } if let Some(gs) = keymap::gdk_button_to_gs(g.current_button()) { send(&conn, InputKind::MouseButtonDown, gs, 0, 0, 0); diff --git a/crates/punktfunk-client-rs/src/main.rs b/crates/punktfunk-client-rs/src/main.rs index c795eb5..f15c00c 100644 --- a/crates/punktfunk-client-rs/src/main.rs +++ b/crates/punktfunk-client-rs/src/main.rs @@ -528,6 +528,7 @@ async fn session(args: Args) -> Result<()> { // low-latency input path without a real input device. if args.input_test { let conn2 = conn.clone(); + let (mw, mh) = (args.mode.width, args.mode.height); tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(2)).await; tracing::info!("input-test: sending scripted datagrams for ~6s"); @@ -547,6 +548,17 @@ async fn session(args: Args) -> Result<()> { flags: 0, }; 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 { for kind in [InputKind::KeyDown, InputKind::KeyUp] { let key = InputEvent { diff --git a/crates/punktfunk-core/src/input.rs b/crates/punktfunk-core/src/input.rs index 8d010b1..c95594a 100644 --- a/crates/punktfunk-core/src/input.rs +++ b/crates/punktfunk-core/src/input.rs @@ -17,7 +17,10 @@ pub enum InputKind { KeyUp = 1, /// Relative motion: `x`/`y` carry `dx`/`dy`. 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, MouseButtonDown = 4, MouseButtonUp = 5, diff --git a/include/punktfunk_core.h b/include/punktfunk_core.h index 01c7300..54e773a 100644 --- a/include/punktfunk_core.h +++ b/include/punktfunk_core.h @@ -273,7 +273,10 @@ enum PunktfunkInputKind PUNKTFUNK_INPUT_KIND_KEY_UP = 1, // Relative motion: `x`/`y` carry `dx`/`dy`. 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_BUTTON_DOWN = 4, PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_UP = 5,