hide app icon in macos dock without real windows

This commit is contained in:
2026-09-04 20:15:26 +08:00
parent ef8af68398
commit ce07be8d28
7 changed files with 138 additions and 48 deletions
+5 -1
View File
@@ -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,
+104
View File
@@ -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<String>,
applied_regular: Option<bool>,
}
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<Wry> for ActivationPolicy {
fn name(&self) -> &'static str {
"activation-policy"
}
fn initialize(
&mut self,
app: &AppHandle,
_config: serde_json::Value,
) -> Result<(), Box<dyn std::error::Error>> {
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);
}
}
+19 -33
View File
@@ -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()
+2 -8
View File
@@ -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();
}
+2 -1
View File
@@ -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);
}
+1
View File
@@ -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()
+5 -5
View File
@@ -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();
}
</script>