fix: ensure clean Wayland input state on exit to prevent stuck buttons

This commit is contained in:
swaits committed 2026-04-05 13:02:39 -06:00
1 parent 7f7d3e59d7
commit 0287589590
3 files changed
+56 -34

No files matched your search

Generated
+1 -1
View File
@@ -196,7 +196,7 @@ dependencies = [
[[package]] [[package]]
name = "mousefree" name = "mousefree"
version = "1.0.1" version = "1.0.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"fontdue", "fontdue",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mousefree" name = "mousefree"
version = "1.0.1" version = "1.0.2"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Keyboard-driven mouse navigation for Wayland" description = "Keyboard-driven mouse navigation for Wayland"
+54 -32
View File
@@ -41,12 +41,16 @@ const MOD_SHIFT: u32 = 1;
const MOD_CTRL: u32 = 4; const MOD_CTRL: u32 = 4;
const MOD_ALT: u32 = 8; const MOD_ALT: u32 = 8;
// Drag animation timing (~150ms total). // Drag animation timing.
const DRAG_SETTLE_MS: u64 = 25; const DRAG_SETTLE_MS: u64 = 25;
const DRAG_PRESS_SETTLE_MS: u64 = 30; const DRAG_PRESS_SETTLE_MS: u64 = 60;
const DRAG_INTERP_STEPS: u32 = 12; const DRAG_INTERP_STEPS: u32 = 12;
const DRAG_STEP_DELAY_MS: u64 = 5; const DRAG_STEP_DELAY_MS: u64 = 2;
const DRAG_RELEASE_SETTLE_MS: u64 = 35; const DRAG_RELEASE_SETTLE_MS: u64 = 60;
// Click timing — mirrors the drag pattern for reliable event delivery.
const CLICK_SETTLE_MS: u64 = 25;
const CLICK_INTER_MS: u64 = 30;
// Maximum number of event loop iterations to wait for a key release before // Maximum number of event loop iterations to wait for a key release before
// giving up. Prevents an infinite hang if the compositor drops the event. // giving up. Prevents an infinite hang if the compositor drops the event.
@@ -318,27 +322,36 @@ impl WaylandBackend {
sleep(Duration::from_millis(DRAG_PRESS_SETTLE_MS)); sleep(Duration::from_millis(DRAG_PRESS_SETTLE_MS));
// 3. Animate the pointer from start to end so apps see continuous // 3. Animate the pointer from start to end so apps see continuous
// motion and cross their drag threshold. // motion and cross their drag threshold. Errors are captured so
let step_delay = Duration::from_millis(DRAG_STEP_DELAY_MS); // the button release in step 4 always executes.
for i in 1..=DRAG_INTERP_STEPS { let drag_result = (|| -> Result<()> {
let t = i as f64 / DRAG_INTERP_STEPS as f64; let step_delay = Duration::from_millis(DRAG_STEP_DELAY_MS);
let x = x1 as f64 + (x2 as f64 - x1 as f64) * t; for i in 1..=DRAG_INTERP_STEPS {
let y = y1 as f64 + (y2 as f64 - y1 as f64) * t; let t = i as f64 / DRAG_INTERP_STEPS as f64;
let cx = (x.clamp(0.0, sw.saturating_sub(1) as f64)) as u32; let x = x1 as f64 + (x2 as f64 - x1 as f64) * t;
let cy = (y.clamp(0.0, sh.saturating_sub(1) as f64)) as u32; let y = y1 as f64 + (y2 as f64 - y1 as f64) * t;
self.send_motion(cx, cy, sw, sh)?; let cx = (x.clamp(0.0, sw.saturating_sub(1) as f64)) as u32;
self.state let cy = (y.clamp(0.0, sh.saturating_sub(1) as f64)) as u32;
.conn self.send_motion(cx, cy, sw, sh)?;
.flush() self.state
.context("flush during drag motion")?; .conn
sleep(step_delay); .flush()
} .context("flush during drag motion")?;
self.roundtrip("motion to drag end")?; sleep(step_delay);
sleep(Duration::from_millis(DRAG_RELEASE_SETTLE_MS)); }
// Land exactly on the drop target and confirm the compositor
// has processed the final position before we release.
self.send_motion(x2, y2, sw, sh)?;
self.roundtrip("motion to drag end")?;
sleep(Duration::from_millis(DRAG_RELEASE_SETTLE_MS));
Ok(())
})();
// 4. Release the button at the destination. // 4. Always release the button, even if the animation failed.
self.send_button(BTN_LEFT, ButtonState::Released)?; self.send_button(BTN_LEFT, ButtonState::Released)?;
self.roundtrip("release at drag end") self.roundtrip("release at drag end")?;
drag_result
} }
// -- Scroll ------------------------------------------------------------- // -- Scroll -------------------------------------------------------------
@@ -392,6 +405,11 @@ impl WaylandBackend {
} }
self.state.conn.flush().context("flush vp destroy")?; self.state.conn.flush().context("flush vp destroy")?;
// Ensure the compositor has fully processed the VP destruction
// (which implicitly releases all held buttons) before tearing
// down the surface.
self.roundtrip("vp destroy")?;
self.teardown_surface() self.teardown_surface()
} }
@@ -416,21 +434,25 @@ impl WaylandBackend {
/// virtual pointer to `(x, y)`, then sends `count` press/release pairs /// virtual pointer to `(x, y)`, then sends `count` press/release pairs
/// on `button`. /// on `button`.
fn click_at(&mut self, x: u32, y: u32, button: u32, count: u32) -> Result<()> { fn click_at(&mut self, x: u32, y: u32, button: u32, count: u32) -> Result<()> {
use std::thread::sleep;
use std::time::Duration;
self.teardown_surface()?; self.teardown_surface()?;
// Move to the target and let focus settle on the underlying window.
self.send_motion(x, y, self.state.screen_w, self.state.screen_h)?; self.send_motion(x, y, self.state.screen_w, self.state.screen_h)?;
self.roundtrip("motion before click")?; self.roundtrip("motion before click")?;
sleep(Duration::from_millis(CLICK_SETTLE_MS));
if let Some(vp) = &self.state.virtual_pointer { for _ in 0..count {
for _ in 0..count { self.send_button(button, ButtonState::Pressed)?;
let ts = timestamp(); self.roundtrip("click press")?;
vp.button(ts, button, ButtonState::Pressed); sleep(Duration::from_millis(CLICK_INTER_MS));
vp.frame();
vp.button(ts, button, ButtonState::Released); self.send_button(button, ButtonState::Released)?;
vp.frame(); self.roundtrip("click release")?;
}
} }
self.roundtrip("click") Ok(())
} }
fn scroll(&mut self, axis: Axis, value: f64, discrete: i32) -> Result<()> { fn scroll(&mut self, axis: Axis, value: f64, discrete: i32) -> Result<()> {