155 lines
5.3 KiB
Rust
155 lines
5.3 KiB
Rust
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::<CursorPositions>(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;
|
|
}
|
|
}
|