diff --git a/src-tauri/.env.example b/src-tauri/.env.example index 0754541..2ec8858 100644 --- a/src-tauri/.env.example +++ b/src-tauri/.env.example @@ -2,3 +2,4 @@ API_BASE_URL=http://127.0.0.1:3000 AUTH_URL=https://auth.example.com/realms/friendolls/protocol/openid-connect/auth JWT_AUDIENCE=friendolls-desktop REDIRECT_URI=http://localhost:8582/callback +REDIRECT_HOST=localhost:8582 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6b9ba1f..61aabdb 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -161,6 +167,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "async-task" version = "4.7.1" @@ -213,6 +241,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "getrandom 0.2.16", + "instant", + "rand 0.8.5", +] + [[package]] name = "base64" version = "0.21.7" @@ -656,6 +695,12 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "dbus" version = "0.9.9" @@ -1051,6 +1096,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "reqwest", + "rust_socketio", "serde", "serde_json", "sha2", @@ -1852,6 +1898,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3315,6 +3370,50 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rust_engineio" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d3572ceba6c5d79eecedf3be93640ca9512fa4100dff6a70f96c514adf4f1f" +dependencies = [ + "adler32", + "async-stream", + "async-trait", + "base64 0.21.7", + "bytes", + "futures-util", + "http", + "native-tls", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", + "tungstenite", + "url", +] + +[[package]] +name = "rust_socketio" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6a8672db895d567b3c0b8a4c0d3e98113ebb32badf6ce66004e743e5ee1e1e" +dependencies = [ + "adler32", + "backoff", + "base64 0.21.7", + "bytes", + "log", + "native-tls", + "rand 0.8.5", + "rust_engineio", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -3691,6 +3790,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -4460,6 +4570,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -4720,6 +4844,26 @@ dependencies = [ "termcolor", ] +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + [[package]] name = "typeid" version = "1.0.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0fc4464..7429e33 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,6 +39,7 @@ tracing = "0.1" tracing-subscriber = "0.3" once_cell = "1" flate2 = "1.0.28" +rust_socketio = "0.6.0" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4cdbf49..949c0ed 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,9 +2,11 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main"], + "windows": ["main", "scene", "preferences"], "permissions": [ "core:default", - "opener:default" + "opener:default", + "core:event:allow-listen", + "core:event:allow-unlisten" ] } diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 5c090b5..d6807d9 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -3,7 +3,7 @@ use tauri_plugin_positioner::WindowExt; use tracing::{error, info}; use crate::{ - core::services::auth::get_tokens, + core::services::{auth::get_tokens, preferences::create_preferences_window}, get_app_handle, services::overlay::{overlay_fullscreen, SCENE_WINDOW_LABEL}, }; @@ -16,7 +16,8 @@ pub async fn init_session() { match get_tokens().await { Some(_) => { info!("User session restored"); - create_scene().await; + create_scene(); + create_preferences_window(); } None => { info!("No active session, user needs to authenticate"); @@ -24,14 +25,15 @@ pub async fn init_session() { info!("Authentication successful, creating scene..."); tauri::async_runtime::spawn(async { info!("Creating scene after auth success..."); - create_scene().await; + create_scene(); + create_preferences_window(); }); }); } } } -pub async fn create_scene() { +pub fn create_scene() { info!("Starting scene creation..."); let webview_window = match tauri::WebviewWindowBuilder::new( get_app_handle(), diff --git a/src-tauri/src/core/services/auth.rs b/src-tauri/src/core/services/auth.rs index 74c72ac..7677abe 100644 --- a/src-tauri/src/core/services/auth.rs +++ b/src-tauri/src/core/services/auth.rs @@ -371,9 +371,15 @@ pub async fn exchange_code_for_auth_pass( ) -> Result { let (app_config, http_client) = { let guard = lock_r!(FDOLL); + let clients = guard.clients.as_ref(); + if clients.is_none() { + error!("Clients not initialized yet!"); + return Err(OAuthError::InvalidConfig); + } + info!("HTTP client retrieved successfully for token exchange"); ( - guard.app_config.clone().ok_or(OAuthError::InvalidConfig)?, - guard.http_client.clone(), + guard.app_config.clone(), + clients.unwrap().http_client.clone(), ) }; @@ -389,13 +395,35 @@ pub async fn exchange_code_for_auth_pass( .finish(); info!("Exchanging authorization code for tokens"); + info!("Token endpoint URL: {}", url); + info!("Request body length: {} bytes", body.len()); let exchange_request = http_client - .post(url) + .post(url.clone()) .header("Content-Type", "application/x-www-form-urlencoded") .body(body); - let exchange_request_response = exchange_request.send().await?; + info!("Sending token exchange request..."); + let exchange_request_response = match exchange_request.send().await { + Ok(resp) => { + info!("Received response with status: {}", resp.status()); + resp + } + Err(e) => { + error!("Failed to send token exchange request: {}", e); + error!("Error details: {:?}", e); + if e.is_timeout() { + error!("Request timed out"); + } + if e.is_connect() { + error!("Connection error - check network and DNS"); + } + if e.is_request() { + error!("Request error - check request format"); + } + return Err(OAuthError::NetworkError(e)); + } + }; if !exchange_request_response.status().is_success() { let status = exchange_request_response.status(); @@ -446,16 +474,14 @@ pub fn init_auth_code_retrieval(on_success: F) where F: FnOnce() + Send + 'static, { - let app_config = match lock_r!(FDOLL).app_config.clone() { - Some(config) => config, - None => { - error!("Cannot initialize auth: app config not available"); - return; - } - }; + info!("init_auth_code_retrieval called"); + let app_config = lock_r!(FDOLL).app_config.clone(); let opener = match APP_HANDLE.get() { - Some(handle) => handle.opener(), + Some(handle) => { + info!("APP_HANDLE retrieved successfully"); + handle.opener() + } None => { error!("Cannot initialize auth: app handle not available"); return; @@ -480,7 +506,10 @@ where } let mut url = match url::Url::parse(&format!("{}/auth", &app_config.auth.auth_url)) { - Ok(url) => url, + Ok(url) => { + info!("Parsed auth URL successfully"); + url + } Err(e) => { error!("Invalid auth URL configuration: {}", e); return; @@ -501,8 +530,10 @@ where // Bind the server FIRST to ensure port is open // We bind synchronously using std::net::TcpListener then convert to tokio::net::TcpListener // to ensure the port is bound before we open the browser. + info!("Attempting to bind to: {}", app_config.auth.redirect_host); let std_listener = match std::net::TcpListener::bind(&app_config.auth.redirect_host) { Ok(s) => { + info!("Successfully bound to {}", app_config.auth.redirect_host); s.set_nonblocking(true).unwrap(); s } @@ -514,7 +545,7 @@ where info!( "Listening on {} for /callback", - &app_config.auth.redirect_host + app_config.auth.redirect_host ); tauri::async_runtime::spawn(async move { @@ -571,8 +602,11 @@ where } }); + info!("Opening auth URL: {}", url); if let Err(e) = opener.open_url(url, None::<&str>) { error!("Failed to open auth portal: {}", e); + } else { + info!("Successfully called open_url for auth portal"); } } @@ -592,8 +626,13 @@ pub async fn refresh_token(refresh_token: &str) -> Result let (app_config, http_client) = { let guard = lock_r!(FDOLL); ( - guard.app_config.clone().ok_or(OAuthError::InvalidConfig)?, - guard.http_client.clone(), + guard.app_config.clone(), + guard + .clients + .as_ref() + .expect("clients present") + .http_client + .clone(), ) }; diff --git a/src-tauri/src/core/services/mod.rs b/src-tauri/src/core/services/mod.rs index 0e4a05d..175f5e0 100644 --- a/src-tauri/src/core/services/mod.rs +++ b/src-tauri/src/core/services/mod.rs @@ -1 +1,3 @@ pub mod auth; +pub mod preferences; +pub mod ws; diff --git a/src-tauri/src/core/services/preferences.rs b/src-tauri/src/core/services/preferences.rs new file mode 100644 index 0000000..41a6655 --- /dev/null +++ b/src-tauri/src/core/services/preferences.rs @@ -0,0 +1,35 @@ +use tracing::{error, info}; + +use crate::get_app_handle; + +pub fn create_preferences_window() { + let webview_window = match tauri::WebviewWindowBuilder::new( + get_app_handle(), + "preferences", + tauri::WebviewUrl::App("/preferences".into()), + ) + .title("Friendolls Preferences") + .inner_size(600.0, 500.0) + .resizable(true) + .decorations(true) + .transparent(false) + .shadow(true) + .visible(true) + .skip_taskbar(false) + .always_on_top(false) + .visible_on_all_workspaces(false) + .build() + { + Ok(window) => { + info!("Preferences window builder succeeded"); + window + } + Err(e) => { + error!("Failed to build Preferences window: {}", e); + return; + } + }; + + #[cfg(debug_assertions)] + webview_window.open_devtools(); +} diff --git a/src-tauri/src/core/services/ws.rs b/src-tauri/src/core/services/ws.rs new file mode 100644 index 0000000..d81ba85 --- /dev/null +++ b/src-tauri/src/core/services/ws.rs @@ -0,0 +1,63 @@ +use rust_socketio::{ClientBuilder, Payload, RawClient}; +use serde_json::json; +use tauri::async_runtime; +use tracing::error; + +use crate::{ + core::{models::app_config::AppConfig, state::FDOLL}, + lock_r, + services::cursor::CursorPosition, +}; + +// Define a callback for handling incoming messages (e.g., 'pong') +fn on_pong(payload: Payload, _socket: RawClient) { + match payload { + Payload::Text(str) => println!("Received pong: {:?}", str), + Payload::Binary(bin) => println!("Received pong (binary): {:?}", bin), + _ => todo!(), + } +} + +pub async fn report_cursor_data(cursor_position: CursorPosition) { + let client = { + let guard = lock_r!(FDOLL); + guard + .clients + .as_ref() + .expect("Clients are initialized") + .ws_client + .as_ref() + .expect("WebSocket client is initialized") + .clone() + }; + + match async_runtime::spawn_blocking(move || { + client.emit( + "cursor-report-position", + Payload::Text(vec![json!({ "position": cursor_position })]), + ) + }) + .await + { + Ok(Ok(_)) => (), + Ok(Err(e)) => error!("Failed to emit ping: {}", e), + Err(e) => error!("Failed to execute blocking task: {}", e), + } +} + +pub fn build_ws_client(app_config: &AppConfig) -> rust_socketio::client::Client { + let client = match ClientBuilder::new( + app_config + .api_base_url + .as_ref() + .expect("Missing API base URL"), + ) + .namespace("/") + .on("pong", on_pong) + .connect() + { + Ok(c) => c, + Err(_) => todo!("TODO error handling"), + }; + client +} diff --git a/src-tauri/src/core/state.rs b/src-tauri/src/core/state.rs index 629ecea..8d1c23a 100644 --- a/src-tauri/src/core/state.rs +++ b/src-tauri/src/core/state.rs @@ -1,15 +1,20 @@ // in app-core/src/state.rs use crate::{ - core::models::app_config::{AppConfig, AuthConfig}, - core::services::auth::{load_auth_pass, AuthPass}, + core::{ + models::app_config::{AppConfig, AuthConfig}, + services::{ + auth::{load_auth_pass, AuthPass}, + ws::build_ws_client, + }, + }, lock_w, }; -use reqwest::Client; use std::{ env, sync::{Arc, LazyLock, RwLock}, }; -use tracing::warn; +use tauri::async_runtime; +use tracing::{info, warn}; #[derive(Default, Clone)] pub struct OAuthFlowTracker { @@ -18,10 +23,15 @@ pub struct OAuthFlowTracker { pub initiated_at: Option, } +pub struct Clients { + pub http_client: reqwest::Client, + pub ws_client: Option, +} + #[derive(Default)] pub struct AppState { - pub app_config: Option, - pub http_client: Client, + pub app_config: AppConfig, + pub clients: Option, pub auth_pass: Option, pub oauth_flow: OAuthFlowTracker, } @@ -35,7 +45,7 @@ pub fn init_fdoll_state() { { let mut guard = lock_w!(FDOLL); dotenvy::dotenv().ok(); - guard.app_config = Some(AppConfig { + guard.app_config = AppConfig { api_base_url: Some(env::var("API_BASE_URL").expect("API_BASE_URL must be set")), auth: AuthConfig { audience: env::var("JWT_AUDIENCE").expect("JWT_AUDIENCE must be set"), @@ -43,7 +53,7 @@ pub fn init_fdoll_state() { redirect_uri: env::var("REDIRECT_URI").expect("REDIRECT_URI must be set"), redirect_host: env::var("REDIRECT_HOST").expect("REDIRECT_HOST must be set"), }, - }); + }; guard.auth_pass = match load_auth_pass() { Ok(pass) => pass, Err(e) => { @@ -51,9 +61,42 @@ pub fn init_fdoll_state() { None } }; - guard.http_client = reqwest::ClientBuilder::new() - .redirect(reqwest::redirect::Policy::none()) + info!("Loaded auth pass"); + + // Initialize HTTP client immediately (non-blocking) + let http_client = reqwest::ClientBuilder::new() + .timeout(std::time::Duration::from_secs(30)) + .connect_timeout(std::time::Duration::from_secs(10)) + .user_agent("friendolls-desktop/0.1.0") .build() .expect("Client should build"); + + // Store HTTP client immediately - WebSocket client will be added later + guard.clients = Some(Clients { + http_client, + ws_client: None, + }); + info!("Initialized HTTP client"); + + // Clone app_config for async task + let app_config = guard.app_config.clone(); + + // Drop the write lock before spawning async task + drop(guard); + + // Initialize WebSocket client in a blocking task to avoid runtime conflicts + async_runtime::spawn(async move { + let ws_client = async_runtime::spawn_blocking(move || build_ws_client(&app_config)) + .await + .expect("Failed to initialize WebSocket client"); + + let mut guard = lock_w!(FDOLL); + if let Some(clients) = guard.clients.as_mut() { + clients.ws_client = Some(ws_client); + } + info!("Initialized FDOLL state with WebSocket client"); + }); + + info!("Initialized FDOLL state (WebSocket client initializing asynchronously)"); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cfd8c07..42402a3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,4 @@ -use crate::services::cursor::channel_cursor_positions; +use crate::services::cursor::start_cursor_tracking; use tauri::async_runtime; use tracing_subscriber; @@ -48,7 +48,7 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_positioner::init()) .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![channel_cursor_positions]) + .invoke_handler(tauri::generate_handler![start_cursor_tracking]) .setup(|app| { APP_HANDLE .set(app.handle().to_owned()) diff --git a/src-tauri/src/services/cursor.rs b/src-tauri/src/services/cursor.rs index faa5116..92bb719 100644 --- a/src-tauri/src/services/cursor.rs +++ b/src-tauri/src/services/cursor.rs @@ -1,7 +1,11 @@ use device_query::{DeviceEvents, DeviceEventsHandler}; +use once_cell::sync::OnceCell; use serde::Serialize; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; use std::time::Duration; -use tauri::ipc::Channel; +use tauri::Emitter; +use tracing::{error, info, warn}; use crate::get_app_handle; @@ -19,6 +23,8 @@ pub struct CursorPositions { pub mapped: CursorPosition, } +static CURSOR_TRACKER: OnceCell<()> = OnceCell::new(); + fn map_to_grid( pos: &CursorPosition, grid_size: i32, @@ -31,17 +37,90 @@ fn map_to_grid( } } +/// 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 channel_cursor_positions(on_event: Channel) { +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..."); let app_handle = get_app_handle(); - let primary_monitor = app_handle.primary_monitor().unwrap().unwrap(); + + // Get primary monitor with retries + let primary_monitor = { + let mut retry_count = 0; + let max_retries = 3; + loop { + match app_handle.primary_monitor() { + Ok(Some(monitor)) => { + info!("Primary monitor acquired"); + break monitor; + } + Ok(None) => { + retry_count += 1; + if retry_count >= max_retries { + return Err(format!( + "No primary monitor found after {} retries", + max_retries + )); + } + warn!( + "Primary monitor not available, retrying... ({}/{})", + retry_count, max_retries + ); + tokio::time::sleep(Duration::from_millis(100)).await; + } + Err(e) => { + retry_count += 1; + if retry_count >= max_retries { + return Err(format!("Failed to get primary monitor: {}", e)); + } + warn!( + "Error getting primary monitor, retrying... ({}/{}): {}", + retry_count, max_retries, e + ); + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + } + }; + let monitor_dimensions = primary_monitor.size(); let logical_monitor_dimensions: tauri::LogicalSize = monitor_dimensions.to_logical(primary_monitor.scale_factor()); - let device_state = - DeviceEventsHandler::new(Duration::from_millis(200)).expect("Failed to start event loop"); - let _guard = device_state.on_mouse_move(move |position| { + info!( + "Monitor dimensions: {}x{}", + logical_monitor_dimensions.width, logical_monitor_dimensions.height + ); + + // Try to initialize the device event handler + 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..."); + + let send_count = Arc::new(AtomicU64::new(0)); + let send_count_clone = Arc::clone(&send_count); + let app_handle_clone = app_handle.clone(); + + let _guard = device_state.on_mouse_move(move |position: &(i32, i32)| { let raw = CursorPosition { x: position.0, y: position.1, @@ -52,13 +131,36 @@ pub async fn channel_cursor_positions(on_event: Channel) { logical_monitor_dimensions.width, logical_monitor_dimensions.height, ); - let positions = CursorPositions { raw, mapped }; - let _ = on_event.send(positions); + let positions = CursorPositions { + raw, + mapped: mapped.clone(), + }; + + // Report to server (existing functionality) + let mapped_for_ws = mapped.clone(); + tauri::async_runtime::spawn(async move { + crate::core::services::ws::report_cursor_data(mapped_for_ws).await; + }); + + // Broadcast to ALL windows using events + match app_handle_clone.emit("cursor-position", &positions) { + Ok(_) => { + let count = send_count_clone.fetch_add(1, Ordering::Relaxed) + 1; + if count % 100 == 0 { + info!("Broadcast {} cursor position updates to all windows. Latest: raw({}, {}), mapped({}, {})", + count, positions.raw.x, positions.raw.y, positions.mapped.x, positions.mapped.y); + } + } + Err(e) => { + error!("Failed to emit cursor position event: {:?}", e); + } + } }); + info!("Mouse move handler registered - now broadcasting cursor events to all windows"); + + // Keep the handler alive forever loop { - // for whatever reason this sleep is not taking effect but it - // does reduce CPU usage on my Mac from 100% to 6% so...cool! - tokio::time::sleep(Duration::from_millis(1000)).await + tokio::time::sleep(Duration::from_secs(3600)).await; } } diff --git a/src/channels/cursor.ts b/src/channels/cursor.ts deleted file mode 100644 index 50ccc25..0000000 --- a/src/channels/cursor.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Channel, invoke } from "@tauri-apps/api/core"; -import { writable } from "svelte/store"; - -export type CursorPositions = { - raw: { x: number; y: number }; - mapped: { x: number; y: number }; -}; -export let cursorPositionOnScreen = writable({ - raw: { x: 0, y: 0 }, - mapped: { x: 0, y: 0 }, -}); - -export function initChannelCursorPosition() { - const channel = new Channel(); - channel.onmessage = (pos) => { - cursorPositionOnScreen.set(pos); - }; - invoke("channel_cursor_positions", { onEvent: channel }); -} diff --git a/src/events/cursor.ts b/src/events/cursor.ts new file mode 100644 index 0000000..bce4904 --- /dev/null +++ b/src/events/cursor.ts @@ -0,0 +1,61 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; +import { writable } from "svelte/store"; + +export type CursorPositions = { + raw: { x: number; y: number }; + mapped: { x: number; y: number }; +}; + +export let cursorPositionOnScreen = writable({ + raw: { x: 0, y: 0 }, + mapped: { x: 0, y: 0 }, +}); + +let unlisten: UnlistenFn | null = null; +let isListening = false; + +/** + * Initialize cursor tracking for this window. + * Can be called from multiple windows - only the first call starts tracking on the Rust side, + * but all windows can independently listen to the broadcast events. + */ +export async function initCursorTracking() { + if (isListening) { + return; + } + + try { + // Start tracking + await invoke("start_cursor_tracking"); + + // Listen to cursor position events (each window subscribes independently) + unlisten = await listen("cursor-position", (event) => { + cursorPositionOnScreen.set(event.payload); + }); + + isListening = true; + } catch (err) { + console.error("[Cursor] Failed to initialize cursor tracking:", err); + throw err; + } +} + +/** + * Stop listening to cursor events in this window. + * Note: This doesn't stop the Rust-side tracking, just stops this window from receiving events. + */ +export function stopCursorTracking() { + if (unlisten) { + unlisten(); + unlisten = null; + isListening = false; + } +} + +// Handle HMR (Hot Module Replacement) cleanup +if (import.meta.hot) { + import.meta.hot.dispose(() => { + stopCursorTracking(); + }); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 73f0b68..581aa8a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,23 @@
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index b0c4d63..1ab31d7 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -4,6 +4,3 @@ // See: https://v2.tauri.app/start/frontend/sveltekit/ for more info export const ssr = false; import "../app.css"; -import { initChannelCursorPosition } from "../channels/cursor"; - -initChannelCursorPosition(); diff --git a/src/routes/preferences/+page.svelte b/src/routes/preferences/+page.svelte new file mode 100644 index 0000000..7b1418e --- /dev/null +++ b/src/routes/preferences/+page.svelte @@ -0,0 +1,28 @@ + + +
+
+
+

Preferences

+

Settings and configuration

+
+ +
+

Cursor Position (Multi-Window Test)

+
+ + Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw.y}) + + + Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen.mapped.y}) + +
+
+
+
diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 8b2eeae..a50fac2 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -1,5 +1,7 @@
@@ -9,13 +11,12 @@

Friendolls

Scene Screen

-
- - Cursor: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw - .y}) +
+ + Raw: ({$cursorPositionOnScreen.raw.x}, {$cursorPositionOnScreen.raw.y}) - - Cursor: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen + + Mapped: ({$cursorPositionOnScreen.mapped.x}, {$cursorPositionOnScreen .mapped.y})