From ce07be8d28ff8df22a458f3490c79457bb9ea3f9 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Fri, 4 Sep 2026 20:15:26 +0800 Subject: [PATCH] hide app icon in macos dock without real windows --- src-tauri/src/lib.rs | 6 +- src-tauri/src/ui/activation_policy.rs | 104 ++++++++++++++++++++++++++ src-tauri/src/ui/control_panel/mod.rs | 52 +++++-------- src-tauri/src/ui/debug/mod.rs | 10 +-- src-tauri/src/ui/mod.rs | 3 +- src-tauri/src/ui/onboarding.rs | 1 + src/routes/control-panel/+page.svelte | 10 +-- 7 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 src-tauri/src/ui/activation_policy.rs diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cf7fe9e..655e1bf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -47,7 +47,11 @@ pub fn run() { ) .expect("Failed to export TypeScript bindings"); - tauri::Builder::default() + let builder = tauri::Builder::default(); + #[cfg(target_os = "macos")] + let builder = builder.plugin(ui::activation_policy::ActivationPolicy::default()); + + builder .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, diff --git a/src-tauri/src/ui/activation_policy.rs b/src-tauri/src/ui/activation_policy.rs new file mode 100644 index 0000000..0b00799 --- /dev/null +++ b/src-tauri/src/ui/activation_policy.rs @@ -0,0 +1,104 @@ +use std::collections::HashSet; + +use tauri::{AppHandle, Manager, RunEvent, Window, WindowEvent, Wry, plugin::Plugin}; + +/// Normal windows live from opening until dismissal. Minimized windows still count. +#[derive(Default)] +pub struct ActivationPolicy { + windows: HashSet, + applied_regular: Option, +} + +impl ActivationPolicy { + fn record_window(&mut self, label: &str, created: bool) { + if matches!(label, "scene" | "splashscreen") { + return; + } + if created { + self.windows.insert(label.to_owned()); + } else { + self.windows.remove(label); + } + } + + fn reconcile(&mut self, app: &AppHandle) -> tauri::Result<()> { + let regular = !self.windows.is_empty(); + if self.applied_regular != Some(regular) { + app.set_activation_policy(if regular { + tauri::ActivationPolicy::Regular + } else { + tauri::ActivationPolicy::Accessory + })?; + self.applied_regular = Some(regular); + } + Ok(()) + } +} + +impl Plugin for ActivationPolicy { + fn name(&self) -> &'static str { + "activation-policy" + } + + fn initialize( + &mut self, + app: &AppHandle, + _config: serde_json::Value, + ) -> Result<(), Box> { + self.reconcile(app)?; + Ok(()) + } + + fn window_created(&mut self, window: Window) { + self.record_window(window.label(), true); + if let Err(error) = self.reconcile(window.app_handle()) { + eprintln!("failed to update macOS activation policy: {error}"); + } + } + + fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { + if let RunEvent::WindowEvent { + label, + event: WindowEvent::Destroyed, + .. + } = event + { + self.record_window(label, false); + if let Err(error) = self.reconcile(app) { + eprintln!("failed to update macOS activation policy: {error}"); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::ActivationPolicy; + + #[test] + fn decoration_windows_do_not_keep_the_app_in_the_dock() { + let mut policy = ActivationPolicy::default(); + policy.record_window("scene", true); + policy.record_window("splashscreen", true); + policy.record_window("onboarding", true); + policy.record_window("splashscreen", false); + assert_eq!(policy.windows.len(), 1); + policy.record_window("onboarding", false); + assert!(policy.windows.is_empty()); + } + + #[test] + fn action_windows_keep_the_app_in_the_dock_after_the_panel_closes() { + let mut policy = ActivationPolicy::default(); + policy.record_window("control-panel", true); + policy.record_window("control-panel", true); + policy.record_window("control-panel-action-add-friend", true); + policy.record_window("control-panel", false); + policy.record_window("control-panel", false); + assert_eq!(policy.windows.len(), 1); + policy.record_window("control-panel-action-add-friend", false); + assert!(policy.windows.is_empty()); + policy.record_window("control-panel", true); + assert_eq!(policy.windows.len(), 1); + } +} diff --git a/src-tauri/src/ui/control_panel/mod.rs b/src-tauri/src/ui/control_panel/mod.rs index b9c6726..ad7b247 100644 --- a/src-tauri/src/ui/control_panel/mod.rs +++ b/src-tauri/src/ui/control_panel/mod.rs @@ -3,40 +3,25 @@ use tauri::{AppHandle, Manager, WebviewUrl}; const WINDOW_LABEL: &str = "control-panel"; const ACTION_WINDOW_PREFIX: &str = "control-panel-action-"; -pub fn init(handle: &AppHandle) { - if let Some(window) = handle.get_webview_window(WINDOW_LABEL) { - window.hide().unwrap(); - return; - }; - - let builder = tauri::WebviewWindowBuilder::new( - handle, - WINDOW_LABEL, - WebviewUrl::App("/control-panel".into()), - ) - .title("Friendolls") - .inner_size(400.0, 600.0) - .min_inner_size(400.0, 600.0) - .resizable(false) - .maximizable(false) - .center() - .visible(false); - - let window = builder.build().unwrap(); - window.hide().unwrap(); - let window_c = window.clone(); - window.on_window_event(move |event: &tauri::WindowEvent| { - if let tauri::WindowEvent::CloseRequested { api, .. } = event { - api.prevent_close(); - window_c.hide().unwrap(); - } - }); -} - pub fn show(handle: &AppHandle) -> Result<(), String> { - let window = handle - .get_webview_window(WINDOW_LABEL) - .ok_or_else(|| "Control panel window was not created during startup.".to_string())?; + let window = if let Some(window) = handle.get_webview_window(WINDOW_LABEL) { + window + } else { + tauri::WebviewWindowBuilder::new( + handle, + WINDOW_LABEL, + WebviewUrl::App("/control-panel".into()), + ) + .title("Friendolls") + .inner_size(400.0, 600.0) + .min_inner_size(400.0, 600.0) + .resizable(false) + .maximizable(false) + .transparent(true) + .center() + .build() + .map_err(|error| error.to_string())? + }; window.unminimize().map_err(|error| error.to_string())?; window.show().map_err(|error| error.to_string())?; @@ -84,6 +69,7 @@ pub async fn open_action_window( .resizable(false) .minimizable(false) .maximizable(false) + .transparent(true) .skip_taskbar(true) .center() .build() diff --git a/src-tauri/src/ui/debug/mod.rs b/src-tauri/src/ui/debug/mod.rs index 242b7d3..3ac2e6e 100644 --- a/src-tauri/src/ui/debug/mod.rs +++ b/src-tauri/src/ui/debug/mod.rs @@ -18,14 +18,8 @@ pub fn init(handle: &AppHandle) { .title("Friendolls Debug") .inner_size(800.0, 600.0) .min_inner_size(500.0, 400.0) + .transparent(true) .visible(true); - let window = builder.build().unwrap(); - let window_c = window.clone(); - window.on_window_event(move |event: &tauri::WindowEvent| { - if let tauri::WindowEvent::CloseRequested { api, .. } = event { - api.prevent_close(); - window_c.hide().unwrap(); - } - }); + builder.build().unwrap(); } diff --git a/src-tauri/src/ui/mod.rs b/src-tauri/src/ui/mod.rs index 78b752d..a453211 100644 --- a/src-tauri/src/ui/mod.rs +++ b/src-tauri/src/ui/mod.rs @@ -1,5 +1,7 @@ use tauri::AppHandle; +#[cfg(target_os = "macos")] +pub mod activation_policy; pub mod control_panel; #[cfg(debug_assertions)] mod debug; @@ -12,7 +14,6 @@ pub fn init(app_handle: &AppHandle) { // #[cfg(debug_assertions)] // debug::init(app_handle); scene::init(app_handle); - control_panel::init(app_handle); tray::init(app_handle); } diff --git a/src-tauri/src/ui/onboarding.rs b/src-tauri/src/ui/onboarding.rs index aa9b6ce..17dbe37 100644 --- a/src-tauri/src/ui/onboarding.rs +++ b/src-tauri/src/ui/onboarding.rs @@ -27,6 +27,7 @@ async fn show(handle: &AppHandle, accessibility_only: bool) -> Result<(), String )) .inner_size(680.0, 520.0) .min_inner_size(680.0, 520.0) + .transparent(true) .resizable(false) .maximizable(false) .center() diff --git a/src/routes/control-panel/+page.svelte b/src/routes/control-panel/+page.svelte index 0093f3d..e585957 100644 --- a/src/routes/control-panel/+page.svelte +++ b/src/routes/control-panel/+page.svelte @@ -68,21 +68,21 @@ return true; } - async function hideWindow() { + async function closeWindow() { try { - await getCurrentWindow().hide(); + await getCurrentWindow().close(); } catch (error) { - console.error("failed to hide control panel", error); + console.error("failed to close control panel", error); } } async function confirm() { - if (await applyAll()) await hideWindow(); + if (await applyAll()) await closeWindow(); } function cancel() { tabs.forEach((tab) => actions[tab.id]?.reset()); - void hideWindow(); + void closeWindow(); }