Files
One-KVM/src/hid/mod.rs

828 lines
28 KiB
Rust

//! HID path: browser (WebSocket or WebRTC DataChannel) → queue → OTG gadget or CH9329.
pub mod backend;
#[cfg(target_os = "linux")]
mod bluetooth;
pub mod ch9329;
mod ch9329_proto;
pub mod consumer;
pub mod datachannel;
mod factory;
pub mod keyboard;
mod macos_drag;
#[cfg(unix)]
pub mod otg;
#[cfg(unix)]
mod otg_device;
pub mod types;
pub mod websocket;
pub use crate::events::LedState;
pub use backend::{HidBackend, HidBackendRuntimeSnapshot, HidBackendType};
pub use keyboard::CanonicalKey;
pub use types::{
ConsumerEvent, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, MouseEvent,
MouseEventType,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HidRuntimeState {
pub available: bool,
pub backend: String,
pub initialized: bool,
pub online: bool,
pub supports_absolute_mouse: bool,
pub keyboard_leds_enabled: bool,
pub led_state: LedState,
pub screen_resolution: Option<(u32, u32)>,
pub device: Option<String>,
pub error: Option<String>,
pub error_code: Option<String>,
}
impl HidRuntimeState {
fn from_backend_type(backend_type: &HidBackendType) -> Self {
Self {
available: !matches!(backend_type, HidBackendType::None),
backend: backend_type.name_str().to_string(),
initialized: false,
online: false,
supports_absolute_mouse: false,
keyboard_leds_enabled: false,
led_state: LedState::default(),
screen_resolution: None,
device: device_for_backend_type(backend_type),
error: None,
error_code: None,
}
}
fn from_backend(backend_type: &HidBackendType, snapshot: HidBackendRuntimeSnapshot) -> Self {
Self {
available: !matches!(backend_type, HidBackendType::None),
backend: backend_type.name_str().to_string(),
initialized: snapshot.initialized,
online: snapshot.online,
supports_absolute_mouse: snapshot.supports_absolute_mouse,
keyboard_leds_enabled: snapshot.keyboard_leds_enabled,
led_state: snapshot.led_state,
screen_resolution: snapshot.screen_resolution,
device: snapshot
.device
.or_else(|| device_for_backend_type(backend_type)),
error: snapshot.error,
error_code: snapshot.error_code,
}
}
fn with_error(
backend_type: &HidBackendType,
current: &Self,
reason: impl Into<String>,
error_code: impl Into<String>,
) -> Self {
let mut next = current.clone();
next.available = !matches!(backend_type, HidBackendType::None);
next.backend = backend_type.name_str().to_string();
next.initialized = false;
next.online = false;
next.keyboard_leds_enabled = false;
next.led_state = LedState::default();
next.device = device_for_backend_type(backend_type);
next.error = Some(reason.into());
next.error_code = Some(error_code.into());
next
}
}
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::{info, warn};
use crate::config::{Ch9329DescriptorConfig, Ch9329DescriptorState};
use crate::error::{AppError, Result};
use crate::events::EventBus;
#[cfg(unix)]
use crate::otg::OtgService;
use factory::HidBackendFactory;
use tokio::sync::mpsc;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
const HID_EVENT_QUEUE_CAPACITY: usize = 64;
const HID_EVENT_SEND_TIMEOUT_MS: u64 = 30;
#[derive(Debug)]
enum QueuedHidEvent {
Keyboard(KeyboardEvent),
Mouse(MouseEvent),
Consumer(ConsumerEvent),
Reset,
}
pub struct HidController {
backend_factory: HidBackendFactory,
backend: Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
backend_type: Arc<RwLock<HidBackendType>>,
events: Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
runtime_state: Arc<RwLock<HidRuntimeState>>,
hid_tx: mpsc::Sender<QueuedHidEvent>,
hid_rx: Mutex<Option<mpsc::Receiver<QueuedHidEvent>>>,
pending_move: Arc<parking_lot::Mutex<Option<MouseEvent>>>,
pending_move_flag: Arc<AtomicBool>,
hid_worker: Mutex<Option<JoinHandle<()>>>,
runtime_worker: Mutex<Option<JoinHandle<()>>>,
backend_available: Arc<AtomicBool>,
reset_requested: Arc<AtomicBool>,
screen_resolution: parking_lot::RwLock<(u32, u32)>,
}
impl HidController {
#[cfg(unix)]
pub fn new(backend_type: HidBackendType, otg_service: Option<Arc<OtgService>>) -> Self {
let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY);
Self {
backend: Arc::new(RwLock::new(None)),
backend_factory: HidBackendFactory::new(otg_service),
backend_type: Arc::new(RwLock::new(backend_type.clone())),
events: Arc::new(tokio::sync::RwLock::new(None)),
runtime_state: Arc::new(RwLock::new(HidRuntimeState::from_backend_type(
&backend_type,
))),
hid_tx,
hid_rx: Mutex::new(Some(hid_rx)),
pending_move: Arc::new(parking_lot::Mutex::new(None)),
pending_move_flag: Arc::new(AtomicBool::new(false)),
hid_worker: Mutex::new(None),
runtime_worker: Mutex::new(None),
backend_available: Arc::new(AtomicBool::new(false)),
reset_requested: Arc::new(AtomicBool::new(false)),
screen_resolution: parking_lot::RwLock::new((1920, 1080)),
}
}
#[cfg(not(unix))]
pub fn new(backend_type: HidBackendType) -> Self {
let (hid_tx, hid_rx) = mpsc::channel(HID_EVENT_QUEUE_CAPACITY);
Self {
backend: Arc::new(RwLock::new(None)),
backend_factory: HidBackendFactory::new(),
backend_type: Arc::new(RwLock::new(backend_type.clone())),
events: Arc::new(tokio::sync::RwLock::new(None)),
runtime_state: Arc::new(RwLock::new(HidRuntimeState::from_backend_type(
&backend_type,
))),
hid_tx,
hid_rx: Mutex::new(Some(hid_rx)),
pending_move: Arc::new(parking_lot::Mutex::new(None)),
pending_move_flag: Arc::new(AtomicBool::new(false)),
hid_worker: Mutex::new(None),
runtime_worker: Mutex::new(None),
backend_available: Arc::new(AtomicBool::new(false)),
reset_requested: Arc::new(AtomicBool::new(false)),
screen_resolution: parking_lot::RwLock::new((1920, 1080)),
}
}
#[cfg(target_os = "linux")]
pub fn set_bond_store(&self, store: crate::db::hid_bonds::HidBondStore) {
let _ = self.backend_factory.bonds.set(Arc::new(store));
}
pub async fn set_event_bus(&self, events: Arc<EventBus>) {
*self.events.write().await = Some(events);
}
pub async fn init(&self) -> Result<()> {
let backend_type = self.backend_type.read().await.clone();
let backend = match self.backend_factory.create_initialized(&backend_type).await {
Ok(Some(backend)) => backend,
Ok(None) => return Ok(()),
Err(error) => {
self.backend_available.store(false, Ordering::Release);
let current = self.runtime_state.read().await.clone();
let error_state = HidRuntimeState::with_error(
&backend_type,
&current,
format!("Failed to initialize HID backend: {}", error),
"init_failed",
);
self.apply_runtime_state(error_state).await;
return Err(error);
}
};
{
let mut slot = self.backend.write().await;
let (width, height) = *self.screen_resolution.read();
backend.set_screen_resolution(width, height);
*slot = Some(backend);
}
self.sync_runtime_state_from_backend().await;
self.start_event_worker().await;
self.restart_runtime_worker().await;
info!("HID backend initialized: {:?}", backend_type);
Ok(())
}
pub async fn shutdown(&self) -> Result<()> {
info!("Shutting down HID controller");
self.stop_runtime_worker().await;
if let Some(backend) = self.backend.write().await.take() {
if let Err(e) = backend.shutdown().await {
warn!("Error shutting down HID backend: {}", e);
}
}
self.backend_available.store(false, Ordering::Release);
let backend_type = self.backend_type.read().await.clone();
let mut shutdown_state = HidRuntimeState::from_backend_type(&backend_type);
if matches!(backend_type, HidBackendType::None) {
shutdown_state.available = false;
} else {
shutdown_state.error = Some("HID backend stopped".to_string());
shutdown_state.error_code = Some("shutdown".to_string());
}
self.apply_runtime_state(shutdown_state).await;
info!("HID controller shutdown complete");
Ok(())
}
pub async fn prepare_otg_rebuild(&self) -> Result<()> {
let backend_type = self.backend_type.read().await.clone();
if !matches!(backend_type, HidBackendType::Otg { .. }) {
return Ok(());
}
info!("Preparing OTG HID backend for gadget rebuild");
self.backend_available.store(false, Ordering::Release);
self.stop_runtime_worker().await;
if let Some(backend) = self.backend.write().await.take() {
backend.prepare_rebuild().await?;
}
let current = self.runtime_state.read().await.clone();
let rebuilding_state = HidRuntimeState::with_error(
&backend_type,
&current,
"OTG gadget is rebuilding",
"rebuilding",
);
self.apply_runtime_state(rebuilding_state).await;
Ok(())
}
pub async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> {
if !self.backend_available.load(Ordering::Acquire) {
return Err(AppError::BadRequest(
"HID backend not available".to_string(),
));
}
self.enqueue_event(QueuedHidEvent::Keyboard(event)).await
}
pub async fn send_mouse(&self, event: MouseEvent) -> Result<()> {
if !self.backend_available.load(Ordering::Acquire) {
return Err(AppError::BadRequest(
"HID backend not available".to_string(),
));
}
if matches!(
event.event_type,
MouseEventType::Move | MouseEventType::MoveAbs
) {
self.enqueue_mouse_move(event)
} else {
self.enqueue_event(QueuedHidEvent::Mouse(event)).await
}
}
pub async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> {
if !self.backend_available.load(Ordering::Acquire) {
return Err(AppError::BadRequest(
"HID backend not available".to_string(),
));
}
self.enqueue_event(QueuedHidEvent::Consumer(event)).await
}
pub async fn bluetooth_status(&self) -> Result<serde_json::Value> {
let backend = self
.backend
.read()
.await
.clone()
.ok_or_else(|| AppError::BadRequest("HID unavailable".into()))?;
backend.bluetooth_status().await
}
pub async fn bluetooth_action(&self, action: &str, seconds: u32) -> Result<()> {
let backend = self
.backend
.read()
.await
.clone()
.ok_or_else(|| AppError::BadRequest("HID unavailable".into()))?;
backend.bluetooth_action(action, seconds).await
}
pub async fn reset(&self) -> Result<()> {
if !self.backend_available.load(Ordering::Acquire) {
return Ok(());
}
self.enqueue_event(QueuedHidEvent::Reset).await
}
pub async fn is_available(&self) -> bool {
self.backend_available.load(Ordering::Acquire)
}
pub async fn backend_type(&self) -> HidBackendType {
self.backend_type.read().await.clone()
}
/// Keep the active capture dimensions across HID reloads and USB rebuilds.
pub async fn set_screen_resolution(&self, width: u32, height: u32) {
if width == 0 || height == 0 || width > 65535 || height > 65535 {
return;
}
{
let mut resolution = self.screen_resolution.write();
if *resolution == (width, height) {
return;
}
*resolution = (width, height);
}
if let Some(backend) = self.backend.read().await.as_ref() {
let (width, height) = *self.screen_resolution.read();
backend.set_screen_resolution(width, height);
}
}
pub async fn snapshot(&self) -> HidRuntimeState {
self.runtime_state.read().await.clone()
}
async fn ch9329_backend(&self) -> Result<Arc<dyn HidBackend>> {
if !matches!(
*self.backend_type.read().await,
HidBackendType::Ch9329 { .. }
) {
return Err(AppError::BadRequest(
"Current HID backend is not CH9329".to_string(),
));
}
self.backend
.read()
.await
.clone()
.ok_or_else(|| AppError::BadRequest("CH9329 backend not available".to_string()))
}
pub async fn apply_ch9329_descriptor(
&self,
descriptor: &Ch9329DescriptorConfig,
) -> Result<Ch9329DescriptorState> {
self.ch9329_backend()
.await?
.apply_ch9329_descriptor(descriptor)
.await
}
pub async fn read_ch9329_descriptor(&self) -> Result<Ch9329DescriptorState> {
self.ch9329_backend().await?.read_ch9329_descriptor().await
}
pub async fn reload(&self, new_backend_type: HidBackendType) -> Result<()> {
info!("Reloading HID backend: {:?}", new_backend_type);
self.backend_available.store(false, Ordering::Release);
self.stop_runtime_worker().await;
if let Some(backend) = self.backend.write().await.take() {
if let Err(e) = backend.shutdown().await {
// A Bluetooth shutdown may fail to restore adapter settings. Surface
// that failure so the config transaction can roll back.
if matches!(
*self.backend_type.read().await,
HidBackendType::Bluetooth { .. }
) {
return Err(e);
}
warn!("Error shutting down old HID backend: {}", e);
}
}
let new_backend = match self
.backend_factory
.create_initialized(&new_backend_type)
.await
{
Ok(backend) => backend,
Err(error) if matches!(&new_backend_type, HidBackendType::None) => return Err(error),
Err(error) => {
warn!("Failed to initialize HID backend: {}", error);
None
}
};
{
let mut slot = self.backend.write().await;
if let Some(backend) = new_backend.as_ref() {
let (width, height) = *self.screen_resolution.read();
backend.set_screen_resolution(width, height);
}
*slot = new_backend;
}
if matches!(new_backend_type, HidBackendType::None) {
*self.backend_type.write().await = HidBackendType::None;
self.apply_runtime_state(HidRuntimeState::from_backend_type(&HidBackendType::None))
.await;
return Ok(());
}
if self.backend.read().await.is_some() {
info!("HID backend reloaded successfully: {:?}", new_backend_type);
self.start_event_worker().await;
*self.backend_type.write().await = new_backend_type.clone();
self.sync_runtime_state_from_backend().await;
self.restart_runtime_worker().await;
Ok(())
} else {
warn!("HID backend reload resulted in no active backend");
self.backend_available.store(false, Ordering::Release);
*self.backend_type.write().await = new_backend_type.clone();
let current = self.runtime_state.read().await.clone();
let error_state = HidRuntimeState::with_error(
&new_backend_type,
&current,
"Failed to initialize HID backend",
"init_failed",
);
self.apply_runtime_state(error_state).await;
Err(AppError::Internal(
"Failed to reload HID backend".to_string(),
))
}
}
async fn apply_runtime_state(&self, next: HidRuntimeState) {
apply_runtime_state(&self.runtime_state, &self.events, next).await;
}
async fn sync_runtime_state_from_backend(&self) {
let backend_opt = self.backend.read().await.clone();
apply_backend_runtime_state(
&self.backend_type,
&self.runtime_state,
&self.events,
self.backend_available.as_ref(),
backend_opt.as_deref(),
)
.await;
}
async fn start_event_worker(&self) {
let mut worker_guard = self.hid_worker.lock().await;
if worker_guard.is_some() {
return;
}
let mut rx_guard = self.hid_rx.lock().await;
let rx = match rx_guard.take() {
Some(rx) => rx,
None => return,
};
let backend = self.backend.clone();
let pending_move = self.pending_move.clone();
let pending_move_flag = self.pending_move_flag.clone();
let reset_requested = self.reset_requested.clone();
let handle = tokio::spawn(async move {
let mut rx = rx;
loop {
let event = match rx.recv().await {
Some(ev) => ev,
None => break,
};
if reset_requested.swap(false, Ordering::AcqRel) {
// A full input queue must not lose a key/button release and leave
// the host stuck. Discard the obsolete batch and send all-up.
while rx.try_recv().is_ok() {}
*pending_move.lock() = None;
pending_move_flag.store(false, Ordering::Release);
process_hid_event(QueuedHidEvent::Reset, &backend).await;
continue;
}
process_hid_event(event, &backend).await;
if pending_move_flag.swap(false, Ordering::AcqRel) {
let move_event = { pending_move.lock().take() };
if let Some(move_event) = move_event {
process_hid_event(QueuedHidEvent::Mouse(move_event), &backend).await;
}
}
}
});
*worker_guard = Some(handle);
}
async fn restart_runtime_worker(&self) {
self.stop_runtime_worker().await;
let backend_opt = self.backend.read().await.clone();
let Some(backend) = backend_opt else {
return;
};
let mut runtime_rx = backend.subscribe_runtime();
let runtime_state = self.runtime_state.clone();
let events = self.events.clone();
let backend_available = self.backend_available.clone();
let backend_type = self.backend_type.clone();
let handle = tokio::spawn(async move {
loop {
if runtime_rx.changed().await.is_err() {
break;
}
apply_backend_runtime_state(
&backend_type,
&runtime_state,
&events,
backend_available.as_ref(),
Some(backend.as_ref()),
)
.await;
}
});
*self.runtime_worker.lock().await = Some(handle);
}
async fn stop_runtime_worker(&self) {
if let Some(handle) = self.runtime_worker.lock().await.take() {
handle.abort();
}
}
fn enqueue_mouse_move(&self, event: MouseEvent) -> Result<()> {
match self.hid_tx.try_send(QueuedHidEvent::Mouse(event.clone())) {
Ok(_) => Ok(()),
Err(mpsc::error::TrySendError::Full(_)) => {
merge_pending_move(&mut self.pending_move.lock(), event);
self.pending_move_flag.store(true, Ordering::Release);
Ok(())
}
Err(mpsc::error::TrySendError::Closed(_)) => {
Err(AppError::BadRequest("HID event queue closed".to_string()))
}
}
}
async fn enqueue_event(&self, event: QueuedHidEvent) -> Result<()> {
match self.hid_tx.try_send(event) {
Ok(_) => Ok(()),
Err(mpsc::error::TrySendError::Full(ev)) => {
let tx = self.hid_tx.clone();
let send_result = tokio::time::timeout(
Duration::from_millis(HID_EVENT_SEND_TIMEOUT_MS),
tx.send(ev),
)
.await;
match send_result {
Ok(Ok(())) => Ok(()),
Ok(Err(_)) => Err(AppError::BadRequest("HID event queue closed".into())),
Err(_) => {
self.reset_requested.store(true, Ordering::Release);
warn!("HID event queue full; scheduling all-input release");
Err(AppError::ServiceUnavailable(
"HID input queue full; input state will be reset".into(),
))
}
}
}
Err(mpsc::error::TrySendError::Closed(_)) => {
Err(AppError::BadRequest("HID event queue closed".to_string()))
}
}
}
}
async fn apply_backend_runtime_state(
backend_type: &Arc<RwLock<HidBackendType>>,
runtime_state: &Arc<RwLock<HidRuntimeState>>,
events: &Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
backend_available: &AtomicBool,
backend: Option<&dyn HidBackend>,
) {
let backend_kind = backend_type.read().await.clone();
let next = match backend {
Some(backend) => HidRuntimeState::from_backend(&backend_kind, backend.runtime_snapshot()),
None => HidRuntimeState::from_backend_type(&backend_kind),
};
backend_available.store(next.initialized, Ordering::Release);
apply_runtime_state(runtime_state, events, next).await;
}
async fn process_hid_event(
event: QueuedHidEvent,
backend: &Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
) {
let backend_opt = backend.read().await.clone();
let backend = match backend_opt {
Some(b) => b,
None => return,
};
let backend_for_send = backend.clone();
let result = tokio::task::spawn_blocking(move || {
futures::executor::block_on(async move {
match event {
QueuedHidEvent::Keyboard(ev) => backend_for_send.send_keyboard(ev).await,
QueuedHidEvent::Mouse(ev) => backend_for_send.send_mouse(ev).await,
QueuedHidEvent::Consumer(ev) => backend_for_send.send_consumer(ev).await,
QueuedHidEvent::Reset => backend_for_send.reset().await,
}
})
})
.await;
let result = match result {
Ok(r) => r,
Err(_) => return,
};
match result {
Ok(_) => {}
Err(e) => {
warn!("HID event processing failed: {}", e);
}
}
}
fn device_for_backend_type(backend_type: &HidBackendType) -> Option<String> {
match backend_type {
HidBackendType::Ch9329 { port, .. } => Some(port.clone()),
_ => None,
}
}
async fn apply_runtime_state(
runtime_state: &Arc<RwLock<HidRuntimeState>>,
events: &Arc<tokio::sync::RwLock<Option<Arc<EventBus>>>>,
next: HidRuntimeState,
) {
let changed = {
let mut guard = runtime_state.write().await;
if *guard == next {
false
} else {
*guard = next.clone();
true
}
};
if !changed {
return;
}
if let Some(events) = events.read().await.as_ref() {
events.mark_device_info_dirty();
}
}
fn merge_pending_move(pending: &mut Option<MouseEvent>, event: MouseEvent) {
if let Some(previous) = pending {
if previous.event_type == MouseEventType::Move && event.event_type == MouseEventType::Move {
previous.x = previous.x.saturating_add(event.x).clamp(-32767, 32767);
previous.y = previous.y.saturating_add(event.y).clamp(-32767, 32767);
return;
}
}
*pending = Some(event);
}
#[cfg(test)]
mod queue_tests {
use super::*;
#[tokio::test]
async fn screen_resolution_updates_backend_and_retains_valid_dimensions() {
#[cfg(unix)]
let controller = HidController::new(HidBackendType::None, None);
#[cfg(not(unix))]
let controller = HidController::new(HidBackendType::None);
let backend = Arc::new(ch9329::Ch9329Backend::new("/dev/null").unwrap());
*controller.backend.write().await = Some(backend.clone());
controller.set_screen_resolution(3840, 2160).await;
assert_eq!(
backend.runtime_snapshot().screen_resolution,
Some((3840, 2160))
);
controller.set_screen_resolution(0, 0).await;
assert_eq!(*controller.screen_resolution.read(), (3840, 2160));
controller.set_screen_resolution(1280, 720).await;
assert_eq!(
backend.runtime_snapshot().screen_resolution,
Some((1280, 720))
);
}
struct TestBackend {
pressed: Arc<AtomicBool>,
reset_done: Arc<tokio::sync::Notify>,
runtime: tokio::sync::watch::Sender<()>,
}
#[async_trait::async_trait]
impl HidBackend for TestBackend {
async fn init(&self) -> Result<()> {
Ok(())
}
async fn send_keyboard(&self, _: KeyboardEvent) -> Result<()> {
self.pressed.store(true, Ordering::Release);
Ok(())
}
async fn send_mouse(&self, _: MouseEvent) -> Result<()> {
Ok(())
}
async fn reset(&self) -> Result<()> {
self.pressed.store(false, Ordering::Release);
self.reset_done.notify_one();
Ok(())
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
fn runtime_snapshot(&self) -> HidBackendRuntimeSnapshot {
HidBackendRuntimeSnapshot::default()
}
fn subscribe_runtime(&self) -> tokio::sync::watch::Receiver<()> {
self.runtime.subscribe()
}
}
#[tokio::test]
async fn congested_queue_releases_host_instead_of_replaying_keydowns() {
#[cfg(unix)]
let controller = HidController::new(HidBackendType::None, None);
#[cfg(not(unix))]
let controller = HidController::new(HidBackendType::None);
let pressed = Arc::new(AtomicBool::new(true));
let done = Arc::new(tokio::sync::Notify::new());
let (runtime, _) = tokio::sync::watch::channel(());
*controller.backend.write().await = Some(Arc::new(TestBackend {
pressed: pressed.clone(),
reset_done: done.clone(),
runtime,
}));
for _ in 0..HID_EVENT_QUEUE_CAPACITY {
controller
.enqueue_event(QueuedHidEvent::Keyboard(KeyboardEvent::key_down(
CanonicalKey::KeyA,
KeyboardModifiers::default(),
)))
.await
.unwrap();
}
assert!(controller
.enqueue_event(QueuedHidEvent::Reset)
.await
.is_err());
controller.start_event_worker().await;
tokio::time::timeout(Duration::from_secs(1), done.notified())
.await
.unwrap();
assert!(!pressed.load(Ordering::Acquire));
}
#[test]
fn relative_motion_is_accumulated_but_absolute_is_replaced() {
let mut pending = Some(MouseEvent::move_rel(100, -20));
merge_pending_move(&mut pending, MouseEvent::move_rel(80, 30));
assert_eq!(
(pending.as_ref().unwrap().x, pending.as_ref().unwrap().y),
(180, 10)
);
merge_pending_move(&mut pending, MouseEvent::move_abs(10, 20));
merge_pending_move(&mut pending, MouseEvent::move_abs(30, 40));
assert_eq!(
(pending.as_ref().unwrap().x, pending.as_ref().unwrap().y),
(30, 40)
);
}
}