use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; use device_query::{DeviceQuery, DeviceState, Keycode}; use once_cell::sync::OnceCell; use tauri::{Emitter, Manager}; use tauri_plugin_positioner::WindowExt; use tracing::{error, info, warn}; use crate::get_app_handle; pub static SCENE_WINDOW_LABEL: &str = "scene"; pub static SPLASH_WINDOW_LABEL: &str = "splash"; static SCENE_INTERACTIVE_STATE: OnceCell> = OnceCell::new(); static MODIFIER_LISTENER_INIT: OnceCell<()> = OnceCell::new(); fn scene_interactive_state() -> Arc { SCENE_INTERACTIVE_STATE .get_or_init(|| Arc::new(AtomicBool::new(false))) .clone() } fn update_scene_interactive(interactive: bool) { let app_handle = get_app_handle(); if let Some(window) = app_handle.get_window(SCENE_WINDOW_LABEL) { if let Err(e) = window.set_ignore_cursor_events(!interactive) { error!("Failed to toggle scene cursor events: {}", e); } if let Err(e) = window.emit("scene-interactive", &interactive) { error!("Failed to emit scene interactive event: {}", e); } } else { warn!("Scene window not available for interactive update"); } } #[cfg(target_os = "macos")] #[link(name = "ApplicationServices", kind = "framework")] extern "C" { fn AXIsProcessTrusted() -> bool; } fn start_scene_modifier_listener() { MODIFIER_LISTENER_INIT.get_or_init(|| { let state = scene_interactive_state(); update_scene_interactive(false); let app_handle = get_app_handle().clone(); #[cfg(target_os = "macos")] unsafe { info!("Accessibility status: {}", AXIsProcessTrusted()); if !AXIsProcessTrusted() { // Warning only - polling might work without explicit permissions for just key state in some contexts, // or we just want to avoid the crash. We'll show the dialog but not return early if we want to try anyway. // However, usually global key monitoring requires it. // Let's show the dialog but NOT return, to try polling. // Or better, let's keep the return if we think it won't work at all, // but since the crash was the main issue, let's try to proceed safely. // For now, I will keep the dialog and the return to encourage users to enable it, // as it's likely needed for global input monitoring. // On second thought, let's keep the return to be safe and clear to the user. error!("Accessibility permissions not granted. Global modifier listener will NOT start."); use tauri_plugin_dialog::DialogExt; use tauri_plugin_dialog::MessageDialogBuilder; use tauri_plugin_dialog::MessageDialogKind; MessageDialogBuilder::new( app_handle.dialog().clone(), "Missing Permissions", "Friendolls needs Accessibility permissions to detect the Alt key for interactivity. Please grant permissions in System Settings -> Privacy & Security -> Accessibility and restart the app.", ) .kind(MessageDialogKind::Warning) .show(|_| {}); return; } } // Spawn a thread for polling key state thread::spawn(move || { let device_state = DeviceState::new(); let mut last_interactive = false; loop { let keys = device_state.get_keys(); // Check for Alt key (Option on Mac) let interactive = (keys.contains(&Keycode::LAlt) || keys.contains(&Keycode::RAlt)) || keys.contains(&Keycode::Command); if interactive != last_interactive { // State changed info!("Key down state chanegd!"); let previous = state.swap(interactive, Ordering::SeqCst); if previous != interactive { if let Some(window) = app_handle.get_window(SCENE_WINDOW_LABEL) { if let Err(err) = window.set_ignore_cursor_events(!interactive) { error!("Failed to toggle scene cursor events: {}", err); } if let Err(err) = window.emit("scene-interactive", &interactive) { error!("Failed to emit scene interactive event: {}", err); } } else { warn!("Scene window not available for interactive update"); } } last_interactive = interactive; } // Poll every 100ms thread::sleep(std::time::Duration::from_millis(100)); } }); }); } pub fn overlay_fullscreen(window: &tauri::Window) -> Result<(), tauri::Error> { // Get the primary monitor let monitor = get_app_handle().primary_monitor()?.unwrap(); // Get the work area (usable space, excluding menu bar/dock/notch) let work_area = monitor.work_area(); // Set window position to top-left of the work area window.set_position(tauri::PhysicalPosition { x: work_area.position.x, y: work_area.position.y, })?; // Set window size to match work area size window.set_size(tauri::PhysicalSize { width: work_area.size.width, height: work_area.size.height, })?; Ok(()) } pub fn open_splash_window() { let app_handle = get_app_handle(); let existing_webview_window = app_handle.get_window(SPLASH_WINDOW_LABEL); if let Some(window) = existing_webview_window { window.show().unwrap(); return; } info!("Starting splash window creation..."); let webview_window = match tauri::WebviewWindowBuilder::new( app_handle, SPLASH_WINDOW_LABEL, tauri::WebviewUrl::App("/splash".into()), ) .title("Friendolls Splash") .inner_size(600.0, 300.0) .resizable(false) .decorations(false) .transparent(true) .shadow(false) .visible(false) // Show it after centering .skip_taskbar(true) .always_on_top(true) .build() { Ok(window) => { info!("Splash window builder succeeded"); window } Err(e) => { error!("Failed to build splash window: {}", e); return; } }; if let Err(e) = webview_window.move_window(tauri_plugin_positioner::Position::Center) { error!("Failed to move splash window to center: {}", e); // Continue anyway } if let Err(e) = webview_window.show() { error!("Failed to show splash window: {}", e); } info!("Splash window initialized successfully."); } pub fn close_splash_window() { let app_handle = get_app_handle(); if let Some(window) = app_handle.get_window(SPLASH_WINDOW_LABEL) { if let Err(e) = window.close() { error!("Failed to close splash window: {}", e); } else { info!("Splash window closed"); } } } pub fn open_scene_window() { let app_handle = get_app_handle(); let existing_webview_window = app_handle.get_window(SCENE_WINDOW_LABEL); if let Some(window) = existing_webview_window { window.show().unwrap(); return; } info!("Starting scene creation..."); let webview_window = match tauri::WebviewWindowBuilder::new( app_handle, SCENE_WINDOW_LABEL, tauri::WebviewUrl::App("/scene".into()), ) .title("Friendolls Scene") .inner_size(600.0, 500.0) .resizable(false) .decorations(false) .transparent(true) .shadow(false) .visible(true) .skip_taskbar(true) .always_on_top(true) .visible_on_all_workspaces(true) .build() { Ok(window) => { info!("Scene window builder succeeded"); window } Err(e) => { error!("Failed to build scene window: {}", e); return; } }; if let Err(e) = webview_window.move_window(tauri_plugin_positioner::Position::Center) { error!("Failed to move scene window to center: {}", e); return; } let window = match get_app_handle().get_window(webview_window.label()) { Some(window) => window, None => { error!("Failed to get scene window after creation"); return; } }; if let Err(e) = overlay_fullscreen(&window) { error!("Failed to set overlay fullscreen: {}", e); return; } if let Err(e) = window.set_ignore_cursor_events(true) { error!("Failed to set ignore cursor events: {}", e); return; } // Start global modifier listener once scene window exists start_scene_modifier_listener(); #[cfg(debug_assertions)] webview_window.open_devtools(); info!("Scene window initialized successfully."); }