Repository navigation
fix(macos): 托盘图标被拖出后自动重建 + 内置图标兜底 + 窗口仅属当前桌面 #499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: beta
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,17 @@ | |
| //! | ||
| //! 图标直接复用 `app.default_window_icon()`(即 tauri.conf.json 里配置的 bundle 图标), | ||
| //! 不额外引入托盘专用图片资源。 | ||
| //! | ||
| //! ── macOS 的「图标被 Command 拖出删除」兜底 ──────────────────── | ||
| //! macOS 允许用户按住 Command 把状态栏图标拖出删除。被删除后 `NSStatusItem` | ||
| //! 对象仍被 tray-icon 持有、没有任何事件通知,但它的 button 已脱离状态栏窗口 | ||
| //! (`button.window()` 为 nil),于是 `TrayIcon::rect()` 返回 `None` —— | ||
| //! 这就是「图标是否还在」的可靠判据。`spawn_tray_guard` 周期性地在主线程 | ||
| //! 检查这个判据,一旦发现图标不在了就移除旧托盘并用同一份参数重建, | ||
| //! 几秒内图标自动回来,用户无需重启 App(该守护仅 macOS 需要: | ||
| //! Windows / Linux 的托盘不存在被拖出删除的机制)。 | ||
|
|
||
| use std::time::Duration; | ||
|
|
||
| use tauri::{ | ||
| menu::{Menu, MenuItem}, | ||
|
|
@@ -53,15 +64,74 @@ pub fn create(app: &AppHandle) -> 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()) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep tray handles on the main thread. After multiple guards exist, one guard can hold a tray handle while another removes it. The first guard can then drop the last handle on its worker thread. The resolved
|
||
| .unwrap_or(false) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Retry when the tray is absent. If
|
||
| } | ||
|
|
||
| /// 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 走一遍; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Start only one tray guard.
Every successful recovery calls
create, which starts another permanent guard while the original guard continues. If macOS keeps the item hidden, repeated recovery attempts accumulate threads and competing recreations. Start the guard once, independently of tray creation.