From 89f2b02ed14ba5786f1646bc14984f96c42378ca Mon Sep 17 00:00:00 2001 From: gianni Date: Thu, 5 Nov 2026 23:46:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(macos):=20=E6=89=98=E7=9B=98=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E8=A2=AB=E6=8B=96=E5=87=BA=E5=90=8E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E5=BB=BA=20+=20=E5=86=85=E7=BD=AE=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E5=85=9C=E5=BA=95=20+=20=E7=AA=97=E5=8F=A3=E4=BB=85=E5=B1=9E?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E6=A1=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tray.rs: 新增 macOS 守卫,每 3 秒用 TrayIcon::rect() 检测托盘是否仍显示, 被 Command 拖出删除后自动移除并重建(重建在主线程执行,避免 AppKit 跨线程问题) - tray.rs: 托盘图标强制使用内置 32x32.png(default_window_icon 在部分构建 环境渲染不可见) - lib.rs: macOS 建窗显式 visible_on_all_workspaces(false),固化窗口只属当前桌面 --- desktop-tauri/src-tauri/src/lib.rs | 4 ++ desktop-tauri/src-tauri/src/tray.rs | 74 ++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/desktop-tauri/src-tauri/src/lib.rs b/desktop-tauri/src-tauri/src/lib.rs index c6dd36f53..c0230f01e 100644 --- a/desktop-tauri/src-tauri/src/lib.rs +++ b/desktop-tauri/src-tauri/src/lib.rs @@ -208,6 +208,10 @@ fn create_main_window(app: &AppHandle, visible: bool) -> tauri::Result tauri::Result<()> { } }); - if let Some(icon) = app.default_window_icon() { - builder = builder.icon(icon.clone()); + // 图标直接使用内置 32x32.png(与 tauri.conf.json 的 bundle.icon 第一项同源)。 + // 不用 `default_window_icon()`:实测该值在部分构建环境下取到的内嵌图标 + // 在 macOS 状态栏渲染异常(按钮有图像但屏幕上不可见),而内置 32x32.png + // 是经过验证的蓝底白箭头图标,尺寸/格式稳定,状态栏必然可见。 + let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/32x32.png")).ok(); + if let Some(icon) = icon { + builder = builder.icon(icon); + } else { + eprintln!("[tray] 警告:未能加载任何托盘图标,将以空白占位显示"); } // 即使拿不到默认图标也照样把托盘建起来:宁可是个无图标的占位, // 也不能让关窗后的用户失去唯一入口 builder.build(app)?; + + // macOS 上托盘图标可能被用户 Command 拖出删除(且无事件通知), + // 建好图标后挂一个自动重建守护,被删后几秒内自动恢复。 + #[cfg(target_os = "macos")] + spawn_tray_guard(app); + Ok(()) } +/// 判断 macOS 上托盘图标当前是否「仍显示在菜单栏」。 +/// +/// 判据:正常显示时 `NSStatusItem` 的 button 挂在状态栏窗口上, +/// `TrayIcon::rect()` 返回 `Some`;被用户 Command 拖出删除后 button 脱离 +/// 状态栏窗口,`rect()` 返回 `None`。这是 tray-icon 在 macOS 上唯一能 +/// 拿到的「图标是否还在」信号(TrayIcon 本身没有任何「被移除」事件, +/// 对象也一直存活,只是不再显示)。 +#[cfg(target_os = "macos")] +fn tray_is_missing(app: &AppHandle) -> bool { + app.tray_by_id(TRAY_ID) + .map(|tray| tray.rect().ok().flatten().is_none()) + .unwrap_or(false) +} + +/// macOS 托盘图标自动重建守护。 +/// +/// 每 3 秒检查一次托盘是否被用户拖出删除;发现缺失时先移除旧托盘 +/// (`remove_tray_by_id` 会触发 `NSStatusBar.removeStatusItem`,彻底清掉 +/// 系统侧已失效的 item),再按启动时同一份参数重建。重建后图标出现在 +/// 菜单栏,`rect()` 恢复 `Some`,守护进入待命。 +/// +/// 检查循环在后台线程跑,但 **重建必须投递回主线程**: +/// `remove_tray_by_id` 与 `TrayIconBuilder::build` 底层都直接触碰 AppKit +/// (NSStatusBar / NSStatusItem),非主线程调用是未定义行为。 +/// `TrayIcon::rect()` 本身已内部投递主线程并阻塞等待,可以放心在后台线程调用。 +#[cfg(target_os = "macos")] +fn spawn_tray_guard(app: &AppHandle) { + let handle = app.clone(); + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_secs(3)); + if !tray_is_missing(&handle) { + continue; + } + eprintln!("[tray] 检测到菜单栏图标被移除,正在自动重建…"); + let thread_handle = handle.clone(); + let closure_handle = handle.clone(); + let _ = thread_handle.run_on_main_thread(move || { + // remove_tray_by_id 返回被移除的 TrayIcon,丢弃即触发 Drop 清理 + let _ = closure_handle.remove_tray_by_id(TRAY_ID); + if let Err(error) = create(&closure_handle) { + eprintln!("[tray] 自动重建托盘图标失败(下轮重试): {error}"); + } + }); + }); +} + /// 显示并聚焦主窗口。窗口可能处于「隐藏」「最小化」两种状态, /// 也可能在轻量模式下已被销毁 —— 三种都要能恢复: /// 前两种按 unminimize → show → set_focus 走一遍;