返回博客

Tauri 暗色 App 全屏菜单栏发白——window theme 只管窗口,菜单栏得用 NSApp

做了一个深色玻璃风格的 Tauri 2 桌面应用,界面整体暗色,看起来和 Ghostty、iTerm2 的质感很接近。但进入全屏之后,把鼠标移到屏幕顶部,系统菜单栏从顶部滑下来——白色的,格外扎眼。

查了一圈文档,在 tauri.conf.json 里加了 "theme": "Dark",重启,菜单栏还是白的。这个坑比想象中深。

现象

  • Tauri 2 应用,webview 内容深色主题,整体界面已经是暗色调。
  • tauri.conf.json 中 window 块已设置 "theme": "Dark",webview 的 prefers-color-scheme 也响应为 dark。
  • 进入全屏后,顶部自动隐藏的系统菜单栏(Apple 菜单 / 应用名 / Edit / View / Window 那一条)滑出时是浅色 Aqua 风格,与暗色 UI 明显不搭。

根因

macOS 的外观系统有两层概念,很多人没意识到它们是独立的:

  • NSWindow.appearance(窗口级):控制窗口本身和 WKWebView 的明暗主题,对应 prefers-color-scheme
  • NSApplication.effectiveAppearance(应用级):控制系统菜单栏、原生对话框、上下文菜单等系统 UI。它来自 NSApp.appearance,如果未显式设置则跟随系统设置。

Tauri 的 theme: "Dark" 只设置了 NSWindow.appearance完全没有动 NSApp.appearance。所以你的窗口暗了,但菜单栏依旧跟系统走——系统是浅色模式,菜单栏就是白的。

坑:window theme 和菜单栏的 appearance 是两套独立控制链

tauri.conf.jsontheme: "Dark" 只写入 NSWindow.appearance,管不到系统菜单栏。

菜单栏属于 NSApp.effectiveAppearance(应用级),必须调用 NSApp.setAppearance() 才能影响。

此外,Info.plist 里的 NSRequiresAquaSystemAppearance = false 只是"允许 Dark",不是"强制 Dark",也无法解决问题。

正解

保留 window.theme: "Dark"(让 webview 正确响应暗色),同时在 Tauri 的 setup 钩子里通过 objc2 强制设置 NSApp.appearance

Cargo.toml 加 macOS 条件依赖(版本与 tauri 2.11 依赖树对齐):

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = "0.3"
objc2-foundation = "0.3"

src-tauri/src/lib.rs 增加函数并在 setup 钩子里尽早调用:

/// 全屏时系统菜单栏跟随 NSApp.appearance(应用级)。
/// window theme 只管窗口级,故必须在此强制设置。
#[cfg(target_os = "macos")]
fn force_dark_appearance() {
    use objc2_app_kit::{NSAppearance, NSAppearanceNameDarkAqua, NSApplication};
    use objc2_foundation::MainThreadMarker;
    let Some(mtm) = MainThreadMarker::new() else { return };
    let app = NSApplication::sharedApplication(mtm);
    // appearanceNamed 是 unsafe,返回 Option<Retained<NSAppearance>>
    let appearance = unsafe { NSAppearance::appearanceNamed(NSAppearanceNameDarkAqua) };
    // setAppearance 接受 Option<&NSAppearance>,故用 as_deref()
    app.setAppearance(appearance.as_deref());
}
 
// 在 .setup(|_app| { ... }) 里调用:
#[cfg(target_os = "macos")]
force_dark_appearance();

tauri.conf.json window 块保持:

{ "titleBarStyle": "Overlay", "hiddenTitle": true, "theme": "Dark" }
正解:NSApp 级别写一次,全屏菜单栏随之变暗

通过 objc2 在 setup 阶段调用 NSApp.setAppearance(darkAqua),一次调用即可覆盖菜单栏、原生对话框、系统上下文菜单——对一个常驻暗色的 App 来说效果是全局且持久的。

验证方法:cargo check 通过 → 重启 App → 进入全屏 → 上划调出菜单栏,应当是深色半透明,与 Ghostty/iTerm2 表现一致。

排查提示

objc2 API 签名随版本有细微变化(比如 appearanceNamed 的返回类型、setAppearance 的参数类型)。不要靠猜,直接跑 cargo check,编译器会精确告诉你类型不匹配在哪一行。

一句话外卖

macOS 系统菜单栏的明暗由 NSApp.appearance(应用级)控制,而非 NSWindow.appearance;Tauri 的 window.theme 只写窗口级,想让菜单栏变暗必须用 objc2 显式设置应用级 appearance。