From 24a63eb883850120ea488e2e721111e291f615c7 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <32899793+srbsingh3@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:55:54 +0530 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20reduce=20motion?= =?UTF-8?q?=20accessibility=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a user-configurable setting to control UI animations based on accessibility preferences. The setting supports three modes: - "system": Follows OS accessibility preferences (default) - "on": Always reduces motion (disables animations) - "off": Always enables animations Platform support includes macOS via NSWorkspace accessibility API, with a default implementation for other platforms. The setting is exposed in the Settings UI under Appearance > Motion and currently applies to toast notification animations. Additional animations can be updated to respect this setting in future changes. --- assets/settings/default.json | 10 + crates/gpui/src/app.rs | 5 + crates/gpui/src/platform.rs | 3 + crates/gpui/src/platform/mac/platform.rs | 8 + crates/settings/src/reduce_motion_setting.rs | 242 +++++++++++++++++++ crates/settings/src/settings.rs | 2 + crates/settings/src/vscode_import.rs | 1 + crates/settings_content/src/workspace.rs | 34 +++ crates/settings_ui/src/page_data.rs | 22 ++ crates/settings_ui/src/settings_ui.rs | 1 + crates/workspace/src/toast_layer.rs | 88 ++++--- 11 files changed, 380 insertions(+), 36 deletions(-) create mode 100644 crates/settings/src/reduce_motion_setting.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 6e834933d1a326..41ead8e2ecb0da 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -190,6 +190,16 @@ // // Default: "client" "window_decorations": "client", + // Whether to reduce motion in UI animations. + // 1. Follow the OS accessibility setting + // "system" + // 2. Always reduce motion (skip animations) + // "on" + // 3. Never reduce motion (always animate) + // "off" + // + // Default: "system" + "reduce_motion": "system", // Whether to use the system provided dialogs for Open and Save As. // When set to false, Zed will use the built-in keyboard-first pickers. "use_system_path_prompts": true, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4c7c68942ed9d4..430156450c0e4a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1271,6 +1271,11 @@ impl App { self.platform.should_auto_hide_scrollbars() } + /// Returns whether the platform's accessibility settings request reduced motion. + pub fn should_reduce_motion(&self) -> bool { + self.platform.should_reduce_motion() + } + /// Restarts the application. pub fn restart(&mut self) { self.restart_observers diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index dbd630896f111d..f58c15fe8f73d6 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -272,6 +272,9 @@ pub(crate) trait Platform: 'static { fn set_cursor_style(&self, style: CursorStyle); fn should_auto_hide_scrollbars(&self) -> bool; + fn should_reduce_motion(&self) -> bool { + false + } fn read_from_clipboard(&self) -> Option; fn write_to_clipboard(&self, item: ClipboardItem); diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 041a914251127b..d1bae3cc251e91 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1031,6 +1031,14 @@ impl Platform for MacPlatform { } } + fn should_reduce_motion(&self) -> bool { + unsafe { + let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; + let reduce: BOOL = msg_send![workspace, accessibilityDisplayShouldReduceMotion]; + reduce != NO + } + } + fn read_from_clipboard(&self) -> Option { let state = self.0.lock(); state.general_pasteboard.read() diff --git a/crates/settings/src/reduce_motion_setting.rs b/crates/settings/src/reduce_motion_setting.rs new file mode 100644 index 00000000000000..86980a9c208858 --- /dev/null +++ b/crates/settings/src/reduce_motion_setting.rs @@ -0,0 +1,242 @@ +use crate::{self as settings, settings_content::ReduceMotion}; +use settings::{RegisterSetting, Settings}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, RegisterSetting)] +pub struct ReduceMotionSetting(pub ReduceMotion); + +impl ReduceMotionSetting { + pub fn should_reduce_motion(&self, cx: &gpui::App) -> bool { + match self.0 { + ReduceMotion::System => cx.should_reduce_motion(), + ReduceMotion::On => true, + ReduceMotion::Off => false, + } + } +} + +pub fn should_reduce_motion(cx: &gpui::App) -> bool { + ReduceMotionSetting::get_global(cx).should_reduce_motion(cx) +} + +impl Settings for ReduceMotionSetting { + fn from_settings(settings: &crate::settings_content::SettingsContent) -> Self { + ReduceMotionSetting(settings.workspace.reduce_motion.unwrap_or_default()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{SettingsStore, default_settings}; + use gpui::{App, TestAppContext, UpdateGlobal}; + use settings_content::ReduceMotion; + + fn init_test(cx: &mut TestAppContext) { + let store = cx.update(|cx| SettingsStore::test(cx)); + cx.update(|cx| cx.set_global(store)); + } + + fn set_reduce_motion(cx: &mut TestAppContext, value: ReduceMotion) { + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.workspace.reduce_motion = Some(value); + }); + }); + }); + } + + #[test] + fn test_reduce_motion_default_is_system() { + assert_eq!(ReduceMotion::default(), ReduceMotion::System); + } + + #[test] + fn test_reduce_motion_deserialize_string_variants() { + assert_eq!( + serde_json::from_str::(r#""system""#).unwrap(), + ReduceMotion::System, + ); + assert_eq!( + serde_json::from_str::(r#""on""#).unwrap(), + ReduceMotion::On, + ); + assert_eq!( + serde_json::from_str::(r#""off""#).unwrap(), + ReduceMotion::Off, + ); + } + + #[test] + fn test_reduce_motion_deserialize_string_aliases() { + // serde(alias = "true") matches the JSON string "true", not the boolean true + assert_eq!( + serde_json::from_str::(r#""true""#).unwrap(), + ReduceMotion::On, + ); + assert_eq!( + serde_json::from_str::(r#""false""#).unwrap(), + ReduceMotion::Off, + ); + } + + #[test] + fn test_reduce_motion_deserialize_rejects_invalid_values() { + assert!(serde_json::from_str::("true").is_err()); + assert!(serde_json::from_str::("false").is_err()); + assert!(serde_json::from_str::(r#""bogus""#).is_err()); + assert!(serde_json::from_str::("42").is_err()); + assert!(serde_json::from_str::("null").is_err()); + } + + #[test] + fn test_reduce_motion_serialize_round_trip() { + for variant in [ReduceMotion::System, ReduceMotion::On, ReduceMotion::Off] { + let json = serde_json::to_string(&variant).unwrap(); + let deserialized: ReduceMotion = serde_json::from_str(&json).unwrap(); + assert_eq!(variant, deserialized); + } + } + + #[gpui::test] + fn test_should_reduce_motion_on_returns_true(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + assert!(ReduceMotionSetting(ReduceMotion::On).should_reduce_motion(cx)); + }); + } + + #[gpui::test] + fn test_should_reduce_motion_off_returns_false(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + assert!(!ReduceMotionSetting(ReduceMotion::Off).should_reduce_motion(cx)); + }); + } + + #[gpui::test] + fn test_should_reduce_motion_system_delegates_to_platform(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + let platform_value = cx.should_reduce_motion(); + let setting_value = + ReduceMotionSetting(ReduceMotion::System).should_reduce_motion(cx); + assert_eq!(setting_value, platform_value); + }); + } + + #[gpui::test] + fn test_from_settings_defaults_to_system(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + assert_eq!(ReduceMotionSetting::get_global(cx).0, ReduceMotion::System); + }); + } + + #[gpui::test] + fn test_from_settings_with_each_variant(cx: &mut TestAppContext) { + init_test(cx); + + for variant in [ReduceMotion::On, ReduceMotion::Off, ReduceMotion::System] { + set_reduce_motion(cx, variant); + cx.update(|cx| { + assert_eq!(ReduceMotionSetting::get_global(cx).0, variant); + }); + } + } + + #[gpui::test] + fn test_global_should_reduce_motion(cx: &mut TestAppContext) { + init_test(cx); + + cx.update(|cx| assert!(!should_reduce_motion(cx))); + + set_reduce_motion(cx, ReduceMotion::On); + cx.update(|cx| assert!(should_reduce_motion(cx))); + + set_reduce_motion(cx, ReduceMotion::Off); + cx.update(|cx| assert!(!should_reduce_motion(cx))); + + set_reduce_motion(cx, ReduceMotion::System); + cx.update(|cx| assert!(!should_reduce_motion(cx))); + } + + #[gpui::test] + fn test_settings_store_parses_reduce_motion_from_json(cx: &mut App) { + let mut store = SettingsStore::new(cx, &default_settings()); + store.register_setting::(); + + store + .set_user_settings(r#"{ "reduce_motion": "on" }"#, cx) + .unwrap(); + assert_eq!( + store.get::(None).0, + ReduceMotion::On, + ); + + store + .set_user_settings(r#"{ "reduce_motion": "off" }"#, cx) + .unwrap(); + assert_eq!( + store.get::(None).0, + ReduceMotion::Off, + ); + + store + .set_user_settings(r#"{ "reduce_motion": "system" }"#, cx) + .unwrap(); + assert_eq!( + store.get::(None).0, + ReduceMotion::System, + ); + } + + #[gpui::test] + fn test_settings_store_parses_string_aliases_from_json(cx: &mut App) { + let mut store = SettingsStore::new(cx, &default_settings()); + store.register_setting::(); + + store + .set_user_settings(r#"{ "reduce_motion": "true" }"#, cx) + .unwrap(); + assert_eq!( + store.get::(None).0, + ReduceMotion::On, + ); + + store + .set_user_settings(r#"{ "reduce_motion": "false" }"#, cx) + .unwrap(); + assert_eq!( + store.get::(None).0, + ReduceMotion::Off, + ); + } + + #[gpui::test] + fn test_settings_store_defaults_when_reduce_motion_absent(cx: &mut App) { + let mut store = SettingsStore::new(cx, &default_settings()); + store.register_setting::(); + + store.set_user_settings(r#"{}"#, cx).unwrap(); + assert_eq!( + store.get::(None).0, + ReduceMotion::System, + ); + } + + #[gpui::test] + fn test_settings_store_assign_json_before_register(cx: &mut App) { + let mut store = SettingsStore::new(cx, &default_settings()); + + store + .set_user_settings(r#"{ "reduce_motion": "on" }"#, cx) + .unwrap(); + store.register_setting::(); + + assert_eq!( + store.get::(None).0, + ReduceMotion::On, + ); + } +} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index d66699e8119136..775b9c02aeadb9 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -3,6 +3,7 @@ mod content_into_gpui; mod editable_setting_control; mod editorconfig_store; mod keymap_file; +mod reduce_motion_setting; mod settings_file; mod settings_store; mod vscode_import; @@ -34,6 +35,7 @@ pub use ::settings_content::*; pub use base_keymap_setting::*; pub use content_into_gpui::IntoGpui; pub use editable_setting_control::*; +pub use reduce_motion_setting::*; pub use editorconfig_store::{ Editorconfig, EditorconfigEvent, EditorconfigProperties, EditorconfigStore, }; diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 48d9cb81d7be9e..811724da826c6d 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -983,6 +983,7 @@ impl VsCodeSettings { CloseWindowWhenNoItems::KeepWindowOpen } }), + reduce_motion: None, zoomed_padding: None, } } diff --git a/crates/settings_content/src/workspace.rs b/crates/settings_content/src/workspace.rs index 4a433a8aaaf148..cd4e56000d5d17 100644 --- a/crates/settings_content/src/workspace.rs +++ b/crates/settings_content/src/workspace.rs @@ -116,6 +116,13 @@ pub struct WorkspaceSettingsContent { /// What draws window decorations/titlebar, the client application (Zed) or display server /// Default: client pub window_decorations: Option, + /// Whether to reduce motion in UI animations. + /// When set to "system", follows the OS accessibility setting. + /// When set to "on", animations are always reduced. + /// When set to "off", animations always play. + /// + /// Default: system + pub reduce_motion: Option, } #[with_fallible_options] @@ -336,6 +343,33 @@ pub enum WindowDecorations { Server, } +#[derive( + Copy, + Clone, + Default, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum ReduceMotion { + /// Follow the OS accessibility setting for reduced motion + #[default] + System, + /// Always reduce motion (skip animations) + #[serde(alias = "true")] + On, + /// Never reduce motion (always animate) + #[serde(alias = "false")] + Off, +} + #[derive( Copy, Clone, diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 2e22b18e56ef89..246ca7ca0fab4d 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -1047,6 +1047,27 @@ fn appearance_page() -> SettingsPage { ] } + fn reduce_motion_section() -> [SettingsPageItem; 2] { + [ + SettingsPageItem::SectionHeader("Motion"), + SettingsPageItem::SettingItem(SettingItem { + title: "Reduce Motion", + description: "Controls whether animations are reduced. When set to System, follows your OS accessibility preference.", + field: Box::new(SettingField { + json_path: Some("reduce_motion"), + pick: |settings_content| { + settings_content.workspace.reduce_motion.as_ref() + }, + write: |settings_content, value| { + settings_content.workspace.reduce_motion = value; + }, + }), + metadata: None, + files: USER, + }), + ] + } + fn cursor_section() -> [SettingsPageItem; 5] { [ SettingsPageItem::SectionHeader("Cursor"), @@ -1243,6 +1264,7 @@ fn appearance_page() -> SettingsPage { ui_font_section(), agent_panel_font_section(), text_rendering_section(), + reduce_motion_section(), cursor_section(), highlighting_section(), guides_section(), diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index bed1fcc953eb95..ebd82b910b2029 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -535,6 +535,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_editable_number_field) .add_basic_renderer::(render_ollama_model_picker) .add_basic_renderer::(render_dropdown) diff --git a/crates/workspace/src/toast_layer.rs b/crates/workspace/src/toast_layer.rs index 5979c376f6542b..01b5605d1f1da7 100644 --- a/crates/workspace/src/toast_layer.rs +++ b/crates/workspace/src/toast_layer.rs @@ -7,6 +7,7 @@ use gpui::{ AnyView, DismissEvent, Entity, EntityId, FocusHandle, ManagedView, MouseButton, Subscription, Task, }; +use settings::should_reduce_motion; use ui::{animation::DefaultAnimations, prelude::*}; use zed_actions::toast; @@ -219,43 +220,58 @@ impl ToastLayer { impl Render for ToastLayer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let Some(active_toast) = &self.active_toast else { - return div(); + return div().into_any_element(); }; - div().absolute().size_full().bottom_0().left_0().child( - v_flex() - .id(("toast-layer-container", active_toast.id)) - .absolute() - .w_full() - .bottom(px(0.)) - .flex() - .flex_col() - .items_center() - .track_focus(&active_toast.focus_handle) - .child( - h_flex() - .id("active-toast-container") - .occlude() - .on_hover(cx.listener(|this, hover_start, _window, cx| { - if *hover_start { - this.pause_dismiss_timer(); - } else { - this.restart_dismiss_timer(cx); - } - cx.stop_propagation(); - })) - .on_click(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_down( - MouseButton::Middle, - cx.listener(|this, _, _, cx| { - this.hide_toast(cx); - }), - ) - .child(active_toast.toast.view()), - ) - .animate_in(AnimationDirection::FromBottom, true), - ) + let reduce_motion = should_reduce_motion(cx); + + let toast_container = v_flex() + .id(("toast-layer-container", active_toast.id)) + .absolute() + .w_full() + .bottom(px(0.)) + .flex() + .flex_col() + .items_center() + .track_focus(&active_toast.focus_handle) + .child( + h_flex() + .id("active-toast-container") + .occlude() + .on_hover(cx.listener(|this, hover_start, _window, cx| { + if *hover_start { + this.pause_dismiss_timer(); + } else { + this.restart_dismiss_timer(cx); + } + cx.stop_propagation(); + })) + .on_click(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_down( + MouseButton::Middle, + cx.listener(|this, _, _, cx| { + this.hide_toast(cx); + }), + ) + .child(active_toast.toast.view()), + ); + + let toast_element = if reduce_motion { + toast_container.into_any_element() + } else { + toast_container + .animate_in(AnimationDirection::FromBottom, true) + .into_any_element() + }; + + div() + .absolute() + .size_full() + .bottom_0() + .left_0() + .child(toast_element) + .into_any_element() } } From 4ce51c15e215c6f46d3283cc9d014876a5d2bdc4 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <32899793+srbsingh3@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:57:46 +0530 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=92=84=20style:=20Simplify=20reduce?= =?UTF-8?q?=20motion=20setting=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shorten the description from "Controls whether animations are reduced. When set to System, follows your OS accessibility preference." to "Reduce or disable animations. System uses your OS preference." for better readability and consistency with other setting descriptions in the UI. --- crates/settings_ui/src/page_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 246ca7ca0fab4d..7f481203545e6f 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -1052,7 +1052,7 @@ fn appearance_page() -> SettingsPage { SettingsPageItem::SectionHeader("Motion"), SettingsPageItem::SettingItem(SettingItem { title: "Reduce Motion", - description: "Controls whether animations are reduced. When set to System, follows your OS accessibility preference.", + description: "Reduce or disable animations. System uses your OS preference.", field: Box::new(SettingField { json_path: Some("reduce_motion"), pick: |settings_content| { From 0096c6a4631fbcef1229a8f1c7e67381fa913cb7 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <32899793+srbsingh3@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:09:11 +0530 Subject: [PATCH 3/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Make=20red?= =?UTF-8?q?uce=20motion=20setting=20required?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove unwrap_or_default() fallback to enforce explicit setting configuration. The reduce_motion setting must now always be present in workspace settings. --- crates/settings/src/reduce_motion_setting.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/settings/src/reduce_motion_setting.rs b/crates/settings/src/reduce_motion_setting.rs index 86980a9c208858..cc353fcc832b19 100644 --- a/crates/settings/src/reduce_motion_setting.rs +++ b/crates/settings/src/reduce_motion_setting.rs @@ -20,7 +20,7 @@ pub fn should_reduce_motion(cx: &gpui::App) -> bool { impl Settings for ReduceMotionSetting { fn from_settings(settings: &crate::settings_content::SettingsContent) -> Self { - ReduceMotionSetting(settings.workspace.reduce_motion.unwrap_or_default()) + ReduceMotionSetting(settings.workspace.reduce_motion.unwrap()) } } From 6eea93b5edf92e559d7b309717ef06d6826835d3 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <32899793+srbsingh3@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:23:40 +0530 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Consolidat?= =?UTF-8?q?e=20reduce=20motion=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify test coverage by consolidating overlapping tests into data-driven test cases: - Merge 3 should_reduce_motion variant tests into single test - Combine 2 deserialization tests into data-driven approach - Consolidate 3 settings store parsing tests with cases array - Remove redundant default value test (covered elsewhere) - Fix alphabetical ordering of pub use statements Reduces line count from 242 to 167 while maintaining identical test coverage. Each consolidated test includes context messages for easier failure diagnosis. --- crates/settings/src/reduce_motion_setting.rs | 145 +++++-------------- crates/settings/src/settings.rs | 2 +- 2 files changed, 36 insertions(+), 111 deletions(-) diff --git a/crates/settings/src/reduce_motion_setting.rs b/crates/settings/src/reduce_motion_setting.rs index cc353fcc832b19..8573cef57c908f 100644 --- a/crates/settings/src/reduce_motion_setting.rs +++ b/crates/settings/src/reduce_motion_setting.rs @@ -52,32 +52,23 @@ mod tests { } #[test] - fn test_reduce_motion_deserialize_string_variants() { - assert_eq!( - serde_json::from_str::(r#""system""#).unwrap(), - ReduceMotion::System, - ); - assert_eq!( - serde_json::from_str::(r#""on""#).unwrap(), - ReduceMotion::On, - ); - assert_eq!( - serde_json::from_str::(r#""off""#).unwrap(), - ReduceMotion::Off, - ); - } - - #[test] - fn test_reduce_motion_deserialize_string_aliases() { - // serde(alias = "true") matches the JSON string "true", not the boolean true - assert_eq!( - serde_json::from_str::(r#""true""#).unwrap(), - ReduceMotion::On, - ); - assert_eq!( - serde_json::from_str::(r#""false""#).unwrap(), - ReduceMotion::Off, - ); + fn test_reduce_motion_deserialize() { + let cases: &[(&str, ReduceMotion)] = &[ + (r#""system""#, ReduceMotion::System), + (r#""on""#, ReduceMotion::On), + (r#""off""#, ReduceMotion::Off), + // serde(alias = "true") matches the JSON string "true", not the boolean true + (r#""true""#, ReduceMotion::On), + (r#""false""#, ReduceMotion::Off), + ]; + + for (json, expected) in cases { + assert_eq!( + serde_json::from_str::(json).unwrap(), + *expected, + "for JSON: {json}", + ); + } } #[test] @@ -99,37 +90,15 @@ mod tests { } #[gpui::test] - fn test_should_reduce_motion_on_returns_true(cx: &mut TestAppContext) { + fn test_should_reduce_motion_variants(cx: &mut TestAppContext) { init_test(cx); cx.update(|cx| { assert!(ReduceMotionSetting(ReduceMotion::On).should_reduce_motion(cx)); - }); - } - - #[gpui::test] - fn test_should_reduce_motion_off_returns_false(cx: &mut TestAppContext) { - init_test(cx); - cx.update(|cx| { assert!(!ReduceMotionSetting(ReduceMotion::Off).should_reduce_motion(cx)); - }); - } - - #[gpui::test] - fn test_should_reduce_motion_system_delegates_to_platform(cx: &mut TestAppContext) { - init_test(cx); - cx.update(|cx| { - let platform_value = cx.should_reduce_motion(); - let setting_value = - ReduceMotionSetting(ReduceMotion::System).should_reduce_motion(cx); - assert_eq!(setting_value, platform_value); - }); - } - - #[gpui::test] - fn test_from_settings_defaults_to_system(cx: &mut TestAppContext) { - init_test(cx); - cx.update(|cx| { - assert_eq!(ReduceMotionSetting::get_global(cx).0, ReduceMotion::System); + assert_eq!( + ReduceMotionSetting(ReduceMotion::System).should_reduce_motion(cx), + cx.should_reduce_motion(), + ); }); } @@ -166,63 +135,19 @@ mod tests { let mut store = SettingsStore::new(cx, &default_settings()); store.register_setting::(); - store - .set_user_settings(r#"{ "reduce_motion": "on" }"#, cx) - .unwrap(); - assert_eq!( - store.get::(None).0, - ReduceMotion::On, - ); - - store - .set_user_settings(r#"{ "reduce_motion": "off" }"#, cx) - .unwrap(); - assert_eq!( - store.get::(None).0, - ReduceMotion::Off, - ); - - store - .set_user_settings(r#"{ "reduce_motion": "system" }"#, cx) - .unwrap(); - assert_eq!( - store.get::(None).0, - ReduceMotion::System, - ); - } - - #[gpui::test] - fn test_settings_store_parses_string_aliases_from_json(cx: &mut App) { - let mut store = SettingsStore::new(cx, &default_settings()); - store.register_setting::(); - - store - .set_user_settings(r#"{ "reduce_motion": "true" }"#, cx) - .unwrap(); - assert_eq!( - store.get::(None).0, - ReduceMotion::On, - ); - - store - .set_user_settings(r#"{ "reduce_motion": "false" }"#, cx) - .unwrap(); - assert_eq!( - store.get::(None).0, - ReduceMotion::Off, - ); - } - - #[gpui::test] - fn test_settings_store_defaults_when_reduce_motion_absent(cx: &mut App) { - let mut store = SettingsStore::new(cx, &default_settings()); - store.register_setting::(); - - store.set_user_settings(r#"{}"#, cx).unwrap(); - assert_eq!( - store.get::(None).0, - ReduceMotion::System, - ); + let cases: &[(&str, ReduceMotion)] = &[ + (r#"{ "reduce_motion": "on" }"#, ReduceMotion::On), + (r#"{ "reduce_motion": "off" }"#, ReduceMotion::Off), + (r#"{ "reduce_motion": "system" }"#, ReduceMotion::System), + (r#"{ "reduce_motion": "true" }"#, ReduceMotion::On), + (r#"{ "reduce_motion": "false" }"#, ReduceMotion::Off), + (r#"{}"#, ReduceMotion::System), + ]; + + for (json, expected) in cases { + store.set_user_settings(json, cx).unwrap(); + assert_eq!(store.get::(None).0, *expected, "for JSON: {json}"); + } } #[gpui::test] diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 775b9c02aeadb9..57b1331122754c 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -35,7 +35,6 @@ pub use ::settings_content::*; pub use base_keymap_setting::*; pub use content_into_gpui::IntoGpui; pub use editable_setting_control::*; -pub use reduce_motion_setting::*; pub use editorconfig_store::{ Editorconfig, EditorconfigEvent, EditorconfigProperties, EditorconfigStore, }; @@ -43,6 +42,7 @@ pub use keymap_file::{ KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, }; +pub use reduce_motion_setting::*; pub use settings_file::*; pub use settings_json::*; pub use settings_store::{ From b4e1c673aed161086e763f5de4d3181193e3b7d0 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <32899793+srbsingh3@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:40:49 +0530 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=92=84=20style:=20Clarify=20reduce=20?= =?UTF-8?q?motion=20setting=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ambiguous "Reduce or disable animations" with "Control UI animations" and specify that System mode reads from macOS preferences, making the platform scope clear. --- crates/settings_ui/src/page_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 7f481203545e6f..f99d116305e827 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -1052,7 +1052,7 @@ fn appearance_page() -> SettingsPage { SettingsPageItem::SectionHeader("Motion"), SettingsPageItem::SettingItem(SettingItem { title: "Reduce Motion", - description: "Reduce or disable animations. System uses your OS preference.", + description: "Reduce UI animations. System follows your macOS preference.", field: Box::new(SettingField { json_path: Some("reduce_motion"), pick: |settings_content| { From 4904c5cd8c25466ebe3fd6e58f47e356e305e5d6 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <32899793+srbsingh3@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:51:59 +0530 Subject: [PATCH 6/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Encapsulat?= =?UTF-8?q?e=20ReduceMotionSetting=20internal=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the inner ReduceMotion field private and add a public value() accessor method. This provides better encapsulation and allows for future changes to the internal representation without breaking the public API. Update all test assertions to use the new accessor method instead of direct field access. --- crates/settings/src/reduce_motion_setting.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/settings/src/reduce_motion_setting.rs b/crates/settings/src/reduce_motion_setting.rs index 8573cef57c908f..da9541bd97fa6c 100644 --- a/crates/settings/src/reduce_motion_setting.rs +++ b/crates/settings/src/reduce_motion_setting.rs @@ -2,9 +2,13 @@ use crate::{self as settings, settings_content::ReduceMotion}; use settings::{RegisterSetting, Settings}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Default, RegisterSetting)] -pub struct ReduceMotionSetting(pub ReduceMotion); +pub struct ReduceMotionSetting(ReduceMotion); impl ReduceMotionSetting { + pub fn value(&self) -> ReduceMotion { + self.0 + } + pub fn should_reduce_motion(&self, cx: &gpui::App) -> bool { match self.0 { ReduceMotion::System => cx.should_reduce_motion(), @@ -109,7 +113,7 @@ mod tests { for variant in [ReduceMotion::On, ReduceMotion::Off, ReduceMotion::System] { set_reduce_motion(cx, variant); cx.update(|cx| { - assert_eq!(ReduceMotionSetting::get_global(cx).0, variant); + assert_eq!(ReduceMotionSetting::get_global(cx).value(), variant); }); } } @@ -146,7 +150,7 @@ mod tests { for (json, expected) in cases { store.set_user_settings(json, cx).unwrap(); - assert_eq!(store.get::(None).0, *expected, "for JSON: {json}"); + assert_eq!(store.get::(None).value(), *expected, "for JSON: {json}"); } } @@ -160,7 +164,7 @@ mod tests { store.register_setting::(); assert_eq!( - store.get::(None).0, + store.get::(None).value(), ReduceMotion::On, ); }