Refactored window management solution & implemented macOS accessory mode
This commit is contained in:
@@ -11,7 +11,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
state::init_app_state,
|
state::init_app_state,
|
||||||
system_tray::init_system_tray,
|
system_tray::init_system_tray,
|
||||||
utilities::toggle_macos_accessory_mode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod lifecycle;
|
pub mod lifecycle;
|
||||||
@@ -21,7 +20,6 @@ pub mod tracing;
|
|||||||
/// init and startup of everything.
|
/// init and startup of everything.
|
||||||
pub async fn launch_app() {
|
pub async fn launch_app() {
|
||||||
init_logging();
|
init_logging();
|
||||||
toggle_macos_accessory_mode(false); // TODO: toggle true once figure out consolidated window management solution
|
|
||||||
open_splash_window();
|
open_splash_window();
|
||||||
update_app().await;
|
update_app().await;
|
||||||
init_app_state();
|
init_app_state();
|
||||||
|
|||||||
@@ -1,41 +1,29 @@
|
|||||||
use tauri::Manager;
|
use tracing::error;
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
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 static APP_MENU_WINDOW_LABEL: &str = "app_menu";
|
||||||
|
|
||||||
pub fn open_app_menu_window() {
|
pub fn open_app_menu_window() {
|
||||||
let app_handle = get_app_handle();
|
let mut config = WindowConfig::regular_ui(APP_MENU_WINDOW_LABEL, "/app-menu", "Friendolls");
|
||||||
let existing_webview_window = app_handle.get_window(APP_MENU_WINDOW_LABEL);
|
config.width = 400.0;
|
||||||
|
config.height = 550.0;
|
||||||
|
config.resizable = true;
|
||||||
|
|
||||||
if let Some(window) = existing_webview_window {
|
match ensure_window(&config, true, false) {
|
||||||
window.show().unwrap();
|
Ok(EnsureWindowResult::Created(_)) => {}
|
||||||
return;
|
Ok(EnsureWindowResult::Existing(_)) => {}
|
||||||
}
|
Err(EnsureWindowError::MissingParent(parent_label)) => {
|
||||||
|
error!(
|
||||||
match tauri::WebviewWindowBuilder::new(
|
"Failed to build {} window due to missing parent '{}': impossible state",
|
||||||
app_handle,
|
APP_MENU_WINDOW_LABEL, parent_label
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(EnsureWindowError::ShowExisting(e))
|
||||||
|
| Err(EnsureWindowError::SetParent(e))
|
||||||
|
| Err(EnsureWindowError::Build(e)) => {
|
||||||
error!("Failed to build {} window: {}", APP_MENU_WINDOW_LABEL, e);
|
error!("Failed to build {} window: {}", APP_MENU_WINDOW_LABEL, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ pub enum ClientConfigError {
|
|||||||
Window(tauri::Error),
|
Window(tauri::Error),
|
||||||
#[error("failed to show client config window: {0}")]
|
#[error("failed to show client config window: {0}")]
|
||||||
ShowWindow(tauri::Error),
|
ShowWindow(tauri::Error),
|
||||||
|
#[error("missing required parent window: {0}")]
|
||||||
|
MissingParent(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static CLIENT_CONFIG_WINDOW_LABEL: &str = "client_config";
|
pub static CLIENT_CONFIG_WINDOW_LABEL: &str = "client_config";
|
||||||
|
|||||||
@@ -1,39 +1,24 @@
|
|||||||
use tauri::Manager;
|
|
||||||
use tracing::error;
|
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};
|
use super::{ClientConfigError, CLIENT_CONFIG_WINDOW_LABEL};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_config_window() -> Result<(), ClientConfigError> {
|
pub fn open_config_window() -> Result<(), ClientConfigError> {
|
||||||
let app_handle = get_app_handle();
|
let mut config = WindowConfig::regular_ui(
|
||||||
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,
|
|
||||||
CLIENT_CONFIG_WINDOW_LABEL,
|
CLIENT_CONFIG_WINDOW_LABEL,
|
||||||
tauri::WebviewUrl::App("/client-config".into()),
|
"/client-config",
|
||||||
)
|
"Advanced Configuration",
|
||||||
.title("Advanced Configuration")
|
);
|
||||||
.inner_size(300.0, 420.0)
|
config.width = 300.0;
|
||||||
.resizable(false)
|
config.height = 420.0;
|
||||||
.maximizable(false)
|
config.visible = false;
|
||||||
.visible(false)
|
|
||||||
.build()
|
match ensure_window(&config, true, true) {
|
||||||
{
|
Ok(EnsureWindowResult::Created(window)) => {
|
||||||
Ok(window) => {
|
|
||||||
if let Err(e) = window.show() {
|
if let Err(e) = window.show() {
|
||||||
error!("Failed to show client config window: {}", e);
|
error!("Failed to show client config window: {}", e);
|
||||||
return Err(ClientConfigError::ShowWindow(e));
|
return Err(ClientConfigError::ShowWindow(e));
|
||||||
@@ -43,7 +28,23 @@ pub fn open_config_window() -> Result<(), ClientConfigError> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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);
|
error!("Failed to build client config window: {}", e);
|
||||||
Err(ClientConfigError::Window(e))
|
Err(ClientConfigError::Window(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ use tracing::{error, info};
|
|||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle,
|
get_app_handle,
|
||||||
services::app_events::{CreateDoll, EditDoll, SetInteractionOverlay},
|
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";
|
static APP_MENU_WINDOW_LABEL: &str = "app_menu";
|
||||||
@@ -71,55 +74,14 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if the window already exists
|
// Check if the window already exists
|
||||||
let existing_window = app_handle.get_webview_window(&window_label);
|
let url_path = if let Some(ref id) = doll_id {
|
||||||
if let Some(window) = existing_window {
|
format!("/doll-editor?id={}", encode_query_value(id))
|
||||||
// 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)
|
|
||||||
} else {
|
} else {
|
||||||
"/doll-editor".to_string()
|
"/doll-editor".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut builder = tauri::WebviewWindowBuilder::new(
|
let has_existing_window = app_handle.get_webview_window(&window_label).is_some();
|
||||||
app_handle,
|
let parent_window = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL);
|
||||||
&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);
|
|
||||||
|
|
||||||
// Set parent if app menu exists
|
// Set parent if app menu exists
|
||||||
// Also disable interaction with parent while child is open
|
// Also disable interaction with parent while child is open
|
||||||
@@ -129,10 +91,11 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
|
|||||||
|
|
||||||
let mut parent_focus_listener_id: Option<u32> = None;
|
let mut parent_focus_listener_id: Option<u32> = 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)
|
// 1. Disable parent interaction immediately (Windows only)
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
set_window_interaction(&parent, false);
|
set_window_interaction(parent, false);
|
||||||
|
|
||||||
// 2. Setup Focus Trap (macOS only)
|
// 2. Setup Focus Trap (macOS only)
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -141,7 +104,7 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
|
|||||||
let app_handle_clone = get_app_handle().clone();
|
let app_handle_clone = get_app_handle().clone();
|
||||||
|
|
||||||
// Emit event to show overlay
|
// 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);
|
error!("Failed to emit set-interaction-overlay event: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,32 +122,22 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
|
|||||||
});
|
});
|
||||||
parent_focus_listener_id = Some(id);
|
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() {
|
let mut config = WindowConfig::regular_ui(window_label.as_str(), url_path, "Doll Editor");
|
||||||
Ok(window) => {
|
config.width = 300.0;
|
||||||
info!("{} window builder succeeded", window_label);
|
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
|
// 3. Setup cleanup hook: When this child window is destroyed, re-enable the parent
|
||||||
let app_handle_clone = get_app_handle().clone();
|
let app_handle_clone = get_app_handle().clone();
|
||||||
|
|
||||||
@@ -223,10 +176,35 @@ pub async fn open_doll_editor_window(doll_id: Option<String>) {
|
|||||||
// #[cfg(debug_assertions)]
|
// #[cfg(debug_assertions)]
|
||||||
// window.open_devtools();
|
// 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);
|
error!("Failed to build {} window: {}", window_label, e);
|
||||||
// If build failed, revert
|
// 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")]
|
#[cfg(target_os = "windows")]
|
||||||
set_window_interaction(&parent, true);
|
set_window_interaction(&parent, true);
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,38 @@ use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind};
|
|||||||
use tauri_plugin_positioner::WindowExt;
|
use tauri_plugin_positioner::WindowExt;
|
||||||
use tracing::{error, info};
|
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";
|
pub static HEALTH_MANAGER_WINDOW_LABEL: &str = "health_manager";
|
||||||
|
|
||||||
/// Closes primary UI windows and shows the health manager with an optional error message.
|
/// Closes primary UI windows and shows the health manager with an optional error message.
|
||||||
pub fn open_health_manager_window(error_message: Option<String>) {
|
pub fn open_health_manager_window(error_message: Option<String>) {
|
||||||
let app_handle = get_app_handle();
|
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 {
|
let webview_window = match ensure_window(&config, true, false) {
|
||||||
if let Err(e) = window.show() {
|
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);
|
error!("Failed to show existing health manager window: {}", e);
|
||||||
MessageDialogBuilder::new(
|
MessageDialogBuilder::new(
|
||||||
app_handle.dialog().clone(),
|
app_handle.dialog().clone(),
|
||||||
@@ -23,39 +45,9 @@ pub fn open_health_manager_window(error_message: Option<String>) {
|
|||||||
)
|
)
|
||||||
.kind(MessageDialogKind::Error)
|
.kind(MessageDialogKind::Error)
|
||||||
.show(|_| {});
|
.show(|_| {});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
Err(EnsureWindowError::SetParent(e)) | Err(EnsureWindowError::Build(e)) => {
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
error!(
|
error!(
|
||||||
"Failed to build {} window: {}",
|
"Failed to build {} window: {}",
|
||||||
HEALTH_MANAGER_WINDOW_LABEL, e
|
HEALTH_MANAGER_WINDOW_LABEL, e
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ pub mod session_windows;
|
|||||||
pub mod sprite;
|
pub mod sprite;
|
||||||
pub mod sprite_recolor;
|
pub mod sprite_recolor;
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
|
pub mod window_manager;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ use tauri_plugin_positioner::WindowExt;
|
|||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::get_app_handle;
|
use crate::get_app_handle;
|
||||||
|
use crate::services::window_manager::{
|
||||||
|
ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig,
|
||||||
|
};
|
||||||
|
|
||||||
use super::interactivity::start_scene_modifier_listener;
|
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() {
|
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...");
|
info!("Starting splash window creation...");
|
||||||
let webview_window = match tauri::WebviewWindowBuilder::new(
|
|
||||||
app_handle,
|
let mut config =
|
||||||
SPLASH_WINDOW_LABEL,
|
WindowConfig::accessory(SPLASH_WINDOW_LABEL, "/splash.html", "Friendolls Splash");
|
||||||
tauri::WebviewUrl::App("/splash.html".into()),
|
config.width = 800.0;
|
||||||
)
|
config.height = 400.0;
|
||||||
.title("Friendolls Splash")
|
config.visible = false;
|
||||||
.inner_size(800.0, 400.0)
|
|
||||||
.resizable(false)
|
let webview_window = match ensure_window(&config, true, false) {
|
||||||
.decorations(false)
|
Ok(EnsureWindowResult::Created(window)) => window,
|
||||||
.transparent(true)
|
Ok(EnsureWindowResult::Existing(_)) => return,
|
||||||
.shadow(false)
|
Err(EnsureWindowError::MissingParent(parent_label)) => {
|
||||||
.visible(false)
|
error!(
|
||||||
.skip_taskbar(true)
|
"Failed to build splash window due to missing parent '{}': impossible state",
|
||||||
.always_on_top(true)
|
parent_label
|
||||||
.build()
|
);
|
||||||
{
|
return;
|
||||||
Ok(window) => {
|
|
||||||
info!("Splash window builder succeeded");
|
|
||||||
window
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(EnsureWindowError::ShowExisting(e))
|
||||||
|
| Err(EnsureWindowError::SetParent(e))
|
||||||
|
| Err(EnsureWindowError::Build(e)) => {
|
||||||
error!("Failed to build splash window: {}", e);
|
error!("Failed to build splash window: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -82,37 +76,26 @@ pub fn close_splash_window() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_scene_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...");
|
info!("Starting scene creation...");
|
||||||
let webview_window = match tauri::WebviewWindowBuilder::new(
|
|
||||||
app_handle,
|
let mut config = WindowConfig::accessory(SCENE_WINDOW_LABEL, "/scene", "Friendolls Scene");
|
||||||
SCENE_WINDOW_LABEL,
|
config.width = 600.0;
|
||||||
tauri::WebviewUrl::App("/scene".into()),
|
config.height = 500.0;
|
||||||
)
|
config.visible_on_all_workspaces = true;
|
||||||
.title("Friendolls Scene")
|
|
||||||
.inner_size(600.0, 500.0)
|
let webview_window = match ensure_window(&config, true, false) {
|
||||||
.resizable(false)
|
Ok(EnsureWindowResult::Created(window)) => window,
|
||||||
.decorations(false)
|
Ok(EnsureWindowResult::Existing(_)) => return,
|
||||||
.transparent(true)
|
Err(EnsureWindowError::MissingParent(parent_label)) => {
|
||||||
.shadow(false)
|
error!(
|
||||||
.visible(true)
|
"Failed to build scene window due to missing parent '{}': impossible state",
|
||||||
.skip_taskbar(true)
|
parent_label
|
||||||
.always_on_top(true)
|
);
|
||||||
.visible_on_all_workspaces(true)
|
return;
|
||||||
.build()
|
|
||||||
{
|
|
||||||
Ok(window) => {
|
|
||||||
info!("Scene window builder succeeded");
|
|
||||||
window
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(EnsureWindowError::ShowExisting(e))
|
||||||
|
| Err(EnsureWindowError::SetParent(e))
|
||||||
|
| Err(EnsureWindowError::Build(e)) => {
|
||||||
error!("Failed to build scene window: {}", e);
|
error!("Failed to build scene window: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,28 @@ use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind};
|
|||||||
use tauri_plugin_positioner::WindowExt;
|
use tauri_plugin_positioner::WindowExt;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
use super::window_manager::{ensure_window, EnsureWindowError, EnsureWindowResult, WindowConfig};
|
||||||
|
|
||||||
pub static WELCOME_WINDOW_LABEL: &str = "welcome";
|
pub static WELCOME_WINDOW_LABEL: &str = "welcome";
|
||||||
|
|
||||||
pub fn open_welcome_window() {
|
pub fn open_welcome_window() {
|
||||||
let app_handle = get_app_handle();
|
let app_handle = get_app_handle();
|
||||||
let existing_webview_window = app_handle.get_window(WELCOME_WINDOW_LABEL);
|
|
||||||
|
|
||||||
if let Some(window) = existing_webview_window {
|
let mut config =
|
||||||
if let Err(e) = window.show() {
|
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);
|
error!("Failed to show existing welcome window: {}", e);
|
||||||
MessageDialogBuilder::new(
|
MessageDialogBuilder::new(
|
||||||
app_handle.dialog().clone(),
|
app_handle.dialog().clone(),
|
||||||
@@ -20,33 +34,9 @@ pub fn open_welcome_window() {
|
|||||||
)
|
)
|
||||||
.kind(MessageDialogKind::Error)
|
.kind(MessageDialogKind::Error)
|
||||||
.show(|_| {});
|
.show(|_| {});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
Err(EnsureWindowError::SetParent(e)) | Err(EnsureWindowError::Build(e)) => {
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
error!("Failed to build {} window: {}", WELCOME_WINDOW_LABEL, e);
|
error!("Failed to build {} window: {}", WELCOME_WINDOW_LABEL, e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
256
src-tauri/src/services/window_manager.rs
Normal file
256
src-tauri/src/services/window_manager.rs
Normal file
@@ -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<Mutex<HashMap<String, WindowKind>>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn window_kinds() -> &'static Mutex<HashMap<String, WindowKind>> {
|
||||||
|
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<String>, 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<String>, 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<EnsureWindowResult, EnsureWindowError> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user