ui(hygiene): refresh webcam placeholder and mouse coverage
This commit is contained in:
parent
069d17980f
commit
e0b2b70b29
Binary file not shown.
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 662 KiB |
@ -3,13 +3,212 @@
|
|||||||
use evdev::{AbsoluteAxisCode, Device, EventType, InputEvent, KeyCode, RelativeAxisCode};
|
use evdev::{AbsoluteAxisCode, Device, EventType, InputEvent, KeyCode, RelativeAxisCode};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::broadcast::Sender;
|
use tokio::sync::broadcast::Sender;
|
||||||
#[cfg(not(coverage))]
|
|
||||||
use tracing::{debug, error, trace, warn};
|
use tracing::{debug, error, trace, warn};
|
||||||
|
|
||||||
use lesavka_common::lesavka::MouseReport;
|
use lesavka_common::lesavka::MouseReport;
|
||||||
|
|
||||||
const SEND_INTERVAL: Duration = Duration::from_millis(1);
|
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<i32>,
|
||||||
|
last_abs_y: &'a mut Option<i32>,
|
||||||
|
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<MouseReport>,
|
||||||
|
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<InputEvent>) {
|
||||||
|
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<I>(
|
||||||
|
fetch_result: Result<I, std::io::Error>,
|
||||||
|
dev_mode: bool,
|
||||||
|
) -> Option<Vec<InputEvent>>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = InputEvent>,
|
||||||
|
{
|
||||||
|
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 {
|
pub struct MouseAggregator {
|
||||||
dev: Device,
|
dev: Device,
|
||||||
tx: Sender<MouseReport>,
|
tx: Sender<MouseReport>,
|
||||||
@ -98,124 +297,12 @@ impl MouseAggregator {
|
|||||||
self.sending_disabled = !send;
|
self.sending_disabled = !send;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
|
||||||
pub fn process_events(&mut self) {
|
pub fn process_events(&mut self) {
|
||||||
let evts: Vec<InputEvent> = match self.dev.fetch_events() {
|
let Some(evts) = collect_fetched_events(self.dev.fetch_events(), self.dev_mode) else {
|
||||||
Ok(it) => it.collect(),
|
return;
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return,
|
|
||||||
Err(e) => {
|
|
||||||
if self.dev_mode {
|
|
||||||
error!("🖱️❌ mouse read err: {e}");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
log_event_batch(self.dev_mode, self.dev.name(), &evts);
|
||||||
if self.dev_mode && !evts.is_empty() {
|
self.runtime().replay_events(evts);
|
||||||
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(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(coverage)]
|
|
||||||
pub fn process_events(&mut self) {
|
|
||||||
let _ = self.dev.fetch_events();
|
|
||||||
self.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_state(&mut self) {
|
pub fn reset_state(&mut self) {
|
||||||
@ -234,49 +321,27 @@ impl MouseAggregator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) {
|
fn flush(&mut self) {
|
||||||
if self.buttons == self.last_buttons && Instant::now() < self.next_send {
|
self.runtime().flush();
|
||||||
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 {
|
|
||||||
#[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.dx = 0;
|
|
||||||
self.dy = 0;
|
|
||||||
self.wheel = 0;
|
|
||||||
self.last_buttons = self.buttons;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
#[allow(dead_code)]
|
||||||
fn set_btn(&mut self, bit: u8, val: i32) {
|
fn set_btn(&mut self, bit: u8, val: i32) {
|
||||||
if val != 0 {
|
MouseEventState {
|
||||||
self.buttons |= 1 << bit
|
buttons: &mut self.buttons,
|
||||||
} else {
|
dx: &mut self.dx,
|
||||||
self.buttons &= !(1 << bit)
|
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))]
|
#[cfg(not(coverage))]
|
||||||
@ -305,6 +370,30 @@ impl MouseAggregator {
|
|||||||
fn abs_jump_threshold(_dev: &Device, _codes: &[AbsoluteAxisCode], abs_scale: i32) -> i32 {
|
fn abs_jump_threshold(_dev: &Device, _codes: &[AbsoluteAxisCode], abs_scale: i32) -> i32 {
|
||||||
(abs_scale * 40).max(50)
|
(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 {
|
impl Drop for MouseAggregator {
|
||||||
@ -315,3 +404,7 @@ impl Drop for MouseAggregator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "mouse_event_contract_tests.rs"]
|
||||||
|
mod mouse_event_contract_tests;
|
||||||
|
|||||||
439
client/src/input/mouse_event_contract_tests.rs
Normal file
439
client/src/input/mouse_event_contract_tests.rs
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
//! Unit coverage for mouse event state transitions.
|
||||||
|
//!
|
||||||
|
//! Scope: exercise the private event decoder used by `MouseAggregator`.
|
||||||
|
//! Targets: `client/src/input/mouse.rs`.
|
||||||
|
//! Why: the quality gate tracks the real crate source, so the core event
|
||||||
|
//! handling needs deterministic coverage without depending on `/dev/uinput`.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use serial_test::serial;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn open_virtual_node(vdev: &mut evdev::uinput::VirtualDevice) -> Option<PathBuf> {
|
||||||
|
for _ in 0..40 {
|
||||||
|
if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
|
||||||
|
if let Some(Ok(path)) = nodes.next() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_virtual_device(vdev: &mut evdev::uinput::VirtualDevice) -> Option<evdev::Device> {
|
||||||
|
let node = open_virtual_node(vdev)?;
|
||||||
|
let dev = evdev::Device::open(node).ok()?;
|
||||||
|
dev.set_nonblocking(true).ok()?;
|
||||||
|
Some(dev)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_relative_mouse(name: &str) -> Option<(evdev::uinput::VirtualDevice, evdev::Device)> {
|
||||||
|
let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
|
||||||
|
keys.insert(evdev::KeyCode::BTN_LEFT);
|
||||||
|
keys.insert(evdev::KeyCode::BTN_RIGHT);
|
||||||
|
keys.insert(evdev::KeyCode::BTN_MIDDLE);
|
||||||
|
|
||||||
|
let mut rel = evdev::AttributeSet::<evdev::RelativeAxisCode>::new();
|
||||||
|
rel.insert(evdev::RelativeAxisCode::REL_X);
|
||||||
|
rel.insert(evdev::RelativeAxisCode::REL_Y);
|
||||||
|
rel.insert(evdev::RelativeAxisCode::REL_WHEEL);
|
||||||
|
|
||||||
|
let mut vdev = evdev::uinput::VirtualDevice::builder()
|
||||||
|
.ok()?
|
||||||
|
.name(name)
|
||||||
|
.with_keys(&keys)
|
||||||
|
.ok()?
|
||||||
|
.with_relative_axes(&rel)
|
||||||
|
.ok()?
|
||||||
|
.build()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let dev = open_virtual_device(&mut vdev)?;
|
||||||
|
Some((vdev, dev))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StateHarness {
|
||||||
|
buttons: u8,
|
||||||
|
dx: i8,
|
||||||
|
dy: i8,
|
||||||
|
wheel: i8,
|
||||||
|
last_abs_x: Option<i32>,
|
||||||
|
last_abs_y: Option<i32>,
|
||||||
|
abs_scale: i32,
|
||||||
|
abs_jump_x: i32,
|
||||||
|
abs_jump_y: i32,
|
||||||
|
has_touch_state: bool,
|
||||||
|
touch_guarded: bool,
|
||||||
|
touch_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateHarness {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
buttons: 0,
|
||||||
|
dx: 0,
|
||||||
|
dy: 0,
|
||||||
|
wheel: 0,
|
||||||
|
last_abs_x: None,
|
||||||
|
last_abs_y: None,
|
||||||
|
abs_scale: 8,
|
||||||
|
abs_jump_x: 120,
|
||||||
|
abs_jump_y: 120,
|
||||||
|
has_touch_state: false,
|
||||||
|
touch_guarded: false,
|
||||||
|
touch_active: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_touch_state() -> Self {
|
||||||
|
Self {
|
||||||
|
has_touch_state: true,
|
||||||
|
..Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&mut self) -> MouseEventState<'_> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_events_update_button_bits_and_touch_release_clears_origins() {
|
||||||
|
let mut harness = StateHarness::with_touch_state();
|
||||||
|
harness.last_abs_x = Some(100);
|
||||||
|
harness.last_abs_y = Some(120);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = harness.state();
|
||||||
|
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 1));
|
||||||
|
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_RIGHT.0, 1));
|
||||||
|
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_MIDDLE.0, 1));
|
||||||
|
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_TOUCH.0, 1));
|
||||||
|
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::KEY_A.0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(harness.buttons & 0b111, 0b111);
|
||||||
|
assert!(harness.touch_guarded);
|
||||||
|
assert!(harness.touch_active);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = harness.state();
|
||||||
|
state.apply_event(&InputEvent::new(EventType::KEY.0, KeyCode::BTN_TOUCH.0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(!harness.touch_active);
|
||||||
|
assert!(harness.last_abs_x.is_none());
|
||||||
|
assert!(harness.last_abs_y.is_none());
|
||||||
|
assert_eq!(harness.buttons & 0b001, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relative_events_accumulate_motion_and_ignore_unknown_codes() {
|
||||||
|
let mut harness = StateHarness::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = harness.state();
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::RELATIVE.0,
|
||||||
|
RelativeAxisCode::REL_X.0,
|
||||||
|
11,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::RELATIVE.0,
|
||||||
|
RelativeAxisCode::REL_Y.0,
|
||||||
|
-7,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::RELATIVE.0,
|
||||||
|
RelativeAxisCode::REL_WHEEL.0,
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::RELATIVE.0,
|
||||||
|
RelativeAxisCode::REL_RX.0,
|
||||||
|
42,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(harness.dx, 11);
|
||||||
|
assert_eq!(harness.dy, -7);
|
||||||
|
assert_eq!(harness.wheel, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn absolute_events_apply_scaled_delta_and_ignore_large_jumps_without_touch_state() {
|
||||||
|
let mut harness = StateHarness::new();
|
||||||
|
harness.last_abs_x = Some(100);
|
||||||
|
harness.last_abs_y = Some(100);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = harness.state();
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_X.0,
|
||||||
|
140,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_Y.0,
|
||||||
|
180,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(harness.dx, 5);
|
||||||
|
assert_eq!(harness.dy, 10);
|
||||||
|
assert_eq!(harness.last_abs_x, Some(140));
|
||||||
|
assert_eq!(harness.last_abs_y, Some(180));
|
||||||
|
|
||||||
|
harness.dx = 0;
|
||||||
|
harness.dy = 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = harness.state();
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_X.0,
|
||||||
|
900,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_Y.0,
|
||||||
|
900,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(harness.dx, 0);
|
||||||
|
assert_eq!(harness.dy, 0);
|
||||||
|
assert_eq!(harness.last_abs_x, Some(900));
|
||||||
|
assert_eq!(harness.last_abs_y, Some(900));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_absolute_samples_only_seed_origin_without_motion() {
|
||||||
|
let mut harness = StateHarness::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = harness.state();
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_X.0,
|
||||||
|
320,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_Y.0,
|
||||||
|
640,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(harness.dx, 0);
|
||||||
|
assert_eq!(harness.dy, 0);
|
||||||
|
assert_eq!(harness.last_abs_x, Some(320));
|
||||||
|
assert_eq!(harness.last_abs_y, Some(640));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn touch_guarded_absolute_updates_only_refresh_origins_until_touch_returns() {
|
||||||
|
let mut harness = StateHarness::with_touch_state();
|
||||||
|
harness.touch_guarded = true;
|
||||||
|
harness.touch_active = false;
|
||||||
|
harness.last_abs_x = Some(10);
|
||||||
|
harness.last_abs_y = Some(20);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = harness.state();
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_MT_POSITION_X.0,
|
||||||
|
50,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_MT_POSITION_Y.0,
|
||||||
|
70,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_MT_TRACKING_ID.0,
|
||||||
|
3,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_MT_POSITION_X.0,
|
||||||
|
82,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_MT_POSITION_Y.0,
|
||||||
|
102,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_PRESSURE.0,
|
||||||
|
999,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(harness.touch_guarded);
|
||||||
|
assert!(harness.touch_active);
|
||||||
|
assert_eq!(harness.last_abs_x, Some(82));
|
||||||
|
assert_eq!(harness.last_abs_y, Some(102));
|
||||||
|
assert_eq!(harness.dx, 4);
|
||||||
|
assert_eq!(harness.dy, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tracking_release_and_unhandled_event_types_stay_safe() {
|
||||||
|
let mut harness = StateHarness::with_touch_state();
|
||||||
|
harness.last_abs_x = Some(33);
|
||||||
|
harness.last_abs_y = Some(44);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = harness.state();
|
||||||
|
state.apply_event(&InputEvent::new(
|
||||||
|
EventType::ABSOLUTE.0,
|
||||||
|
AbsoluteAxisCode::ABS_MT_TRACKING_ID.0,
|
||||||
|
-1,
|
||||||
|
));
|
||||||
|
state.apply_event(&InputEvent::new(EventType::SWITCH.0, 0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(harness.touch_guarded);
|
||||||
|
assert!(!harness.touch_active);
|
||||||
|
assert!(harness.last_abs_x.is_none());
|
||||||
|
assert!(harness.last_abs_y.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replay_events_applies_regular_events_and_flushes_on_sync_boundaries() {
|
||||||
|
let mut harness = StateHarness::new();
|
||||||
|
let (tx, mut rx) = tokio::sync::broadcast::channel(8);
|
||||||
|
let mut next_send = Instant::now() - Duration::from_millis(5);
|
||||||
|
let mut last_buttons = 0_u8;
|
||||||
|
|
||||||
|
MouseRuntime {
|
||||||
|
tx: &tx,
|
||||||
|
dev_mode: true,
|
||||||
|
sending_disabled: false,
|
||||||
|
next_send: &mut next_send,
|
||||||
|
last_buttons: &mut last_buttons,
|
||||||
|
event_state: harness.state(),
|
||||||
|
}
|
||||||
|
.replay_events(vec![
|
||||||
|
InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 1),
|
||||||
|
InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_X.0, 9),
|
||||||
|
InputEvent::new(EventType::SYNCHRONIZATION.0, 0, 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let pkt = rx.try_recv().expect("sync flush packet");
|
||||||
|
assert_eq!(pkt.data[0], 1);
|
||||||
|
assert_eq!(pkt.data[1], 9);
|
||||||
|
assert_eq!(pkt.data[2], 0);
|
||||||
|
assert_eq!(pkt.data[3], 0);
|
||||||
|
assert_eq!(harness.dx, 0);
|
||||||
|
assert_eq!(harness.dy, 0);
|
||||||
|
assert_eq!(harness.wheel, 0);
|
||||||
|
assert_eq!(last_buttons, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_fetched_events_handles_ok_would_block_and_other_errors() {
|
||||||
|
let ok_events = collect_fetched_events(
|
||||||
|
Ok(vec![InputEvent::new(
|
||||||
|
EventType::RELATIVE.0,
|
||||||
|
RelativeAxisCode::REL_X.0,
|
||||||
|
1,
|
||||||
|
)]),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("ok result should yield an event batch");
|
||||||
|
assert_eq!(ok_events.len(), 1);
|
||||||
|
|
||||||
|
let would_block = collect_fetched_events::<Vec<InputEvent>>(
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::WouldBlock,
|
||||||
|
"synthetic would-block",
|
||||||
|
)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert!(would_block.is_none());
|
||||||
|
|
||||||
|
let other_error = collect_fetched_events::<Vec<InputEvent>>(
|
||||||
|
Err(std::io::Error::other("synthetic read failure")),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert!(other_error.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn log_event_batch_tolerates_empty_and_populated_batches() {
|
||||||
|
log_event_batch(false, Some("quiet-mouse"), &[]);
|
||||||
|
log_event_batch(
|
||||||
|
true,
|
||||||
|
Some("trace-mouse"),
|
||||||
|
&[InputEvent::new(
|
||||||
|
EventType::RELATIVE.0,
|
||||||
|
RelativeAxisCode::REL_Y.0,
|
||||||
|
4,
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn process_events_emits_live_relative_packets_for_the_real_crate_path() {
|
||||||
|
let Some((mut vdev, dev)) = build_relative_mouse("lesavka-unit-mouse-rel") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, mut rx) = tokio::sync::broadcast::channel(16);
|
||||||
|
let mut agg = MouseAggregator::new(dev, true, tx);
|
||||||
|
|
||||||
|
vdev.emit(&[
|
||||||
|
InputEvent::new(EventType::KEY.0, KeyCode::BTN_LEFT.0, 1),
|
||||||
|
InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_X.0, 12),
|
||||||
|
InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_Y.0, -5),
|
||||||
|
InputEvent::new(EventType::RELATIVE.0, RelativeAxisCode::REL_WHEEL.0, 1),
|
||||||
|
])
|
||||||
|
.expect("emit relative frame");
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(25));
|
||||||
|
agg.process_events();
|
||||||
|
|
||||||
|
let pkt = rx.try_recv().expect("mouse packet");
|
||||||
|
assert_eq!(pkt.data[0], 1);
|
||||||
|
assert_eq!(pkt.data[1], 12);
|
||||||
|
assert_eq!(pkt.data[2], (-5_i8) as u8);
|
||||||
|
assert_eq!(pkt.data[3], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn process_events_handles_disconnected_virtual_devices_without_panicking() {
|
||||||
|
let Some((vdev, dev)) = build_relative_mouse("lesavka-unit-mouse-err") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tx, _rx) = tokio::sync::broadcast::channel(4);
|
||||||
|
let mut agg = MouseAggregator::new(dev, true, tx);
|
||||||
|
|
||||||
|
drop(vdev);
|
||||||
|
thread::sleep(Duration::from_millis(40));
|
||||||
|
agg.process_events();
|
||||||
|
}
|
||||||
@ -89,8 +89,8 @@
|
|||||||
"loc": 398
|
"loc": 398
|
||||||
},
|
},
|
||||||
"client/src/input/mouse.rs": {
|
"client/src/input/mouse.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 98.85,
|
||||||
"loc": 317
|
"loc": 410
|
||||||
},
|
},
|
||||||
"client/src/launcher/clipboard.rs": {
|
"client/src/launcher/clipboard.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
@ -132,6 +132,10 @@
|
|||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
"loc": 184
|
"loc": 184
|
||||||
},
|
},
|
||||||
|
"client/src/launcher/ui/session_preview_coverage.rs": {
|
||||||
|
"line_percent": 100.0,
|
||||||
|
"loc": 7
|
||||||
|
},
|
||||||
"client/src/layout.rs": {
|
"client/src/layout.rs": {
|
||||||
"line_percent": 97.56,
|
"line_percent": 97.56,
|
||||||
"loc": 78
|
"loc": 78
|
||||||
|
|||||||
@ -158,9 +158,6 @@ mod mouse_contract {
|
|||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn relative_events_emit_button_motion_and_wheel_packets() {
|
fn relative_events_emit_button_motion_and_wheel_packets() {
|
||||||
if cfg!(coverage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some((mut vdev, dev)) = build_relative_mouse("lesavka-include-mouse-rel") else {
|
let Some((mut vdev, dev)) = build_relative_mouse("lesavka-include-mouse-rel") else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -205,9 +202,6 @@ mod mouse_contract {
|
|||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn touch_tracking_updates_touch_state_and_clears_abs_origins_on_release() {
|
fn touch_tracking_updates_touch_state_and_clears_abs_origins_on_release() {
|
||||||
if cfg!(coverage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some((mut vdev, dev)) = build_touch_device("lesavka-include-mouse-touch") else {
|
let Some((mut vdev, dev)) = build_touch_device("lesavka-include-mouse-touch") else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -342,9 +336,6 @@ mod mouse_contract {
|
|||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn absolute_motion_ignores_large_jumps_without_touch_state() {
|
fn absolute_motion_ignores_large_jumps_without_touch_state() {
|
||||||
if cfg!(coverage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some((mut vdev, dev)) = build_absolute_mouse_without_touch("lesavka-include-abs-jump")
|
let Some((mut vdev, dev)) = build_absolute_mouse_without_touch("lesavka-include-abs-jump")
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
@ -382,9 +373,6 @@ mod mouse_contract {
|
|||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn absolute_motion_applies_scaled_delta_within_threshold() {
|
fn absolute_motion_applies_scaled_delta_within_threshold() {
|
||||||
if cfg!(coverage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some((mut vdev, dev)) = build_absolute_mouse_without_touch("lesavka-include-abs-delta")
|
let Some((mut vdev, dev)) = build_absolute_mouse_without_touch("lesavka-include-abs-delta")
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
@ -418,9 +406,6 @@ mod mouse_contract {
|
|||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn touch_guarded_inactive_abs_events_only_update_origins() {
|
fn touch_guarded_inactive_abs_events_only_update_origins() {
|
||||||
if cfg!(coverage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some((mut vdev, dev)) = build_touch_device("lesavka-include-touch-guarded") else {
|
let Some((mut vdev, dev)) = build_touch_device("lesavka-include-touch-guarded") else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user