use device_query::{DeviceEvents, DeviceEventsHandler}; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; use std::time::Duration; use tauri::Emitter; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; use ts_rs::TS; use crate::{get_app_handle, lock_r, state::FDOLL}; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct CursorPosition { pub x: f64, pub y: f64, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct CursorPositions { pub raw: CursorPosition, pub mapped: CursorPosition, } static CURSOR_TRACKER: OnceCell<()> = OnceCell::new(); /// Convert absolute screen coordinates to normalized coordinates (0.0 - 1.0) pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition { let guard = lock_r!(FDOLL); let screen_w = guard.app_data.scene.display.screen_width as f64; let screen_h = guard.app_data.scene.display.screen_height as f64; CursorPosition { x: (pos.x / screen_w).clamp(0.0, 1.0), y: (pos.y / screen_h).clamp(0.0, 1.0), } } /// Convert normalized coordinates to absolute screen coordinates pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition { let guard = lock_r!(FDOLL); let screen_w = guard.app_data.scene.display.screen_width as f64; let screen_h = guard.app_data.scene.display.screen_height as f64; CursorPosition { x: (normalized.x * screen_w).round(), y: (normalized.y * screen_h).round(), } } /// Initialize cursor tracking - can be called multiple times safely from any window /// Only the first call will actually start tracking, subsequent calls are no-ops #[tauri::command] pub async fn start_cursor_tracking() -> Result<(), String> { info!("start_cursor_tracking called"); // Use OnceCell to ensure this only runs once, even if called from multiple windows CURSOR_TRACKER.get_or_init(|| { info!("First call to start_cursor_tracking - spawning cursor tracking task"); tauri::async_runtime::spawn(async { if let Err(e) = init_cursor_tracking().await { error!("Failed to initialize cursor tracking: {}", e); } }); }); info!("Cursor tracking initialization registered"); Ok(()) } async fn init_cursor_tracking() -> Result<(), String> { info!("Initializing cursor tracking..."); // Create a channel to decouple event generation (producer) from processing (consumer). // Capacity 100 is plenty for 500ms polling (2Hz). let (tx, mut rx) = mpsc::channel::(100); // Spawn the consumer task // This task handles WebSocket reporting and local broadcasting. // It runs independently of the device event loop. tauri::async_runtime::spawn(async move { info!("Cursor event consumer started"); let app_handle = get_app_handle(); while let Some(positions) = rx.recv().await { let mapped_for_ws = positions.mapped.clone(); // 1. WebSocket reporting crate::services::ws::report_cursor_data(mapped_for_ws).await; // 2. Broadcast to local windows if let Err(e) = app_handle.emit("cursor-position", &positions) { error!("Failed to emit cursor position event: {:?}", e); } } warn!("Cursor event consumer stopped (channel closed)"); }); // Try to initialize the device event handler // Using 500ms sleep as requested by user to reduce CPU usage let device_state = DeviceEventsHandler::new(Duration::from_millis(500)) .ok_or("Failed to create device event handler (already running?)")?; info!("Device event handler created successfully"); info!("Setting up mouse move handler for event broadcasting..."); // Get scale factor from global state #[cfg(target_os = "windows")] let scale_factor = { let guard = lock_r!(FDOLL); guard.app_data.scene.display.monitor_scale_factor }; // The producer closure moves `tx` into it. // device_query runs this closure on its own thread. let _guard = device_state.on_mouse_move(move |position: &(i32, i32)| { // `device_query` crate appears to behave // differently on Windows vs other platforms. // // It doesn't take into account the monitor scale // factor on Windows, so we handle it manually. #[cfg(target_os = "windows")] let raw = CursorPosition { x: position.0 as f64 / scale_factor, y: position.1 as f64 / scale_factor, }; #[cfg(not(target_os = "windows"))] let raw = CursorPosition { x: position.0 as f64, y: position.1 as f64, }; let mapped = absolute_to_normalized(&raw); let positions = CursorPositions { raw, mapped }; // Send to consumer channel (non-blocking) if let Err(e) = tx.try_send(positions) { debug!("Failed to send cursor position to channel: {:?}", e); } }); info!("Mouse move handler registered - now broadcasting cursor events to all windows"); // Keep the handler alive forever // This loop is necessary to keep `_guard` and `device_state` in scope. loop { tokio::time::sleep(Duration::from_secs(3600)).await; } }