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 走一遍;