Files
friendolls-desktop/src-tauri/src/services/cursor.rs

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;
}
}