use gpui::{App, Global};
#[derive(Default)]
struct SystemReduceMotion {
applied: Option<bool>,
following: bool,
}
impl Global for SystemReduceMotion {}
pub(crate) fn init(cx: &mut App) {
apply_system_reduce_motion(cx);
}
pub fn apply_system_reduce_motion(cx: &mut App) {
if is_test_scheduler(cx) {
return;
}
apply_preference(platform::read(), cx);
let state = cx.default_global::<SystemReduceMotion>();
if !state.following {
state.following = true;
platform::follow(cx);
}
}
fn is_test_scheduler(cx: &App) -> bool {
cx.background_executor()
.scheduler_executor()
.scheduler()
.as_test()
.is_some()
}
fn apply_preference(preference: Option<bool>, cx: &mut App) {
let Some(reduce) = preference else {
return;
};
let applied = cx
.try_global::<SystemReduceMotion>()
.and_then(|state| state.applied);
if cx.reduce_motion() != applied.unwrap_or(false) {
return;
}
cx.set_reduce_motion(reduce);
cx.default_global::<SystemReduceMotion>().applied = Some(reduce);
}
#[cfg(target_os = "macos")]
mod platform {
use gpui::App;
use objc2_app_kit::NSWorkspace;
pub(super) fn read() -> Option<bool> {
Some(NSWorkspace::sharedWorkspace().accessibilityDisplayShouldReduceMotion())
}
pub(super) fn follow(_cx: &mut App) {}
}
#[cfg(target_os = "windows")]
mod platform {
use gpui::App;
use windows::Win32::{
Foundation::BOOL,
UI::WindowsAndMessaging::{
SPI_GETCLIENTAREAANIMATION, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
},
};
pub(super) fn read() -> Option<bool> {
let mut animations_enabled = BOOL(0);
unsafe {
SystemParametersInfoW(
SPI_GETCLIENTAREAANIMATION,
0,
Some((&mut animations_enabled as *mut BOOL).cast()),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
)
}
.ok()?;
Some(!animations_enabled.as_bool())
}
pub(super) fn follow(_cx: &mut App) {}
}
#[cfg(target_os = "linux")]
mod platform {
use ashpd::desktop::settings::{ReducedMotion, Settings};
use futures::StreamExt as _;
use gpui::App;
pub(super) fn read() -> Option<bool> {
None
}
pub(super) fn follow(cx: &mut App) {
cx.spawn(async move |cx| {
let settings = Settings::new().await.ok()?;
let current = settings.reduced_motion().await.ok()?;
cx.update(|cx| super::apply_preference(Some(reduces(current)), cx));
let mut changes = settings.receive_reduced_motion_changed().await.ok()?;
while let Some(preference) = changes.next().await {
cx.update(|cx| super::apply_preference(Some(reduces(preference)), cx));
}
Some(())
})
.detach();
}
fn reduces(preference: ReducedMotion) -> bool {
matches!(preference, ReducedMotion::ReducedMotion)
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
mod platform {
use gpui::App;
pub(super) fn read() -> Option<bool> {
None
}
pub(super) fn follow(_cx: &mut App) {}
}
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
use super::{apply_preference, apply_system_reduce_motion};
#[gpui::test]
fn a_system_preference_for_reduced_motion_sets_the_flag(cx: &mut TestAppContext) {
cx.update(|cx| {
apply_preference(Some(true), cx);
assert!(cx.reduce_motion());
});
}
#[gpui::test]
fn an_unknown_system_preference_leaves_the_flag_alone(cx: &mut TestAppContext) {
cx.update(|cx| {
apply_preference(None, cx);
assert!(!cx.reduce_motion());
cx.set_reduce_motion(true);
apply_preference(None, cx);
assert!(cx.reduce_motion());
});
}
#[gpui::test]
fn the_system_drives_the_flag_until_the_application_sets_it(cx: &mut TestAppContext) {
cx.update(|cx| {
apply_preference(Some(true), cx);
apply_preference(Some(false), cx);
assert!(!cx.reduce_motion());
apply_preference(Some(true), cx);
assert!(cx.reduce_motion());
cx.set_reduce_motion(false);
apply_preference(Some(true), cx);
assert!(!cx.reduce_motion());
});
}
#[gpui::test]
fn the_test_scheduler_is_never_asked_for_the_platform_preference(cx: &mut TestAppContext) {
cx.update(|cx| {
apply_system_reduce_motion(cx);
assert!(!cx.reduce_motion());
});
cx.run_until_parked();
cx.update(|cx| assert!(!cx.reduce_motion()));
}
#[gpui::test]
fn a_flag_the_application_set_before_the_first_reading_is_kept(cx: &mut TestAppContext) {
cx.update(|cx| {
cx.set_reduce_motion(true);
apply_preference(Some(false), cx);
assert!(cx.reduce_motion());
});
}
}