Refresh windows when OS appearance changes

Antonio Scandurra created

Change summary

crates/contacts_status_item/src/contacts_status_item.rs |  13 
crates/diagnostics/src/diagnostics.rs                   |   2 
crates/editor/src/element.rs                            |   4 
crates/gpui/src/app.rs                                  |  47 ++
crates/gpui/src/elements/list.rs                        |   4 
crates/gpui/src/elements/text.rs                        |   2 
crates/gpui/src/platform.rs                             |  28 +
crates/gpui/src/platform/mac.rs                         |   1 
crates/gpui/src/platform/mac/appearance.rs              |  37 ++
crates/gpui/src/platform/mac/platform.rs                |   5 
crates/gpui/src/platform/mac/status_item.rs             | 146 ++++++++--
crates/gpui/src/platform/mac/window.rs                  |  47 +++
crates/gpui/src/platform/test.rs                        |   6 
crates/gpui/src/presenter.rs                            |  29 +
14 files changed, 304 insertions(+), 67 deletions(-)

Detailed changes

crates/contacts_status_item/src/contacts_status_item.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{elements::*, Entity, RenderContext, View};
+use gpui::{color::Color, elements::*, Appearance, Entity, RenderContext, View};
 
 pub struct ContactsStatusItem;
 
@@ -11,8 +11,15 @@ impl View for ContactsStatusItem {
         "ContactsStatusItem"
     }
 
-    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
-        Svg::new("icons/zed_22.svg").aligned().boxed()
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let color = match cx.appearance {
+            Appearance::Light | Appearance::VibrantLight => Color::black(),
+            Appearance::Dark | Appearance::VibrantDark => Color::white(),
+        };
+        Svg::new("icons/zed_22.svg")
+            .with_color(color)
+            .aligned()
+            .boxed()
     }
 }
 

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1149,7 +1149,7 @@ mod tests {
         editor: &ViewHandle<Editor>,
         cx: &mut MutableAppContext,
     ) -> Vec<(u32, String)> {
-        let mut presenter = cx.build_presenter(editor.id(), 0.);
+        let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
         let mut cx = presenter.build_layout_context(Default::default(), false, cx);
         cx.render(editor, |editor, cx| {
             let snapshot = editor.snapshot(cx);

crates/editor/src/element.rs 🔗

@@ -2044,7 +2044,7 @@ mod tests {
 
         let layouts = editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);
-            let mut presenter = cx.build_presenter(window_id, 30.);
+            let mut presenter = cx.build_presenter(window_id, 30., Default::default());
             let layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx);
             element.layout_line_numbers(0..6, &Default::default(), &snapshot, &layout_cx)
         });
@@ -2083,7 +2083,7 @@ mod tests {
         );
 
         let mut scene = Scene::new(1.0);
-        let mut presenter = cx.build_presenter(window_id, 30.);
+        let mut presenter = cx.build_presenter(window_id, 30., Default::default());
         let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx);
         let (size, mut state) = element.layout(
             SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),

crates/gpui/src/app.rs 🔗

@@ -9,8 +9,8 @@ use crate::{
     platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::post_inc,
-    AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton, MouseRegionId,
-    PathPromptOptions, TextLayoutCache,
+    Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
+    MouseRegionId, PathPromptOptions, TextLayoutCache,
 };
 pub use action::*;
 use anyhow::{anyhow, Context, Result};
@@ -579,6 +579,7 @@ impl TestAppContext {
                 hovered_region_ids: Default::default(),
                 clicked_region_ids: None,
                 refreshing: false,
+                appearance: Appearance::Light,
             };
             f(view, &mut render_cx)
         })
@@ -1260,6 +1261,7 @@ impl MutableAppContext {
         &mut self,
         window_id: usize,
         titlebar_height: f32,
+        appearance: Appearance,
     ) -> HashMap<usize, ElementBox> {
         self.start_frame();
         #[allow(clippy::needless_collect)]
@@ -1287,6 +1289,7 @@ impl MutableAppContext {
                         hovered_region_ids: Default::default(),
                         clicked_region_ids: None,
                         refreshing: false,
+                        appearance,
                     })
                     .unwrap(),
                 )
@@ -1925,9 +1928,11 @@ impl MutableAppContext {
                 this.cx
                     .platform
                     .open_window(window_id, window_options, this.foreground.clone());
-            let presenter = Rc::new(RefCell::new(
-                this.build_presenter(window_id, window.titlebar_height()),
-            ));
+            let presenter = Rc::new(RefCell::new(this.build_presenter(
+                window_id,
+                window.titlebar_height(),
+                window.appearance(),
+            )));
 
             {
                 let mut app = this.upgrade();
@@ -1977,6 +1982,12 @@ impl MutableAppContext {
                 }));
             }
 
+            {
+                let mut app = this.upgrade();
+                window
+                    .on_appearance_changed(Box::new(move || app.update(|cx| cx.refresh_windows())));
+            }
+
             window.set_input_handler(Box::new(WindowInputHandler {
                 app: this.upgrade().0,
                 window_id,
@@ -2019,7 +2030,11 @@ impl MutableAppContext {
             root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
 
             let mut status_item = this.cx.platform.add_status_item();
-            let presenter = Rc::new(RefCell::new(this.build_presenter(window_id, 0.)));
+            let presenter = Rc::new(RefCell::new(this.build_presenter(
+                window_id,
+                0.,
+                status_item.appearance(),
+            )));
 
             {
                 let mut app = this.upgrade();
@@ -2035,6 +2050,12 @@ impl MutableAppContext {
                 }));
             }
 
+            {
+                let mut app = this.upgrade();
+                status_item
+                    .on_appearance_changed(Box::new(move || app.update(|cx| cx.refresh_windows())));
+            }
+
             let scene = presenter.borrow_mut().build_scene(
                 status_item.size(),
                 status_item.scale_factor(),
@@ -2071,10 +2092,16 @@ impl MutableAppContext {
         self.flush_effects();
     }
 
-    pub fn build_presenter(&mut self, window_id: usize, titlebar_height: f32) -> Presenter {
+    pub fn build_presenter(
+        &mut self,
+        window_id: usize,
+        titlebar_height: f32,
+        appearance: Appearance,
+    ) -> Presenter {
         Presenter::new(
             window_id,
             titlebar_height,
+            appearance,
             self.cx.font_cache.clone(),
             TextLayoutCache::new(self.cx.platform.fonts()),
             self.assets.clone(),
@@ -2412,7 +2439,7 @@ impl MutableAppContext {
             {
                 {
                     let mut presenter = presenter.borrow_mut();
-                    presenter.invalidate(&mut invalidation, self);
+                    presenter.invalidate(&mut invalidation, window.appearance(), self);
                     let scene =
                         presenter.build_scene(window.size(), window.scale_factor(), false, self);
                     window.present_scene(scene);
@@ -2476,6 +2503,7 @@ impl MutableAppContext {
             let mut presenter = presenter.borrow_mut();
             presenter.refresh(
                 invalidation.as_mut().unwrap_or(&mut Default::default()),
+                window.appearance(),
                 self,
             );
             let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self);
@@ -4082,6 +4110,7 @@ pub struct RenderParams {
     pub hovered_region_ids: HashSet<MouseRegionId>,
     pub clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
     pub refreshing: bool,
+    pub appearance: Appearance,
 }
 
 pub struct RenderContext<'a, T: View> {
@@ -4092,6 +4121,7 @@ pub struct RenderContext<'a, T: View> {
     pub(crate) clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
     pub app: &'a mut MutableAppContext,
     pub titlebar_height: f32,
+    pub appearance: Appearance,
     pub refreshing: bool,
 }
 
@@ -4112,6 +4142,7 @@ impl<'a, V: View> RenderContext<'a, V> {
             hovered_region_ids: params.hovered_region_ids.clone(),
             clicked_region_ids: params.clicked_region_ids.clone(),
             refreshing: params.refreshing,
+            appearance: params.appearance,
         }
     }
 

crates/gpui/src/elements/list.rs 🔗

@@ -659,7 +659,7 @@ mod tests {
 
     #[crate::test(self)]
     fn test_layout(cx: &mut crate::MutableAppContext) {
-        let mut presenter = cx.build_presenter(0, 0.);
+        let mut presenter = cx.build_presenter(0, 0., Default::default());
         let (_, view) = cx.add_window(Default::default(), |_| TestView);
         let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
 
@@ -759,7 +759,7 @@ mod tests {
             .unwrap_or(10);
 
         let (_, view) = cx.add_window(Default::default(), |_| TestView);
-        let mut presenter = cx.build_presenter(0, 0.);
+        let mut presenter = cx.build_presenter(0, 0., Default::default());
         let mut next_id = 0;
         let elements = Rc::new(RefCell::new(
             (0..rng.gen_range(0..=20))

crates/gpui/src/elements/text.rs 🔗

@@ -291,7 +291,7 @@ mod tests {
     #[crate::test(self)]
     fn test_soft_wrapping_with_carriage_returns(cx: &mut MutableAppContext) {
         let (window_id, _) = cx.add_window(Default::default(), |_| TestView);
-        let mut presenter = cx.build_presenter(window_id, Default::default());
+        let mut presenter = cx.build_presenter(window_id, Default::default(), Default::default());
         fonts::with_font_cache(cx.font_cache().clone(), || {
             let mut text = Text::new("Hello\r\n".into(), Default::default()).with_soft_wrap(true);
             let (_, state) = text.layout(

crates/gpui/src/platform.rs 🔗

@@ -132,6 +132,8 @@ pub trait Window {
     fn scale_factor(&self) -> f32;
     fn titlebar_height(&self) -> f32;
     fn present_scene(&mut self, scene: Scene);
+    fn appearance(&self) -> Appearance;
+    fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
 }
 
 #[derive(Debug)]
@@ -147,6 +149,20 @@ pub struct TitlebarOptions<'a> {
     pub traffic_light_position: Option<Vector2F>,
 }
 
+#[derive(Copy, Clone, Debug)]
+pub enum Appearance {
+    Light,
+    VibrantLight,
+    Dark,
+    VibrantDark,
+}
+
+impl Default for Appearance {
+    fn default() -> Self {
+        Self::Light
+    }
+}
+
 #[derive(Debug)]
 pub enum WindowBounds {
     Maximized,
@@ -173,6 +189,12 @@ pub enum CursorStyle {
     IBeam,
 }
 
+impl Default for CursorStyle {
+    fn default() -> Self {
+        Self::Arrow
+    }
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
 pub struct AppVersion {
     major: usize,
@@ -180,12 +202,6 @@ pub struct AppVersion {
     patch: usize,
 }
 
-impl Default for CursorStyle {
-    fn default() -> Self {
-        Self::Arrow
-    }
-}
-
 impl FromStr for AppVersion {
     type Err = anyhow::Error;
 

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

@@ -0,0 +1,37 @@
+use std::ffi::CStr;
+
+use cocoa::{
+    appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight},
+    base::id,
+    foundation::NSString,
+};
+use objc::{msg_send, sel, sel_impl};
+
+use crate::Appearance;
+
+impl Appearance {
+    pub unsafe fn from_native(appearance: id) -> Self {
+        let name: id = msg_send![appearance, name];
+        if name == NSAppearanceNameVibrantLight {
+            Self::VibrantLight
+        } else if name == NSAppearanceNameVibrantDark {
+            Self::VibrantDark
+        } else if name == NSAppearanceNameAqua {
+            Self::Light
+        } else if name == NSAppearanceNameDarkAqua {
+            Self::Dark
+        } else {
+            println!(
+                "unknown appearance: {:?}",
+                CStr::from_ptr(name.UTF8String())
+            );
+            Self::Light
+        }
+    }
+}
+
+#[link(name = "AppKit", kind = "framework")]
+extern "C" {
+    pub static NSAppearanceNameAqua: id;
+    pub static NSAppearanceNameDarkAqua: id;
+}

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

@@ -52,6 +52,11 @@ use time::UtcOffset;
 #[allow(non_upper_case_globals)]
 const NSUTF8StringEncoding: NSUInteger = 4;
 
+#[allow(non_upper_case_globals)]
+pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
+#[allow(non_upper_case_globals)]
+pub const NSKeyValueObservingOptionNew: NSInteger = 1;
+
 const MAC_PLATFORM_IVAR: &str = "platform";
 static mut APP_CLASS: *const Class = ptr::null();
 static mut APP_DELEGATE_CLASS: *const Class = ptr::null();

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

@@ -1,12 +1,18 @@
 use crate::{
     geometry::vector::{vec2f, Vector2F},
-    platform::{self, mac::renderer::Renderer},
+    platform::{
+        self,
+        mac::{
+            platform::{NSKeyValueObservingOptionNew, NSViewLayerContentsRedrawDuringViewResize},
+            renderer::Renderer,
+        },
+    },
     Event, FontSystem, Scene,
 };
 use cocoa::{
     appkit::{NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
     base::{id, nil, YES},
-    foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize},
+    foundation::{NSPoint, NSRect, NSSize, NSString},
 };
 use ctor::ctor;
 use foreign_types::ForeignTypeRef;
@@ -15,7 +21,7 @@ use objc::{
     declare::ClassDecl,
     msg_send,
     rc::StrongPtr,
-    runtime::{Class, Object, Sel},
+    runtime::{Class, Object, Protocol, Sel},
     sel, sel_impl,
 };
 use std::{
@@ -77,6 +83,20 @@ unsafe fn build_classes() {
             sel!(flagsChanged:),
             handle_view_event as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(
+            sel!(makeBackingLayer),
+            make_backing_layer as extern "C" fn(&Object, Sel) -> id,
+        );
+        decl.add_method(
+            sel!(observeValueForKeyPath:ofObject:change:context:),
+            appearance_changed as extern "C" fn(&Object, Sel, id, id, id, id),
+        );
+
+        decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
+        decl.add_method(
+            sel!(displayLayer:),
+            display_layer as extern "C" fn(&Object, Sel, id),
+        );
 
         decl.register()
     };
@@ -86,15 +106,16 @@ pub struct StatusItem(Rc<RefCell<StatusItemState>>);
 
 struct StatusItemState {
     native_item: StrongPtr,
+    native_view: StrongPtr,
     renderer: Renderer,
+    scene: Option<Scene>,
     event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
+    appearance_changed_callback: Option<Box<dyn FnMut()>>,
 }
 
 impl StatusItem {
     pub fn add(fonts: Arc<dyn FontSystem>) -> Self {
         unsafe {
-            let pool = NSAutoreleasePool::new(nil);
-
             let renderer = Renderer::new(false, fonts);
             let status_bar = NSStatusBar::systemStatusBar(nil);
             let native_item =
@@ -103,39 +124,51 @@ impl StatusItem {
             let button = native_item.button();
             let _: () = msg_send![button, setHidden: YES];
 
-            let item = Self(Rc::new_cyclic(|state| {
-                let parent_view = button.superview().superview();
-
-                let view: id = msg_send![VIEW_CLASS, alloc];
-                NSView::initWithFrame_(
-                    view,
-                    NSRect::new(NSPoint::new(0., 0.), NSView::frame(parent_view).size),
-                );
-                view.setWantsBestResolutionOpenGLSurface_(YES);
-                view.setLayer(renderer.layer().as_ptr() as id);
-                view.setWantsLayer(true);
-                (*view).set_ivar(STATE_IVAR, Weak::into_raw(state.clone()) as *const c_void);
-                parent_view.addSubview_(view.autorelease());
-
-                RefCell::new(StatusItemState {
-                    native_item,
-                    renderer,
-                    event_callback: None,
-                })
+            let native_view = msg_send![VIEW_CLASS, alloc];
+            let state = Rc::new(RefCell::new(StatusItemState {
+                native_item,
+                native_view: StrongPtr::new(native_view),
+                renderer,
+                scene: None,
+                event_callback: None,
+                appearance_changed_callback: None,
             }));
 
+            let parent_view = button.superview().superview();
+            NSView::initWithFrame_(
+                native_view,
+                NSRect::new(NSPoint::new(0., 0.), NSView::frame(parent_view).size),
+            );
+            (*native_view).set_ivar(
+                STATE_IVAR,
+                Weak::into_raw(Rc::downgrade(&state)) as *const c_void,
+            );
+            native_view.setWantsBestResolutionOpenGLSurface_(YES);
+            native_view.setWantsLayer(true);
+            let _: () = msg_send![
+                native_view,
+                setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
+            ];
+            let _: () = msg_send![
+                button,
+                addObserver: native_view
+                forKeyPath: NSString::alloc(nil).init_str("effectiveAppearance")
+                options: NSKeyValueObservingOptionNew
+                context: nil
+            ];
+
+            parent_view.addSubview_(native_view);
+
             {
-                let item = item.0.borrow();
-                let layer = item.renderer.layer();
-                let scale_factor = item.scale_factor();
-                let size = item.size() * scale_factor;
+                let state = state.borrow();
+                let layer = state.renderer.layer();
+                let scale_factor = state.scale_factor();
+                let size = state.size() * scale_factor;
                 layer.set_contents_scale(scale_factor.into());
                 layer.set_drawable_size(metal::CGSize::new(size.x().into(), size.y().into()));
             }
 
-            pool.drain();
-
-            item
+            Self(state)
         }
     }
 }
@@ -149,6 +182,10 @@ impl platform::Window for StatusItem {
         self.0.borrow_mut().event_callback = Some(callback);
     }
 
+    fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
+        self.0.borrow_mut().appearance_changed_callback = Some(callback);
+    }
+
     fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {
         unimplemented!()
     }
@@ -223,7 +260,18 @@ impl platform::Window for StatusItem {
     }
 
     fn present_scene(&mut self, scene: Scene) {
-        self.0.borrow_mut().renderer.render(&scene);
+        self.0.borrow_mut().scene = Some(scene);
+        unsafe {
+            let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES];
+        }
+    }
+
+    fn appearance(&self) -> crate::Appearance {
+        unsafe {
+            let appearance: id =
+                msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
+            crate::Appearance::from_native(appearance)
+        }
     }
 }
 
@@ -247,6 +295,7 @@ impl StatusItemState {
 extern "C" fn dealloc_view(this: &Object, _: Sel) {
     unsafe {
         drop_state(this);
+
         let _: () = msg_send![super(this, class!(NSView)), dealloc];
     }
 }
@@ -266,6 +315,39 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     }
 }
 
+extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
+    if let Some(state) = unsafe { get_state(this).upgrade() } {
+        let state = state.borrow();
+        state.renderer.layer().as_ptr() as id
+    } else {
+        nil
+    }
+}
+
+extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
+    unsafe {
+        if let Some(state) = get_state(this).upgrade() {
+            let mut state = state.borrow_mut();
+            if let Some(scene) = state.scene.take() {
+                state.renderer.render(&scene);
+            }
+        }
+    }
+}
+
+extern "C" fn appearance_changed(this: &Object, _: Sel, _: id, _: id, _: id, _: id) {
+    unsafe {
+        if let Some(state) = get_state(this).upgrade() {
+            let mut state_borrow = state.as_ref().borrow_mut();
+            if let Some(mut callback) = state_borrow.appearance_changed_callback.take() {
+                drop(state_borrow);
+                callback();
+                state.borrow_mut().appearance_changed_callback = Some(callback);
+            }
+        }
+    }
+}
+
 unsafe fn get_state(object: &Object) -> Weak<RefCell<StatusItemState>> {
     let raw: *mut c_void = *object.get_ivar(STATE_IVAR);
     let weak1 = Weak::from_raw(raw as *mut RefCell<StatusItemState>);

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

@@ -1,4 +1,3 @@
-use super::{geometry::RectFExt, renderer::Renderer};
 use crate::{
     executor,
     geometry::{
@@ -6,7 +5,12 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     keymap::Keystroke,
-    platform::{self, Event, WindowBounds},
+    mac::platform::{NSKeyValueObservingOptionNew, NSViewLayerContentsRedrawDuringViewResize},
+    platform::{
+        self,
+        mac::{geometry::RectFExt, renderer::Renderer},
+        Event, WindowBounds,
+    },
     InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
     MouseMovedEvent, Scene,
 };
@@ -102,9 +106,6 @@ unsafe impl objc::Encode for NSRange {
     }
 }
 
-#[allow(non_upper_case_globals)]
-const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
-
 #[ctor]
 unsafe fn build_classes() {
     WINDOW_CLASS = {
@@ -264,6 +265,10 @@ unsafe fn build_classes() {
             attributed_substring_for_proposed_range
                 as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
         );
+        decl.add_method(
+            sel!(observeValueForKeyPath:ofObject:change:context:),
+            appearance_changed as extern "C" fn(&Object, Sel, id, id, id, id),
+        );
 
         // Suppress beep on keystrokes with modifier keys.
         decl.add_method(
@@ -298,6 +303,7 @@ struct WindowState {
     fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
     should_close_callback: Option<Box<dyn FnMut() -> bool>>,
     close_callback: Option<Box<dyn FnOnce()>>,
+    appearance_changed_callback: Option<Box<dyn FnMut()>>,
     input_handler: Option<Box<dyn InputHandler>>,
     pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
     performed_key_equivalent: bool,
@@ -376,6 +382,7 @@ impl Window {
                 close_callback: None,
                 activate_callback: None,
                 fullscreen_callback: None,
+                appearance_changed_callback: None,
                 input_handler: None,
                 pending_key_down: None,
                 performed_key_equivalent: false,
@@ -433,6 +440,13 @@ impl Window {
 
             native_window.center();
             native_window.makeKeyAndOrderFront_(nil);
+            let _: () = msg_send![
+                native_window,
+                addObserver: native_view
+                forKeyPath: NSString::alloc(nil).init_str("effectiveAppearance")
+                options: NSKeyValueObservingOptionNew
+                context: nil
+            ];
 
             window.0.borrow().move_traffic_light();
             pool.drain();
@@ -634,6 +648,17 @@ impl platform::Window for Window {
     fn titlebar_height(&self) -> f32 {
         self.0.as_ref().borrow().titlebar_height()
     }
+
+    fn appearance(&self) -> crate::Appearance {
+        unsafe {
+            let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance];
+            crate::Appearance::from_native(appearance)
+        }
+    }
+
+    fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
+        self.0.borrow_mut().appearance_changed_callback = Some(callback);
+    }
 }
 
 impl WindowState {
@@ -1270,6 +1295,18 @@ extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
     }
 }
 
+extern "C" fn appearance_changed(this: &Object, _: Sel, _: id, _: id, _: id, _: id) {
+    unsafe {
+        let state = get_window_state(this);
+        let mut state_borrow = state.as_ref().borrow_mut();
+        if let Some(mut callback) = state_borrow.appearance_changed_callback.take() {
+            drop(state_borrow);
+            callback();
+            state.borrow_mut().appearance_changed_callback = Some(callback);
+        }
+    }
+}
+
 async fn synthetic_drag(
     window_state: Weak<RefCell<WindowState>>,
     drag_id: usize,

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

@@ -298,6 +298,12 @@ impl super::Window for Window {
     fn present_scene(&mut self, scene: crate::Scene) {
         self.current_scene = Some(scene);
     }
+
+    fn appearance(&self) -> crate::Appearance {
+        crate::Appearance::Light
+    }
+
+    fn on_appearance_changed(&mut self, _: Box<dyn FnMut()>) {}
 }
 
 pub fn platform() -> Platform {

crates/gpui/src/presenter.rs 🔗

@@ -11,10 +11,10 @@ use crate::{
         HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
     },
     text_layout::TextLayoutCache,
-    Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
-    FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId, ParentId,
-    ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle,
-    View, ViewHandle, WeakModelHandle, WeakViewHandle,
+    Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, Appearance, AssetCache, ElementBox,
+    Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId,
+    ParentId, ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle,
+    UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use collections::{HashMap, HashSet};
 use pathfinder_geometry::vector::{vec2f, Vector2F};
@@ -40,12 +40,14 @@ pub struct Presenter {
     clicked_button: Option<MouseButton>,
     mouse_position: Vector2F,
     titlebar_height: f32,
+    appearance: Appearance,
 }
 
 impl Presenter {
     pub fn new(
         window_id: usize,
         titlebar_height: f32,
+        appearance: Appearance,
         font_cache: Arc<FontCache>,
         text_layout_cache: TextLayoutCache,
         asset_cache: Arc<AssetCache>,
@@ -53,7 +55,7 @@ impl Presenter {
     ) -> Self {
         Self {
             window_id,
-            rendered_views: cx.render_views(window_id, titlebar_height),
+            rendered_views: cx.render_views(window_id, titlebar_height, appearance),
             cursor_regions: Default::default(),
             mouse_regions: Default::default(),
             font_cache,
@@ -65,15 +67,18 @@ impl Presenter {
             clicked_button: None,
             mouse_position: vec2f(0., 0.),
             titlebar_height,
+            appearance,
         }
     }
 
     pub fn invalidate(
         &mut self,
         invalidation: &mut WindowInvalidation,
+        appearance: Appearance,
         cx: &mut MutableAppContext,
     ) {
         cx.start_frame();
+        self.appearance = appearance;
         for view_id in &invalidation.removed {
             invalidation.updated.remove(view_id);
             self.rendered_views.remove(view_id);
@@ -96,14 +101,20 @@ impl Presenter {
                         )
                     }),
                     refreshing: false,
+                    appearance,
                 })
                 .unwrap(),
             );
         }
     }
 
-    pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) {
-        self.invalidate(invalidation, cx);
+    pub fn refresh(
+        &mut self,
+        invalidation: &mut WindowInvalidation,
+        appearance: Appearance,
+        cx: &mut MutableAppContext,
+    ) {
+        self.invalidate(invalidation, appearance, cx);
         for (view_id, view) in &mut self.rendered_views {
             if !invalidation.updated.contains(view_id) {
                 *view = cx
@@ -122,6 +133,7 @@ impl Presenter {
                             )
                         }),
                         refreshing: true,
+                        appearance,
                     })
                     .unwrap();
             }
@@ -194,6 +206,7 @@ impl Presenter {
                 )
             }),
             titlebar_height: self.titlebar_height,
+            appearance: self.appearance,
             window_size,
             app: cx,
         }
@@ -545,6 +558,7 @@ pub struct LayoutContext<'a> {
     pub refreshing: bool,
     pub window_size: Vector2F,
     titlebar_height: f32,
+    appearance: Appearance,
     hovered_region_ids: HashSet<MouseRegionId>,
     clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
 }
@@ -619,6 +633,7 @@ impl<'a> LayoutContext<'a> {
                 hovered_region_ids: self.hovered_region_ids.clone(),
                 clicked_region_ids: self.clicked_region_ids.clone(),
                 refreshing: self.refreshing,
+                appearance: self.appearance,
             };
             f(view, &mut render_cx)
         })