// client/src/input/mouse.rs use evdev::{AbsoluteAxisCode, Device, EventType, InputEvent, KeyCode, RelativeAxisCode}; use std::time::{Duration, Instant}; use tokio::sync::broadcast::{self, Sender}; use tracing::{debug, error, trace, warn}; use lesavka_common::lesavka::MouseReport; const SEND_INTERVAL: Duration = Duration::from_millis(1); 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 evts: Vec = match self.dev.fetch_events() { Ok(it) => it.collect(), Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return, Err(e) => { if self.dev_mode { error!("🖱️❌ mouse read err: {e}"); } return; } }; if self.dev_mode && !evts.is_empty() { trace!( "🖱️ {} evts from {}", evts.len(), self.dev.name().unwrap_or("?") ); } for e in evts { match e.event_type() { EventType::KEY => match e.code() { c if c == KeyCode::BTN_LEFT.0 => self.set_btn(0, e.value()), c if c == KeyCode::BTN_RIGHT.0 => self.set_btn(1, e.value()), c if c == KeyCode::BTN_MIDDLE.0 => self.set_btn(2, e.value()), c if c == KeyCode::BTN_TOUCH.0 => { self.touch_guarded = true; self.touch_active = e.value() != 0; if !self.touch_active { self.last_abs_x = None; self.last_abs_y = None; } self.set_btn(0, e.value()); } _ => {} }, EventType::RELATIVE => match e.code() { c if c == RelativeAxisCode::REL_X.0 => { self.dx = self.dx.saturating_add(e.value().clamp(-127, 127) as i8) } c if c == RelativeAxisCode::REL_Y.0 => { self.dy = self.dy.saturating_add(e.value().clamp(-127, 127) as i8) } c if c == RelativeAxisCode::REL_WHEEL.0 => { self.wheel = self.wheel.saturating_add(e.value().clamp(-1, 1) as i8) } _ => {} }, EventType::ABSOLUTE => match e.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(e.value()); continue; } if let Some(prev) = self.last_abs_x { if !self.has_touch_state { let delta = (e.value() - prev).abs(); if delta > self.abs_jump_x { self.last_abs_x = Some(e.value()); continue; } } let delta = (e.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(e.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(e.value()); continue; } if let Some(prev) = self.last_abs_y { if !self.has_touch_state { let delta = (e.value() - prev).abs(); if delta > self.abs_jump_y { self.last_abs_y = Some(e.value()); continue; } } let delta = (e.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(e.value()); } c if c == AbsoluteAxisCode::ABS_MT_TRACKING_ID.0 => { if e.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; } } _ => {} }, EventType::SYNCHRONIZATION => self.flush(), _ => {} } } } 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(), }); } } fn flush(&mut self) { if self.buttons == self.last_buttons && Instant::now() < self.next_send { return; } self.next_send = Instant::now() + SEND_INTERVAL; let pkt = [ self.buttons, self.dx.clamp(-127, 127) as u8, self.dy.clamp(-127, 127) as u8, self.wheel as u8, ]; if !self.sending_disabled { if let Err(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); } } self.dx = 0; self.dy = 0; self.wheel = 0; self.last_buttons = self.buttons; } #[inline] fn set_btn(&mut self, bit: u8, val: i32) { if val != 0 { self.buttons |= 1 << bit } else { self.buttons &= !(1 << bit) } } 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.iter().any(|c| *c == 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 } } impl Drop for MouseAggregator { fn drop(&mut self) { let _ = self.dev.ungrab(); let _ = self.tx.send(MouseReport { data: [0; 8].into(), }); } }