gpui_macos/
system_theme.rs1use crate::{ActiveTheme, Theme};
3use gpui_kit::{App, Global, Window};
4use std::collections::HashSet;
5#[derive(Default)]
6struct AppearancePolicy {
7 following: bool,
8 windows: HashSet<gpui_kit::WindowId>,
9 closed: Option<gpui_kit::Subscription>,
10}
11impl Global for AppearancePolicy {}
12
13impl Theme {
14 pub fn follow_system(cx: &mut App) {
17 cx.set_window_appearance(None);
18 cx.default_global::<AppearancePolicy>().following = true;
19 sync(cx);
20 }
21 pub fn is_following_system(cx: &App) -> bool {
22 cx.try_global::<AppearancePolicy>()
23 .is_some_and(|policy| policy.following)
24 }
25}
26pub(crate) fn stop_following(cx: &mut App) {
27 cx.default_global::<AppearancePolicy>().following = false;
28}
29fn sync(cx: &mut App) {
30 let mut next = Theme::system(cx);
31 if let Some(current) = cx.try_global::<Theme>() {
32 next.glass_tint = current.glass_tint;
33 let default = if current.is_dark() {
35 Theme::dark()
36 } else {
37 Theme::light()
38 };
39 if current.accent != default.accent {
40 next = next.with_accent(current.accent);
41 }
42 }
43 if cx.try_global::<Theme>() != Some(&next) {
44 next.apply_palette(cx);
45 }
46}
47pub fn observe_window(window: &mut Window, cx: &mut App) {
50 if cx.default_global::<AppearancePolicy>().closed.is_none() {
51 let closed = cx.on_window_closed(|cx, id| {
52 cx.default_global::<AppearancePolicy>().windows.remove(&id);
53 });
54 cx.default_global::<AppearancePolicy>().closed = Some(closed);
55 }
56 if cx
57 .default_global::<AppearancePolicy>()
58 .windows
59 .insert(window.window_handle().window_id())
60 {
61 window
62 .observe_window_appearance(|_, cx| {
63 if Theme::is_following_system(cx) {
64 sync(cx);
65 }
66 gpui_kit::base::apply_system_reduce_motion(cx);
67 })
68 .detach();
69 }
70}
71pub fn refresh_accessibility(cx: &mut App) {
73 crate::native::refresh_sidebar_metrics();
74 let mut theme = cx.macos().clone();
75 theme.read_accessibility();
76 theme.apply_palette(cx);
77 gpui_kit::base::apply_system_reduce_motion(cx);
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use gpui_kit::{Context, IntoElement, Render, TestAppContext, div, px, size};
84
85 struct ObservedWindow;
86 impl Render for ObservedWindow {
87 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
88 div()
89 }
90 }
91
92 #[gpui_kit::test]
93 fn appearance_observation_forgets_closed_windows_and_keeps_live_windows(
94 cx: &mut TestAppContext,
95 ) {
96 cx.update(crate::init);
97 let open = |cx: &mut TestAppContext| {
98 cx.open_window(size(px(100.), px(100.)), |window, cx| {
99 observe_window(window, cx);
100 observe_window(window, cx);
101 ObservedWindow
102 })
103 };
104 let persistent = open(cx);
105 for _ in 0..20 {
106 let temporary = open(cx);
107 cx.update(|cx| assert_eq!(cx.global::<AppearancePolicy>().windows.len(), 2));
108 temporary
109 .update(cx, |_, window, _| window.remove_window())
110 .unwrap();
111 cx.run_until_parked();
112 cx.update(|cx| {
113 assert_eq!(cx.global::<AppearancePolicy>().windows.len(), 1);
114 assert!(
115 cx.global::<AppearancePolicy>()
116 .windows
117 .contains(&persistent.window_id())
118 );
119 });
120 }
121 persistent
122 .update(cx, |_, window, _| window.remove_window())
123 .unwrap();
124 cx.run_until_parked();
125 cx.update(|cx| assert!(cx.global::<AppearancePolicy>().windows.is_empty()));
126 }
127}