diff --git a/src-tauri/src/init/mod.rs b/src-tauri/src/init/mod.rs index 1e3446e..6d29ee7 100644 --- a/src-tauri/src/init/mod.rs +++ b/src-tauri/src/init/mod.rs @@ -11,7 +11,6 @@ use crate::{ }, state::init_app_state, system_tray::init_system_tray, - utilities::toggle_macos_accessory_mode, }; pub mod lifecycle; @@ -21,7 +20,6 @@ pub mod tracing; /// init and startup of everything. pub async fn launch_app() { init_logging(); - toggle_macos_accessory_mode(false); // TODO: toggle true once figure out consolidated window management solution open_splash_window(); update_app().await; init_app_state(); diff --git a/src-tauri/src/services/app_menu.rs b/src-tauri/src/services/app_menu.rs index be6a57a..9ed87a3 100644 --- a/src-tauri/src/services/app_menu.rs +++ b/src-tauri/src/services/app_menu.rs @@ -1,41 +1,29 @@ -use tauri::Manager; -use tracing::{error, info}; +use tracing::error; -use crate::get_app_handle; +use crate::services::window_manager::{ + ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig, +}; pub static APP_MENU_WINDOW_LABEL: &str = "app_menu"; pub fn open_app_menu_window() { - let app_handle = get_app_handle(); - let existing_webview_window = app_handle.get_window(APP_MENU_WINDOW_LABEL); + let mut config = WindowConfig::regular_ui(APP_MENU_WINDOW_LABEL, "/app-menu", "Friendolls"); + config.width = 400.0; + config.height = 550.0; + config.resizable = true; - if let Some(window) = existing_webview_window { - window.show().unwrap(); - return; - } - - match tauri::WebviewWindowBuilder::new( - app_handle, - APP_MENU_WINDOW_LABEL, - tauri::WebviewUrl::App("/app-menu".into()), - ) - .title("Friendolls") - .inner_size(400.0, 550.0) - .resizable(true) - .maximizable(false) - .decorations(true) - .transparent(false) - .shadow(true) - .visible(true) - .skip_taskbar(false) - .always_on_top(false) - .visible_on_all_workspaces(false) - .build() - { - Ok(_) => { - info!("{} window builder succeeded", APP_MENU_WINDOW_LABEL); + match ensure_window(&config, true, false) { + Ok(EnsureWindowResult::Created(_)) => {} + Ok(EnsureWindowResult::Existing(_)) => {} + Err(EnsureWindowError::MissingParent(parent_label)) => { + error!( + "Failed to build {} window due to missing parent '{}': impossible state", + APP_MENU_WINDOW_LABEL, parent_label + ); } - Err(e) => { + Err(EnsureWindowError::ShowExisting(e)) + | Err(EnsureWindowError::SetParent(e)) + | Err(EnsureWindowError::Build(e)) => { error!("Failed to build {} window: {}", APP_MENU_WINDOW_LABEL, e); } } diff --git a/src-tauri/src/services/client_config/mod.rs b/src-tauri/src/services/client_config/mod.rs index 1df85dd..099dd4e 100644 --- a/src-tauri/src/services/client_config/mod.rs +++ b/src-tauri/src/services/client_config/mod.rs @@ -28,6 +28,8 @@ pub enum ClientConfigError { Window(tauri::Error), #[error("failed to show client config window: {0}")] ShowWindow(tauri::Error), + #[error("missing required parent window: {0}")] + MissingParent(String), } pub static CLIENT_CONFIG_WINDOW_LABEL: &str = "client_config"; diff --git a/src-tauri/src/services/client_config/window.rs b/src-tauri/src/services/client_config/window.rs index 6a65b73..44073f3 100644 --- a/src-tauri/src/services/client_config/window.rs +++ b/src-tauri/src/services/client_config/window.rs @@ -1,39 +1,24 @@ -use tauri::Manager; use tracing::error; -use crate::get_app_handle; +use crate::services::window_manager::{ + ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig, +}; use super::{ClientConfigError, CLIENT_CONFIG_WINDOW_LABEL}; #[tauri::command] pub fn open_config_window() -> Result<(), ClientConfigError> { - let app_handle = get_app_handle(); - let existing_webview_window = app_handle.get_window(CLIENT_CONFIG_WINDOW_LABEL); - - if let Some(window) = existing_webview_window { - if let Err(e) = window.show() { - error!("Failed to show client config window: {e}"); - return Err(ClientConfigError::ShowWindow(e)); - } - if let Err(e) = window.set_focus() { - error!("Failed to focus client config window: {e}"); - } - return Ok(()); - } - - match tauri::WebviewWindowBuilder::new( - app_handle, + let mut config = WindowConfig::regular_ui( CLIENT_CONFIG_WINDOW_LABEL, - tauri::WebviewUrl::App("/client-config".into()), - ) - .title("Advanced Configuration") - .inner_size(300.0, 420.0) - .resizable(false) - .maximizable(false) - .visible(false) - .build() - { - Ok(window) => { + "/client-config", + "Advanced Configuration", + ); + config.width = 300.0; + config.height = 420.0; + config.visible = false; + + match ensure_window(&config, true, true) { + Ok(EnsureWindowResult::Created(window)) => { if let Err(e) = window.show() { error!("Failed to show client config window: {}", e); return Err(ClientConfigError::ShowWindow(e)); @@ -43,7 +28,23 @@ pub fn open_config_window() -> Result<(), ClientConfigError> { } Ok(()) } - Err(e) => { + Ok(EnsureWindowResult::Existing(_)) => Ok(()), + Err(EnsureWindowError::MissingParent(parent_label)) => { + error!( + "Missing parent '{}' for client config window: impossible state", + parent_label + ); + Err(ClientConfigError::MissingParent(parent_label)) + } + Err(EnsureWindowError::ShowExisting(e)) => { + error!("Failed to show client config window: {e}"); + Err(ClientConfigError::ShowWindow(e)) + } + Err(EnsureWindowError::SetParent(e)) => { + error!("Failed to set parent for client config window: {}", e); + Err(ClientConfigError::Window(e)) + } + Err(EnsureWindowError::Build(e)) => { error!("Failed to build client config window: {}", e); Err(ClientConfigError::Window(e)) } diff --git a/src-tauri/src/services/doll_editor.rs b/src-tauri/src/services/doll_editor.rs index 95249b4..409a860 100644 --- a/src-tauri/src/services/doll_editor.rs +++ b/src-tauri/src/services/doll_editor.rs @@ -7,6 +7,9 @@ use tracing::{error, info}; use crate::{ get_app_handle, services::app_events::{CreateDoll, EditDoll, SetInteractionOverlay}, + services::window_manager::{ + encode_query_value, ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig, + }, }; static APP_MENU_WINDOW_LABEL: &str = "app_menu"; @@ -71,55 +74,14 @@ pub async fn open_doll_editor_window(doll_id: Option) { }; // Check if the window already exists - let existing_window = app_handle.get_webview_window(&window_label); - if let Some(window) = existing_window { - // If it exists, we might want to reload it with new params or just focus it - if let Err(e) = window.set_focus() { - error!("Failed to focus existing doll editor window: {}", e); - } - - // Ensure overlay is active on parent (redundancy for safety) - #[cfg(target_os = "macos")] - if let Some(parent) = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL) { - if let Err(e) = SetInteractionOverlay(true).emit(&parent) { - error!("Failed to ensure interaction overlay on parent: {}", e); - } - } - - // Emit event to update context - if let Some(id) = doll_id { - if let Err(e) = EditDoll(id).emit(&window) { - error!("Failed to emit edit-doll event: {}", e); - } - } else if let Err(e) = CreateDoll.emit(&window) { - error!("Failed to emit create-doll event: {}", e); - } - - return; - } - - let url_path = if let Some(id) = doll_id { - format!("/doll-editor?id={}", id) + let url_path = if let Some(ref id) = doll_id { + format!("/doll-editor?id={}", encode_query_value(id)) } else { "/doll-editor".to_string() }; - let mut builder = tauri::WebviewWindowBuilder::new( - app_handle, - &window_label, - tauri::WebviewUrl::App(url_path.into()), - ) - .title("Doll Editor") - .inner_size(300.0, 400.0) - .resizable(false) - .maximizable(false) - .decorations(true) - .transparent(false) - .shadow(true) - .visible(true) - .skip_taskbar(false) - .always_on_top(true) // Helper window, nice to stay on top - .visible_on_all_workspaces(false); + let has_existing_window = app_handle.get_webview_window(&window_label).is_some(); + let parent_window = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL); // Set parent if app menu exists // Also disable interaction with parent while child is open @@ -129,10 +91,11 @@ pub async fn open_doll_editor_window(doll_id: Option) { let mut parent_focus_listener_id: Option = None; - if let Some(parent) = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL) { + if !has_existing_window { + if let Some(parent) = &parent_window { // 1. Disable parent interaction immediately (Windows only) #[cfg(target_os = "windows")] - set_window_interaction(&parent, false); + set_window_interaction(parent, false); // 2. Setup Focus Trap (macOS only) #[cfg(target_os = "macos")] @@ -141,7 +104,7 @@ pub async fn open_doll_editor_window(doll_id: Option) { let app_handle_clone = get_app_handle().clone(); // Emit event to show overlay - if let Err(e) = SetInteractionOverlay(true).emit(&parent) { + if let Err(e) = SetInteractionOverlay(true).emit(parent) { error!("Failed to emit set-interaction-overlay event: {}", e); } @@ -159,32 +122,22 @@ pub async fn open_doll_editor_window(doll_id: Option) { }); parent_focus_listener_id = Some(id); } - - match builder.parent(&parent) { - Ok(b) => builder = b, - Err(e) => { - error!("Failed to set parent for doll editor window: {}", e); - // If we fail, revert changes - #[cfg(target_os = "windows")] - set_window_interaction(&parent, true); - - #[cfg(target_os = "macos")] - { - if let Some(id) = parent_focus_listener_id { - parent.unlisten(id); - } - // Remove overlay if we failed - let _ = SetInteractionOverlay(false).emit(&parent); - } - return; - } - }; + } } - match builder.build() { - Ok(window) => { - info!("{} window builder succeeded", window_label); + let mut config = WindowConfig::regular_ui(window_label.as_str(), url_path, "Doll Editor"); + config.width = 300.0; + config.height = 400.0; + config.always_on_top = true; + config.parent_label = if !has_existing_window && parent_window.is_some() { + Some(APP_MENU_WINDOW_LABEL) + } else { + None + }; + config.require_parent = false; + match ensure_window(&config, true, true) { + Ok(EnsureWindowResult::Created(window)) => { // 3. Setup cleanup hook: When this child window is destroyed, re-enable the parent let app_handle_clone = get_app_handle().clone(); @@ -223,10 +176,35 @@ pub async fn open_doll_editor_window(doll_id: Option) { // #[cfg(debug_assertions)] // window.open_devtools(); } - Err(e) => { + Ok(EnsureWindowResult::Existing(window)) => { + #[cfg(target_os = "macos")] + if let Some(parent) = parent_window { + if let Err(e) = SetInteractionOverlay(true).emit(&parent) { + error!("Failed to ensure interaction overlay on parent: {}", e); + } + } + + if let Some(id) = doll_id { + if let Err(e) = EditDoll(id).emit(&window) { + error!("Failed to emit edit-doll event: {}", e); + } + } else if let Err(e) = CreateDoll.emit(&window) { + error!("Failed to emit create-doll event: {}", e); + } + } + Err(EnsureWindowError::ShowExisting(e)) => { + error!("Failed to show existing {} window: {}", window_label, e); + } + Err(EnsureWindowError::MissingParent(parent_label)) => { + error!( + "Failed to create {} due to missing parent '{}': impossible state", + window_label, parent_label + ); + } + Err(EnsureWindowError::SetParent(e)) | Err(EnsureWindowError::Build(e)) => { error!("Failed to build {} window: {}", window_label, e); // If build failed, revert - if let Some(parent) = get_app_handle().get_webview_window(APP_MENU_WINDOW_LABEL) { + if let Some(parent) = parent_window { #[cfg(target_os = "windows")] set_window_interaction(&parent, true); diff --git a/src-tauri/src/services/health_manager.rs b/src-tauri/src/services/health_manager.rs index 3a1166a..2fbb3ba 100644 --- a/src-tauri/src/services/health_manager.rs +++ b/src-tauri/src/services/health_manager.rs @@ -5,16 +5,38 @@ use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind}; use tauri_plugin_positioner::WindowExt; use tracing::{error, info}; +use super::window_manager::{ + encode_query_value, ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig, +}; + pub static HEALTH_MANAGER_WINDOW_LABEL: &str = "health_manager"; /// Closes primary UI windows and shows the health manager with an optional error message. pub fn open_health_manager_window(error_message: Option) { let app_handle = get_app_handle(); - let existing_webview_window = app_handle.get_window(HEALTH_MANAGER_WINDOW_LABEL); + info!("Building health manager window"); + let mut config = WindowConfig::regular_ui( + HEALTH_MANAGER_WINDOW_LABEL, + format!( + "/health-manager?err={}", + encode_query_value(error_message.as_deref().unwrap_or("Something went wrong!")) + ), + "Health Manager", + ); + config.visible = false; - if let Some(window) = existing_webview_window { - if let Err(e) = window.show() { + let webview_window = match ensure_window(&config, true, false) { + Ok(EnsureWindowResult::Created(window)) => window, + Ok(EnsureWindowResult::Existing(_)) => return, + Err(EnsureWindowError::MissingParent(parent_label)) => { + error!( + "Failed to build {} window due to missing parent '{}': impossible state", + HEALTH_MANAGER_WINDOW_LABEL, parent_label + ); + return; + } + Err(EnsureWindowError::ShowExisting(e)) => { error!("Failed to show existing health manager window: {}", e); MessageDialogBuilder::new( app_handle.dialog().clone(), @@ -23,39 +45,9 @@ pub fn open_health_manager_window(error_message: Option) { ) .kind(MessageDialogKind::Error) .show(|_| {}); + return; } - return; - } - - info!("Building health manager window"); - let webview_window = match tauri::WebviewWindowBuilder::new( - app_handle, - HEALTH_MANAGER_WINDOW_LABEL, - tauri::WebviewUrl::App( - format!( - "/health-manager?err={}", - error_message.unwrap_or(String::from("Something went wrong!")) - ) - .into(), - ), - ) - .title("Health Manager") - .inner_size(420.0, 420.0) - .resizable(false) - .decorations(true) - .transparent(false) - .shadow(true) - .visible(false) - .skip_taskbar(false) - .always_on_top(false) - .visible_on_all_workspaces(false) - .build() - { - Ok(window) => { - info!("{} window builder succeeded", HEALTH_MANAGER_WINDOW_LABEL); - window - } - Err(e) => { + Err(EnsureWindowError::SetParent(e)) | Err(EnsureWindowError::Build(e)) => { error!( "Failed to build {} window: {}", HEALTH_MANAGER_WINDOW_LABEL, e diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 8940034..5aae99f 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -18,4 +18,5 @@ pub mod session_windows; pub mod sprite; pub mod sprite_recolor; pub mod welcome; +pub mod window_manager; pub mod ws; diff --git a/src-tauri/src/services/scene/windows.rs b/src-tauri/src/services/scene/windows.rs index 5f6790f..b83f2d0 100644 --- a/src-tauri/src/services/scene/windows.rs +++ b/src-tauri/src/services/scene/windows.rs @@ -3,6 +3,9 @@ use tauri_plugin_positioner::WindowExt; use tracing::{error, info}; use crate::get_app_handle; +use crate::services::window_manager::{ + ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig, +}; use super::interactivity::start_scene_modifier_listener; @@ -24,36 +27,27 @@ pub fn overlay_fullscreen(window: &tauri::Window) -> Result<(), tauri::Error> { } 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.html".into()), - ) - .title("Friendolls Splash") - .inner_size(800.0, 400.0) - .resizable(false) - .decorations(false) - .transparent(true) - .shadow(false) - .visible(false) - .skip_taskbar(true) - .always_on_top(true) - .build() - { - Ok(window) => { - info!("Splash window builder succeeded"); - window + + let mut config = + WindowConfig::accessory(SPLASH_WINDOW_LABEL, "/splash.html", "Friendolls Splash"); + config.width = 800.0; + config.height = 400.0; + config.visible = false; + + let webview_window = match ensure_window(&config, true, false) { + Ok(EnsureWindowResult::Created(window)) => window, + Ok(EnsureWindowResult::Existing(_)) => return, + Err(EnsureWindowError::MissingParent(parent_label)) => { + error!( + "Failed to build splash window due to missing parent '{}': impossible state", + parent_label + ); + return; } - Err(e) => { + Err(EnsureWindowError::ShowExisting(e)) + | Err(EnsureWindowError::SetParent(e)) + | Err(EnsureWindowError::Build(e)) => { error!("Failed to build splash window: {}", e); return; } @@ -82,37 +76,26 @@ pub fn close_splash_window() { } 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 + + let mut config = WindowConfig::accessory(SCENE_WINDOW_LABEL, "/scene", "Friendolls Scene"); + config.width = 600.0; + config.height = 500.0; + config.visible_on_all_workspaces = true; + + let webview_window = match ensure_window(&config, true, false) { + Ok(EnsureWindowResult::Created(window)) => window, + Ok(EnsureWindowResult::Existing(_)) => return, + Err(EnsureWindowError::MissingParent(parent_label)) => { + error!( + "Failed to build scene window due to missing parent '{}': impossible state", + parent_label + ); + return; } - Err(e) => { + Err(EnsureWindowError::ShowExisting(e)) + | Err(EnsureWindowError::SetParent(e)) + | Err(EnsureWindowError::Build(e)) => { error!("Failed to build scene window: {}", e); return; } diff --git a/src-tauri/src/services/welcome.rs b/src-tauri/src/services/welcome.rs index 7876bd5..666c43f 100644 --- a/src-tauri/src/services/welcome.rs +++ b/src-tauri/src/services/welcome.rs @@ -4,14 +4,28 @@ use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind}; use tauri_plugin_positioner::WindowExt; use tracing::{error, info}; +use super::window_manager::{ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig}; + pub static WELCOME_WINDOW_LABEL: &str = "welcome"; pub fn open_welcome_window() { let app_handle = get_app_handle(); - let existing_webview_window = app_handle.get_window(WELCOME_WINDOW_LABEL); - if let Some(window) = existing_webview_window { - if let Err(e) = window.show() { + let mut config = + WindowConfig::regular_ui(WELCOME_WINDOW_LABEL, "/welcome", "Welcome to Friendolls"); + config.visible = false; + + let webview_window = match ensure_window(&config, true, false) { + Ok(EnsureWindowResult::Created(window)) => window, + Ok(EnsureWindowResult::Existing(_)) => return, + Err(EnsureWindowError::MissingParent(parent_label)) => { + error!( + "Failed to build {} window due to missing parent '{}': impossible state", + WELCOME_WINDOW_LABEL, parent_label + ); + return; + } + Err(EnsureWindowError::ShowExisting(e)) => { error!("Failed to show existing welcome window: {}", e); MessageDialogBuilder::new( app_handle.dialog().clone(), @@ -20,33 +34,9 @@ pub fn open_welcome_window() { ) .kind(MessageDialogKind::Error) .show(|_| {}); + return; } - return; - } - - let webview_window = match tauri::WebviewWindowBuilder::new( - app_handle, - WELCOME_WINDOW_LABEL, - tauri::WebviewUrl::App("/welcome".into()), - ) - .title("Welcome to Friendolls") - .inner_size(420.0, 420.0) - .resizable(false) - .maximizable(false) - .decorations(true) - .transparent(false) - .shadow(true) - .visible(false) - .skip_taskbar(false) - .always_on_top(false) - .visible_on_all_workspaces(false) - .build() - { - Ok(window) => { - info!("{} window builder succeeded", WELCOME_WINDOW_LABEL); - window - } - Err(e) => { + Err(EnsureWindowError::SetParent(e)) | Err(EnsureWindowError::Build(e)) => { error!("Failed to build {} window: {}", WELCOME_WINDOW_LABEL, e); return; } diff --git a/src-tauri/src/services/window_manager.rs b/src-tauri/src/services/window_manager.rs new file mode 100644 index 0000000..c112efb --- /dev/null +++ b/src-tauri/src/services/window_manager.rs @@ -0,0 +1,256 @@ +use std::{collections::HashMap, sync::Mutex}; + +use tauri::{Manager, WebviewUrl, WebviewWindow, WindowEvent}; +use tracing::{error, info}; +use url::form_urlencoded; + +use crate::{get_app_handle, utilities::toggle_macos_accessory_mode}; + +static WINDOW_KINDS: std::sync::OnceLock>> = + std::sync::OnceLock::new(); + +fn window_kinds() -> &'static Mutex> { + WINDOW_KINDS.get_or_init(|| Mutex::new(HashMap::new())) +} + +#[derive(Clone, Copy)] +pub enum WindowKind { + RegularUi, + Accessory, +} + +pub struct WindowConfig<'a> { + pub label: &'a str, + pub url_path: String, + pub title: &'a str, + pub width: f64, + pub height: f64, + pub resizable: bool, + pub maximizable: bool, + pub decorations: bool, + pub transparent: bool, + pub shadow: bool, + pub visible: bool, + pub skip_taskbar: bool, + pub always_on_top: bool, + pub visible_on_all_workspaces: bool, + pub parent_label: Option<&'a str>, + pub require_parent: bool, + pub kind: WindowKind, +} + +impl<'a> WindowConfig<'a> { + pub fn regular_ui(label: &'a str, url_path: impl Into, title: &'a str) -> Self { + Self { + label, + url_path: url_path.into(), + title, + width: 420.0, + height: 420.0, + resizable: false, + maximizable: false, + decorations: true, + transparent: false, + shadow: true, + visible: true, + skip_taskbar: false, + always_on_top: false, + visible_on_all_workspaces: false, + parent_label: None, + require_parent: false, + kind: WindowKind::RegularUi, + } + } + + pub fn accessory(label: &'a str, url_path: impl Into, title: &'a str) -> Self { + Self { + label, + url_path: url_path.into(), + title, + width: 600.0, + height: 500.0, + resizable: false, + maximizable: false, + decorations: false, + transparent: true, + shadow: false, + visible: true, + skip_taskbar: true, + always_on_top: true, + visible_on_all_workspaces: false, + parent_label: None, + require_parent: false, + kind: WindowKind::Accessory, + } + } +} + +pub enum EnsureWindowResult { + Existing(WebviewWindow), + Created(WebviewWindow), +} + +pub enum EnsureWindowError { + ShowExisting(tauri::Error), + MissingParent(String), + SetParent(tauri::Error), + Build(tauri::Error), +} + +fn apply_existing_window_behavior( + window: &WebviewWindow, + label: &str, + show_existing: bool, + focus_existing: bool, +) -> Result<(), EnsureWindowError> { + if show_existing { + if let Err(e) = window.show() { + return Err(EnsureWindowError::ShowExisting(e)); + } + } + + if focus_existing { + if let Err(e) = window.set_focus() { + error!("Failed to focus existing '{}' window: {}", label, e); + } + } + + Ok(()) +} + +pub fn encode_query_value(value: &str) -> String { + let mut serializer = form_urlencoded::Serializer::new(String::new()); + serializer.append_pair("v", value); + let encoded = serializer.finish(); + encoded + .strip_prefix("v=") + .unwrap_or(encoded.as_str()) + .to_string() +} + +pub fn ensure_window( + config: &WindowConfig, + show_existing: bool, + focus_existing: bool, +) -> Result { + let app_handle = get_app_handle(); + + if let Some(window) = app_handle.get_webview_window(config.label) { + apply_existing_window_behavior(&window, config.label, show_existing, focus_existing)?; + + if let Ok(mut guard) = window_kinds().lock() { + guard.insert(config.label.to_string(), config.kind); + } + + sync_macos_accessory_mode_for_current_windows(); + return Ok(EnsureWindowResult::Existing(window)); + } + + let mut builder = tauri::WebviewWindowBuilder::new( + app_handle, + config.label, + WebviewUrl::App(config.url_path.clone().into()), + ) + .title(config.title) + .inner_size(config.width, config.height) + .resizable(config.resizable) + .decorations(config.decorations) + .transparent(config.transparent) + .shadow(config.shadow) + .visible(config.visible) + .skip_taskbar(config.skip_taskbar) + .always_on_top(config.always_on_top) + .visible_on_all_workspaces(config.visible_on_all_workspaces); + + builder = builder.maximizable(config.maximizable); + + if let Some(parent_label) = config.parent_label { + if let Some(parent) = app_handle.get_webview_window(parent_label) { + builder = builder + .parent(&parent) + .map_err(EnsureWindowError::SetParent)?; + } else if config.require_parent { + return Err(EnsureWindowError::MissingParent(parent_label.to_string())); + } + } + + match builder.build() { + Ok(window) => { + info!("{} window builder succeeded", config.label); + if let Ok(mut guard) = window_kinds().lock() { + guard.insert(config.label.to_string(), config.kind); + } + attach_macos_accessory_mode_listener(&window); + sync_macos_accessory_mode_for_current_windows(); + Ok(EnsureWindowResult::Created(window)) + } + Err(e) => { + if let Some(window) = app_handle.get_webview_window(config.label) { + apply_existing_window_behavior( + &window, + config.label, + show_existing, + focus_existing, + )?; + + if let Ok(mut guard) = window_kinds().lock() { + guard.insert(config.label.to_string(), config.kind); + } + + sync_macos_accessory_mode_for_current_windows(); + return Ok(EnsureWindowResult::Existing(window)); + } + + Err(EnsureWindowError::Build(e)) + } + } +} + +pub fn attach_macos_accessory_mode_listener(window: &WebviewWindow) { + let label = window.label().to_string(); + window.on_window_event(move |event| { + if let WindowEvent::Destroyed = event { + if let Ok(mut guard) = window_kinds().lock() { + guard.remove(&label); + } + info!( + "Window '{}' destroyed, syncing macOS activation policy", + label + ); + sync_macos_accessory_mode_for_current_windows(); + } + }); +} + +pub fn sync_macos_accessory_mode_for_current_windows() { + #[cfg(target_os = "macos")] + { + let app_handle = get_app_handle(); + let has_regular_ui_window = if let Ok(guard) = window_kinds().lock() { + guard.iter().any(|(label, kind)| { + if !matches!(kind, WindowKind::RegularUi) { + return false; + } + + if let Some(window) = app_handle.get_webview_window(label.as_str()) { + return window.is_visible().unwrap_or(false); + } + + false + }) + } else { + false + }; + + info!( + "Syncing macOS accessory mode: has_regular_ui_window={}, policy={}", + has_regular_ui_window, + if has_regular_ui_window { + "Regular" + } else { + "Accessory" + } + ); + toggle_macos_accessory_mode(!has_regular_ui_window); + } +}