diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 1a3e28fff9accb..f53b6769adb1cd 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1325,6 +1325,20 @@ impl App { self.platform.window_appearance() } + /// Overrides the appearance (light/dark) applied to the app's windows, independent of + /// the OS-wide setting. Pass `None` to clear the override and follow the system again. + /// The current value is reported by [`App::window_appearance`]. + /// + /// On macOS this sets the underlying `NSApplication.appearance`, which controls the + /// native window chrome (the window border and titlebar) of every window. Use this + /// when the app uses a dark theme while the system is in light mode (or vice versa) + /// so the window edges render to match the theme. While an appearance is forced, + /// windows stop tracking system light/dark changes; pass `None` to resume following + /// the system. On other platforms this is a no-op. + pub fn set_window_appearance(&self, appearance: Option) { + self.platform.set_window_appearance(appearance); + } + /// Returns the window button layout configuration when supported. pub fn button_layout(&self) -> Option { self.platform.button_layout() diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 3d23df0e944cdf..fe138886a51dd3 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -167,6 +167,16 @@ pub trait Platform: 'static { /// Returns the appearance of the application's windows. fn window_appearance(&self) -> WindowAppearance; + /// Overrides the appearance (light/dark) applied to the app's windows, independent + /// of the OS-wide setting. Pass `None` to clear the override and follow the system + /// again. The override is reflected by [`Platform::window_appearance`]. + /// + /// Currently only implemented on macOS, where it sets `NSApplication.appearance` so + /// the native window chrome (the window border and titlebar) of every window matches + /// a dark app theme even when the system is in light mode (or vice versa). A no-op on + /// other platforms. + fn set_window_appearance(&self, _appearance: Option) {} + /// Returns the window button layout configuration when supported. fn button_layout(&self) -> Option { None diff --git a/crates/gpui_macos/src/platform.rs b/crates/gpui_macos/src/platform.rs index eed9e90bcd3f7b..dd4fcb31a01a9a 100644 --- a/crates/gpui_macos/src/platform.rs +++ b/crates/gpui_macos/src/platform.rs @@ -7,9 +7,10 @@ use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; use cocoa::{ appkit::{ - NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, - NSControl as _, NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, - NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow, + NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight, NSApplication, + NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, NSControl as _, + NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSSavePanel, + NSVisualEffectState, NSVisualEffectView, NSWindow, }, base::{BOOL, NO, YES, id, nil, selector}, foundation::{ @@ -675,6 +676,29 @@ impl Platform for MacPlatform { } } + fn set_window_appearance(&self, appearance: Option) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + // `None` clears the override by setting a nil appearance, so the app + // falls back to tracking the system-wide light/dark setting. + let ns_appearance: id = match appearance { + None => nil, + Some(appearance) => { + let name: id = match appearance { + WindowAppearance::Light => crate::window_appearance::NSAppearanceNameAqua, + WindowAppearance::Dark => { + crate::window_appearance::NSAppearanceNameDarkAqua + } + WindowAppearance::VibrantLight => NSAppearanceNameVibrantLight, + WindowAppearance::VibrantDark => NSAppearanceNameVibrantDark, + }; + msg_send![class!(NSAppearance), appearanceNamed: name] + } + }; + let _: () = msg_send![app, setAppearance: ns_appearance]; + } + } + fn open_url(&self, url: &str) { unsafe { let ns_url = NSURL::alloc(nil).initWithString_(ns_string(url)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1029c82ef6cdbc..3a3a5f4ee08ad5 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -436,6 +436,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut App) { .detach(); init_cursor_hide_mode(cx); + init_app_appearance(cx); init_reduce_motion(cx); cx.observe_new(|_multi_workspace: &mut MultiWorkspace, window, cx| { @@ -2019,6 +2020,24 @@ fn init_cursor_hide_mode(cx: &mut App) { cx.observe_global::(apply).detach(); } +fn init_app_appearance(cx: &mut App) { + // Force the native window chrome (border + titlebar) to match the selected theme. + // `System` follows the OS (no override); any other theme forces its appearance, so a + // dark theme doesn't render a light window border when the system is in light mode. + let apply = |cx: &mut App| { + let appearance = match ThemeSettings::get_global(cx).theme.mode() { + Some(theme_settings::ThemeAppearanceMode::System) => None, + _ => Some(match cx.theme().appearance() { + theme::Appearance::Light => gpui::WindowAppearance::Light, + theme::Appearance::Dark => gpui::WindowAppearance::Dark, + }), + }; + cx.set_window_appearance(appearance); + }; + apply(cx); + cx.observe_global::(apply).detach(); +} + #[derive(Copy, Clone, Debug, settings::RegisterSetting)] struct ReduceMotionSetting(settings::ReduceMotionMode);