Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions assets/settings/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions crates/gpui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions crates/gpui/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClipboardItem>;
fn write_to_clipboard(&self, item: ClipboardItem);
Expand Down
8 changes: 8 additions & 0 deletions crates/gpui/src/platform/mac/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClipboardItem> {
let state = self.0.lock();
state.general_pasteboard.read()
Expand Down
171 changes: 171 additions & 0 deletions crates/settings/src/reduce_motion_setting.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use crate::{self as settings, settings_content::ReduceMotion};
use settings::{RegisterSetting, Settings};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, RegisterSetting)]
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(),
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())
}
}

#[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() {
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::<ReduceMotion>(json).unwrap(),
*expected,
"for JSON: {json}",
);
}
}

#[test]
fn test_reduce_motion_deserialize_rejects_invalid_values() {
assert!(serde_json::from_str::<ReduceMotion>("true").is_err());
assert!(serde_json::from_str::<ReduceMotion>("false").is_err());
assert!(serde_json::from_str::<ReduceMotion>(r#""bogus""#).is_err());
assert!(serde_json::from_str::<ReduceMotion>("42").is_err());
assert!(serde_json::from_str::<ReduceMotion>("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_variants(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
assert!(ReduceMotionSetting(ReduceMotion::On).should_reduce_motion(cx));
assert!(!ReduceMotionSetting(ReduceMotion::Off).should_reduce_motion(cx));
assert_eq!(
ReduceMotionSetting(ReduceMotion::System).should_reduce_motion(cx),
cx.should_reduce_motion(),
);
});
}

#[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).value(), 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::<ReduceMotionSetting>();

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::<ReduceMotionSetting>(None).value(), *expected, "for JSON: {json}");
}
}

#[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::<ReduceMotionSetting>();

assert_eq!(
store.get::<ReduceMotionSetting>(None).value(),
ReduceMotion::On,
);
}
}
2 changes: 2 additions & 0 deletions crates/settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,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::{
Expand Down
1 change: 1 addition & 0 deletions crates/settings/src/vscode_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,7 @@ impl VsCodeSettings {
CloseWindowWhenNoItems::KeepWindowOpen
}
}),
reduce_motion: None,
zoomed_padding: None,
}
}
Expand Down
34 changes: 34 additions & 0 deletions crates/settings_content/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<WindowDecorations>,
/// 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<ReduceMotion>,
}

#[with_fallible_options]
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions crates/settings_ui/src/page_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,27 @@ fn appearance_page() -> SettingsPage {
]
}

fn reduce_motion_section() -> [SettingsPageItem; 2] {
[
SettingsPageItem::SectionHeader("Motion"),
SettingsPageItem::SettingItem(SettingItem {
title: "Reduce Motion",
description: "Reduce UI animations. System follows your macOS 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"),
Expand Down Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions crates/settings_ui/src/settings_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::EditPredictionsMode>(render_dropdown)
.add_basic_renderer::<settings::RelativeLineNumbers>(render_dropdown)
.add_basic_renderer::<settings::WindowDecorations>(render_dropdown)
.add_basic_renderer::<settings::ReduceMotion>(render_dropdown)
.add_basic_renderer::<settings::FontSize>(render_editable_number_field)
.add_basic_renderer::<settings::OllamaModelName>(render_ollama_model_picker)
.add_basic_renderer::<settings::SemanticTokens>(render_dropdown)
Expand Down
Loading