// client/src/input/mouse.rs use evdev::{AbsoluteAxisCode, Device, EventType, InputEvent, KeyCode, RelativeAxisCode}; use std::time::{Duration, Instant}; use tokio::sync::broadcast::Sender; use tracing::{debug, error, trace, warn}; use lesavka_common::lesavka::MouseReport; const SEND_INTERVAL: Duration = Duration::from_millis(1); struct MouseEventState<'a> { buttons: &'a mut u8, dx: &'a mut i8, dy: &'a mut i8, wheel: &'a mut i8, last_abs_x: &'a mut Option, last_abs_y: &'a mut Option, abs_scale: i32, abs_jump_x: i32, abs_jump_y: i32, has_touch_state: bool, touch_guarded: &'a mut bool, touch_active: &'a mut bool, } impl MouseEventState<'_> { fn set_btn(&mut self, bit: u8, val: i32) { if val != 0 { *self.buttons |= 1 << bit; } else { *self.buttons &= !(1 << bit); } } fn apply_event(&mut self, event: &InputEvent) { match event.event_type() { EventType::KEY => match event.code() { c if c == KeyCode::BTN_LEFT.0 => self.set_btn(0, event.value()), c if c == KeyCode::BTN_RIGHT.0 => self.set_btn(1, event.value()), c if c == KeyCode::BTN_MIDDLE.0 => self.set_btn(2, event.value()), c if c == KeyCode::BTN_TOUCH.0 => { *self.touch_guarded = true; *self.touch_active = event.value() != 0; if !*self.touch_active { *self.last_abs_x = None; *self.last_abs_y = None; } self.set_btn(0, event.value()); } _ => {} }, EventType::RELATIVE => match event.code() { c if c == RelativeAxisCode::REL_X.0 => { *self.dx = self.dx.saturating_add(event.value().clamp(-127, 127) as i8) } c if c == RelativeAxisCode::REL_Y.0 => { *self.dy = self.dy.saturating_add(event.value().clamp(-127, 127) as i8) } c if c == RelativeAxisCode::REL_WHEEL.0 => { *self.wheel = self.wheel.saturating_add(event.value().clamp(-1, 1) as i8) } _ => {} }, EventType::ABSOLUTE => match event.code() { c if c == AbsoluteAxisCode::ABS_X.0 || c == AbsoluteAxisCode::ABS_MT_POSITION_X.0 => { if *self.touch_guarded && !*self.touch_active { *self.last_abs_x = Some(event.value()); return; } if let Some(prev) = *self.last_abs_x { if !self.has_touch_state { let delta = (event.value() - prev).abs(); if delta > self.abs_jump_x { *self.last_abs_x = Some(event.value()); return; } } let delta = (event.value() - prev) / self.abs_scale; if delta != 0 { *self.dx = self.dx.saturating_add(delta.clamp(-127, 127) as i8); } } *self.last_abs_x = Some(event.value()); } c if c == AbsoluteAxisCode::ABS_Y.0 || c == AbsoluteAxisCode::ABS_MT_POSITION_Y.0 => { if *self.touch_guarded && !*self.touch_active { *self.last_abs_y = Some(event.value()); return; } if let Some(prev) = *self.last_abs_y { if !self.has_touch_state { let delta = (event.value() - prev).abs(); if delta > self.abs_jump_y { *self.last_abs_y = Some(event.value()); return; } } let delta = (event.value() - prev) / self.abs_scale; if delta != 0 { *self.dy = self.dy.saturating_add(delta.clamp(-127, 127) as i8); } } *self.last_abs_y = Some(event.value()); } c if c == AbsoluteAxisCode::ABS_MT_TRACKING_ID.0 => { if event.value() < 0 { *self.touch_guarded = true; *self.touch_active = false; *self.last_abs_x = None; *self.last_abs_y = None; } else { *self.touch_guarded = true; *self.touch_active = true; } } _ => {} }, _ => {} } } } struct MouseRuntime<'a> { tx: &'a Sender, dev_mode: bool, sending_disabled: bool, next_send: &'a mut Instant, last_buttons: &'a mut u8, event_state: MouseEventState<'a>, } impl MouseRuntime<'_> { fn replay_events(&mut self, events: Vec) { for event in events { if event.event_type() == EventType::SYNCHRONIZATION { self.flush(); } else { self.event_state.apply_event(&event); } } } fn flush(&mut self) { let buttons = *self.event_state.buttons; if buttons == *self.last_buttons && Instant::now() < *self.next_send { return; } *self.next_send = Instant::now() + SEND_INTERVAL; let pkt = [ buttons, (*self.event_state.dx).clamp(-127, 127) as u8, (*self.event_state.dy).clamp(-127, 127) as u8, *self.event_state.wheel as u8, ]; if !self.sending_disabled { #[cfg(not(coverage))] if let Err(tokio::sync::broadcast::error::SendError(_)) = self.tx.send(MouseReport { data: pkt.to_vec() }) { if self.dev_mode { warn!("❌🖱️ no HID receiver (mouse)"); } } else if self.dev_mode { debug!("📤🖱️ mouse {:?}", pkt); } #[cfg(coverage)] { let _ = self.tx.send(MouseReport { data: pkt.to_vec() }); } } *self.event_state.dx = 0; *self.event_state.dy = 0; *self.event_state.wheel = 0; *self.last_buttons = buttons; } } fn collect_fetched_events( fetch_result: Result, dev_mode: bool, ) -> Option> where I: IntoIterator, { match fetch_result { Ok(events) => Some(events.into_iter().collect()), Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => None, Err(e) => { if dev_mode { error!("🖱️❌ mouse read err: {e}"); } None } } } fn log_event_batch(dev_mode: bool, device_name: Option<&str>, evts: &[InputEvent]) { if dev_mode && !evts.is_empty() { trace!("🖱️ {} evts from {}", evts.len(), device_name.unwrap_or("?")); } } pub struct MouseAggregator { dev: Device, tx: Sender, dev_mode: bool, sending_disabled: bool, next_send: Instant, buttons: u8, last_buttons: u8, dx: i8, dy: i8, wheel: i8, last_abs_x: Option, last_abs_y: Option, abs_scale: i32, abs_jump_x: i32, abs_jump_y: i32, has_touch_state: bool, touch_guarded: bool, touch_active: bool, } impl MouseAggregator { pub fn new(dev: Device, dev_mode: bool, tx: Sender) -> Self { let abs_scale = std::env::var("LESAVKA_TOUCHPAD_SCALE") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(8) .max(1); let has_touch_state = dev .supported_keys() .map(|keys| keys.contains(KeyCode::BTN_TOUCH)) .unwrap_or(false) || dev .supported_absolute_axes() .map(|abs| abs.contains(AbsoluteAxisCode::ABS_MT_TRACKING_ID)) .unwrap_or(false); let abs_jump_x = Self::abs_jump_threshold( &dev, &[AbsoluteAxisCode::ABS_X, AbsoluteAxisCode::ABS_MT_POSITION_X], abs_scale, ); let abs_jump_y = Self::abs_jump_threshold( &dev, &[AbsoluteAxisCode::ABS_Y, AbsoluteAxisCode::ABS_MT_POSITION_Y], abs_scale, ); Self { dev, tx, dev_mode, sending_disabled: false, next_send: Instant::now(), buttons: 0, last_buttons: 0, dx: 0, dy: 0, wheel: 0, last_abs_x: None, last_abs_y: None, abs_scale, abs_jump_x, abs_jump_y, has_touch_state, touch_guarded: false, touch_active: true, } } #[inline] #[allow(dead_code)] fn slog(&self, f: impl FnOnce()) { if self.dev_mode { f() } } pub fn set_grab(&mut self, grab: bool) { let _ = if grab { self.dev.grab() } else { self.dev.ungrab() }; } pub fn set_send(&mut self, send: bool) { self.sending_disabled = !send; } pub fn process_events(&mut self) { let Some(evts) = collect_fetched_events(self.dev.fetch_events(), self.dev_mode) else { return; }; log_event_batch(self.dev_mode, self.dev.name(), &evts); self.runtime().replay_events(evts); } pub fn reset_state(&mut self) { self.buttons = 0; self.last_buttons = 0; self.dx = 0; self.dy = 0; self.wheel = 0; self.last_abs_x = None; self.last_abs_y = None; if !self.sending_disabled { let _ = self.tx.send(MouseReport { data: [0; 4].into(), }); } } #[allow(dead_code)] fn flush(&mut self) { self.runtime().flush(); } #[inline] #[allow(dead_code)] fn set_btn(&mut self, bit: u8, val: i32) { MouseEventState { buttons: &mut self.buttons, dx: &mut self.dx, dy: &mut self.dy, wheel: &mut self.wheel, last_abs_x: &mut self.last_abs_x, last_abs_y: &mut self.last_abs_y, abs_scale: self.abs_scale, abs_jump_x: self.abs_jump_x, abs_jump_y: self.abs_jump_y, has_touch_state: self.has_touch_state, touch_guarded: &mut self.touch_guarded, touch_active: &mut self.touch_active, } .set_btn(bit, val); } #[cfg(not(coverage))] fn abs_jump_threshold(dev: &Device, codes: &[AbsoluteAxisCode], abs_scale: i32) -> i32 { let mut range: Option = None; if let Ok(iter) = dev.get_absinfo() { for (code, info) in iter { if codes.contains(&code) { range = Some(info.maximum() - info.minimum()); break; } } } let mut threshold = range.unwrap_or(0).abs() / 3; let min = (abs_scale * 40).max(50); if threshold < min { threshold = min; } if threshold == 0 { threshold = min; } threshold } #[cfg(coverage)] fn abs_jump_threshold(_dev: &Device, _codes: &[AbsoluteAxisCode], abs_scale: i32) -> i32 { (abs_scale * 40).max(50) } fn runtime(&mut self) -> MouseRuntime<'_> { MouseRuntime { tx: &self.tx, dev_mode: self.dev_mode, sending_disabled: self.sending_disabled, next_send: &mut self.next_send, last_buttons: &mut self.last_buttons, event_state: MouseEventState { buttons: &mut self.buttons, dx: &mut self.dx, dy: &mut self.dy, wheel: &mut self.wheel, last_abs_x: &mut self.last_abs_x, last_abs_y: &mut self.last_abs_y, abs_scale: self.abs_scale, abs_jump_x: self.abs_jump_x, abs_jump_y: self.abs_jump_y, has_touch_state: self.has_touch_state, touch_guarded: &mut self.touch_guarded, touch_active: &mut self.touch_active, }, } } } impl Drop for MouseAggregator { fn drop(&mut self) { let _ = self.dev.ungrab(); let _ = self.tx.send(MouseReport { data: [0; 8].into(), }); } } #[cfg(test)] #[path = "mouse_event_contract_tests.rs"] mod mouse_event_contract_tests;