Initialize the `SystemAppearance` using the app's global window appearance (#7508)

Marshall Bowers created

This PR changes our approach to initializing the `SystemAppearance` so
that we can do it earlier in the startup process.

Previously we were using the appearance from the window, meaning that we
couldn't initialize the value until we first opened the window.

Now we read the `window_appearance` from the `AppContext`. On macOS this
is backed by the
[`effectiveAppearance`](https://developer.apple.com/documentation/appkit/nsapplication/2967171-effectiveappearance)
on the `NSApplication`.

We currently still watch for changes to the appearance at the window
level, as the only hook I could find in the documentation is
[`viewDidChangeEffectiveAppearance`](https://developer.apple.com/documentation/appkit/nsview/2977088-viewdidchangeeffectiveappearance),
which is at the `NSView` level.

In my testing this makes it so Zed appropriately chooses the correct
light/dark theme on startup.

Release Notes:

- N/A

Change summary

crates/collab_ui/src/notifications/incoming_call_notification.rs  |  4 
crates/collab_ui/src/notifications/project_shared_notification.rs |  4 
crates/gpui/src/app.rs                                            |  6 
crates/gpui/src/platform.rs                                       | 26 
crates/gpui/src/platform/mac/platform.rs                          | 11 
crates/gpui/src/platform/test/platform.rs                         |  6 
crates/theme/src/settings.rs                                      | 14 
crates/workspace/src/workspace.rs                                 |  2 
crates/zed/src/main.rs                                            |  1 
9 files changed, 46 insertions(+), 28 deletions(-)

Detailed changes

crates/collab_ui/src/notifications/incoming_call_notification.rs 🔗

@@ -5,7 +5,7 @@ use futures::StreamExt;
 use gpui::{prelude::*, AppContext, WindowHandle};
 use settings::Settings;
 use std::sync::{Arc, Weak};
-use theme::{SystemAppearance, ThemeSettings};
+use theme::ThemeSettings;
 use ui::{prelude::*, Button, Label};
 use util::ResultExt;
 use workspace::AppState;
@@ -35,8 +35,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
                     let options = notification_window_options(screen, window_size);
                     let window = cx
                         .open_window(options, |cx| {
-                            SystemAppearance::init_for_window(cx);
-
                             cx.new_view(|_| {
                                 IncomingCallNotification::new(
                                     incoming_call.clone(),

crates/collab_ui/src/notifications/project_shared_notification.rs 🔗

@@ -6,7 +6,7 @@ use collections::HashMap;
 use gpui::{AppContext, Size};
 use settings::Settings;
 use std::sync::{Arc, Weak};
-use theme::{SystemAppearance, ThemeSettings};
+use theme::ThemeSettings;
 use ui::{prelude::*, Button, Label};
 use workspace::AppState;
 
@@ -28,8 +28,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
             for screen in cx.displays() {
                 let options = notification_window_options(screen, window_size);
                 let window = cx.open_window(options, |cx| {
-                    SystemAppearance::init_for_window(cx);
-
                     cx.new_view(|_| {
                         ProjectSharedNotification::new(
                             owner.clone(),

crates/gpui/src/app.rs 🔗

@@ -14,6 +14,7 @@ use smol::future::FutureExt;
 pub use test_context::*;
 use time::UtcOffset;
 
+use crate::WindowAppearance;
 use crate::{
     current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
     AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
@@ -522,6 +523,11 @@ impl AppContext {
         self.platform.displays()
     }
 
+    /// Returns the appearance of the application's windows.
+    pub fn window_appearance(&self) -> WindowAppearance {
+        self.platform.window_appearance()
+    }
+
     /// Writes data to the platform clipboard.
     pub fn write_to_clipboard(&self, item: ClipboardItem) {
         self.platform.write_to_clipboard(item)

crates/gpui/src/platform.rs 🔗

@@ -67,6 +67,9 @@ pub(crate) trait Platform: 'static {
         options: WindowOptions,
     ) -> Box<dyn PlatformWindow>;
 
+    /// Returns the appearance of the application's windows.
+    fn window_appearance(&self) -> WindowAppearance;
+
     fn set_display_link_output_callback(
         &self,
         display_id: DisplayId,
@@ -559,29 +562,30 @@ pub enum WindowBounds {
     Fixed(Bounds<GlobalPixels>),
 }
 
-/// The appearance of the window, as defined by the operating system
-/// On macOS, this corresponds to named [NSAppearance](https://developer.apple.com/documentation/appkit/nsappearance)
-/// values
+/// The appearance of the window, as defined by the operating system.
+///
+/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance)
+/// values.
 #[derive(Copy, Clone, Debug)]
 pub enum WindowAppearance {
-    /// A light appearance
+    /// A light appearance.
     ///
-    /// on macOS, this corresponds to the `aqua` appearance
+    /// On macOS, this corresponds to the `aqua` appearance.
     Light,
 
-    /// A light appearance with vibrant colors
+    /// A light appearance with vibrant colors.
     ///
-    /// on macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance
+    /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance.
     VibrantLight,
 
-    /// A dark appearance
+    /// A dark appearance.
     ///
-    /// on macOS, this corresponds to the `darkAqua` appearance
+    /// On macOS, this corresponds to the `darkAqua` appearance.
     Dark,
 
-    /// A dark appearance with vibrant colors
+    /// A dark appearance with vibrant colors.
     ///
-    /// on macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance
+    /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance.
     VibrantDark,
 }
 

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -3,7 +3,8 @@ use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
     ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem,
     MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput,
-    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowOptions,
+    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowAppearance,
+    WindowOptions,
 };
 use anyhow::anyhow;
 use block::ConcreteBlock;
@@ -505,6 +506,14 @@ impl Platform for MacPlatform {
         ))
     }
 
+    fn window_appearance(&self) -> WindowAppearance {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let appearance: id = msg_send![app, effectiveAppearance];
+            WindowAppearance::from_native(appearance)
+        }
+    }
+
     fn set_display_link_output_callback(
         &self,
         display_id: DisplayId,

crates/gpui/src/platform/test/platform.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
     Keymap, Platform, PlatformDisplay, PlatformTextSystem, Task, TestDisplay, TestWindow,
-    WindowOptions,
+    WindowAppearance, WindowOptions,
 };
 use anyhow::{anyhow, Result};
 use collections::VecDeque;
@@ -178,6 +178,10 @@ impl Platform for TestPlatform {
         Box::new(window)
     }
 
+    fn window_appearance(&self) -> WindowAppearance {
+        WindowAppearance::Light
+    }
+
     fn set_display_link_output_callback(
         &self,
         _display_id: DisplayId,

crates/theme/src/settings.rs 🔗

@@ -4,7 +4,7 @@ use anyhow::Result;
 use derive_more::{Deref, DerefMut};
 use gpui::{
     px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Global, Pixels, Subscription,
-    ViewContext, WindowContext,
+    ViewContext,
 };
 use refineable::Refineable;
 use schemars::{
@@ -49,6 +49,12 @@ struct GlobalSystemAppearance(SystemAppearance);
 impl Global for GlobalSystemAppearance {}
 
 impl SystemAppearance {
+    /// Initializes the [`SystemAppearance`] for the application.
+    pub fn init(cx: &mut AppContext) {
+        *cx.default_global::<GlobalSystemAppearance>() =
+            GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into()));
+    }
+
     /// Returns the global [`SystemAppearance`].
     ///
     /// Inserts a default [`SystemAppearance`] if one does not yet exist.
@@ -56,12 +62,6 @@ impl SystemAppearance {
         cx.default_global::<GlobalSystemAppearance>().0
     }
 
-    /// Initializes the [`SystemAppearance`] for the current window.
-    pub fn init_for_window(cx: &mut WindowContext) {
-        *cx.default_global::<GlobalSystemAppearance>() =
-            GlobalSystemAppearance(SystemAppearance(cx.appearance().into()));
-    }
-
     /// Returns the global [`SystemAppearance`].
     pub fn global(cx: &AppContext) -> Self {
         cx.global::<GlobalSystemAppearance>().0

crates/workspace/src/workspace.rs 🔗

@@ -855,8 +855,6 @@ impl Workspace {
                     let workspace_id = workspace_id.clone();
                     let project_handle = project_handle.clone();
                     move |cx| {
-                        SystemAppearance::init_for_window(cx);
-
                         cx.new_view(|cx| {
                             Workspace::new(workspace_id, project_handle, app_state, cx)
                         })

crates/zed/src/main.rs 🔗

@@ -126,6 +126,7 @@ fn main() {
             AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
         }
 
+        SystemAppearance::init(cx);
         OpenListener::set_global(listener.clone(), cx);
 
         load_embedded_fonts(cx);