gpui_base/
reduce_motion.rs1use gpui::{App, Global};
36
37#[derive(Default)]
40struct SystemReduceMotion {
41 applied: Option<bool>,
42 following: bool,
43}
44
45impl Global for SystemReduceMotion {}
46
47pub(crate) fn init(cx: &mut App) {
49 apply_system_reduce_motion(cx);
50}
51
52pub fn apply_system_reduce_motion(cx: &mut App) {
66 if is_test_scheduler(cx) {
67 return;
68 }
69 apply_preference(platform::read(), cx);
70 let state = cx.default_global::<SystemReduceMotion>();
71 if !state.following {
72 state.following = true;
73 platform::follow(cx);
74 }
75}
76
77fn is_test_scheduler(cx: &App) -> bool {
80 cx.background_executor()
81 .scheduler_executor()
82 .scheduler()
83 .as_test()
84 .is_some()
85}
86
87fn apply_preference(preference: Option<bool>, cx: &mut App) {
90 let Some(reduce) = preference else {
91 return;
92 };
93 let applied = cx
94 .try_global::<SystemReduceMotion>()
95 .and_then(|state| state.applied);
96 if cx.reduce_motion() != applied.unwrap_or(false) {
97 return;
98 }
99 cx.set_reduce_motion(reduce);
100 cx.default_global::<SystemReduceMotion>().applied = Some(reduce);
101}
102
103#[cfg(target_os = "macos")]
104mod platform {
105 use gpui::App;
106 use objc2_app_kit::NSWorkspace;
107
108 pub(super) fn read() -> Option<bool> {
109 Some(NSWorkspace::sharedWorkspace().accessibilityDisplayShouldReduceMotion())
110 }
111
112 pub(super) fn follow(_cx: &mut App) {}
113}
114
115#[cfg(target_os = "windows")]
116mod platform {
117 use gpui::App;
118 use windows::Win32::{
119 Foundation::BOOL,
120 UI::WindowsAndMessaging::{
121 SPI_GETCLIENTAREAANIMATION, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
122 },
123 };
124
125 pub(super) fn read() -> Option<bool> {
126 let mut animations_enabled = BOOL(0);
127 unsafe {
130 SystemParametersInfoW(
131 SPI_GETCLIENTAREAANIMATION,
132 0,
133 Some((&mut animations_enabled as *mut BOOL).cast()),
134 SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
135 )
136 }
137 .ok()?;
138 Some(!animations_enabled.as_bool())
139 }
140
141 pub(super) fn follow(_cx: &mut App) {}
142}
143
144#[cfg(target_os = "linux")]
145mod platform {
146 use ashpd::desktop::settings::{ReducedMotion, Settings};
147 use futures::StreamExt as _;
148 use gpui::App;
149
150 pub(super) fn read() -> Option<bool> {
152 None
153 }
154
155 pub(super) fn follow(cx: &mut App) {
156 cx.spawn(async move |cx| {
157 let settings = Settings::new().await.ok()?;
158 let current = settings.reduced_motion().await.ok()?;
159 cx.update(|cx| super::apply_preference(Some(reduces(current)), cx));
160 let mut changes = settings.receive_reduced_motion_changed().await.ok()?;
161 while let Some(preference) = changes.next().await {
162 cx.update(|cx| super::apply_preference(Some(reduces(preference)), cx));
163 }
164 Some(())
165 })
166 .detach();
167 }
168
169 fn reduces(preference: ReducedMotion) -> bool {
170 matches!(preference, ReducedMotion::ReducedMotion)
171 }
172}
173
174#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
175mod platform {
176 use gpui::App;
177
178 pub(super) fn read() -> Option<bool> {
179 None
180 }
181
182 pub(super) fn follow(_cx: &mut App) {}
183}
184
185#[cfg(test)]
186mod tests {
187 use gpui::TestAppContext;
188
189 use super::{apply_preference, apply_system_reduce_motion};
190
191 #[gpui::test]
192 fn a_system_preference_for_reduced_motion_sets_the_flag(cx: &mut TestAppContext) {
193 cx.update(|cx| {
194 apply_preference(Some(true), cx);
195 assert!(cx.reduce_motion());
196 });
197 }
198
199 #[gpui::test]
200 fn an_unknown_system_preference_leaves_the_flag_alone(cx: &mut TestAppContext) {
201 cx.update(|cx| {
202 apply_preference(None, cx);
203 assert!(!cx.reduce_motion());
204
205 cx.set_reduce_motion(true);
206 apply_preference(None, cx);
207 assert!(cx.reduce_motion());
208 });
209 }
210
211 #[gpui::test]
212 fn the_system_drives_the_flag_until_the_application_sets_it(cx: &mut TestAppContext) {
213 cx.update(|cx| {
214 apply_preference(Some(true), cx);
215 apply_preference(Some(false), cx);
216 assert!(!cx.reduce_motion());
217 apply_preference(Some(true), cx);
218 assert!(cx.reduce_motion());
219
220 cx.set_reduce_motion(false);
221 apply_preference(Some(true), cx);
222 assert!(!cx.reduce_motion());
223 });
224 }
225
226 #[gpui::test]
227 fn the_test_scheduler_is_never_asked_for_the_platform_preference(cx: &mut TestAppContext) {
228 cx.update(|cx| {
229 apply_system_reduce_motion(cx);
230 assert!(!cx.reduce_motion());
231 });
232 cx.run_until_parked();
233 cx.update(|cx| assert!(!cx.reduce_motion()));
234 }
235
236 #[gpui::test]
237 fn a_flag_the_application_set_before_the_first_reading_is_kept(cx: &mut TestAppContext) {
238 cx.update(|cx| {
239 cx.set_reduce_motion(true);
240 apply_preference(Some(false), cx);
241 assert!(cx.reduce_motion());
242 });
243 }
244}