From 0d1264516dd738dc4345cff9ab566246d5fa0075 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 13 Oct 2025 15:24:45 -0500 Subject: [PATCH] settings_ui: Implement reset to default button (#40135) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings/src/settings_store.rs | 32 +++++++++-- crates/settings_ui/src/settings_ui.rs | 76 ++++++++++++++++++++++++++- crates/ui/src/components/icon.rs | 26 ++++++++- 3 files changed, 127 insertions(+), 7 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 0f3457bc3463dfb58dc020dfd0398e0efdfd5e21..2dffed750b9f012c128e5afa18854791658be0cc 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -488,7 +488,7 @@ impl SettingsStore { files } - fn get_content_for_file(&self, file: SettingsFile) -> Option<&SettingsContent> { + pub fn get_content_for_file(&self, file: SettingsFile) -> Option<&SettingsContent> { match file { SettingsFile::User => self .user_settings @@ -542,6 +542,25 @@ impl SettingsStore { &self, target_file: SettingsFile, pick: fn(&SettingsContent) -> &Option, + ) -> (SettingsFile, Option<&T>) { + self.get_value_from_file_inner(target_file, pick, true) + } + + /// Same as `Self::get_value_from_file` except that it does not include the current file. + /// Therefore it returns the value that was potentially overloaded by the target file. + pub fn get_value_up_to_file( + &self, + target_file: SettingsFile, + pick: fn(&SettingsContent) -> &Option, + ) -> (SettingsFile, Option<&T>) { + self.get_value_from_file_inner(target_file, pick, false) + } + + fn get_value_from_file_inner( + &self, + target_file: SettingsFile, + pick: fn(&SettingsContent) -> &Option, + include_target_file: bool, ) -> (SettingsFile, Option<&T>) { // todo(settings_ui): Add a metadata field for overriding the "overrides" tag, for contextually different settings // e.g. disable AI isn't overridden, or a vec that gets extended instead or some such @@ -551,10 +570,15 @@ impl SettingsStore { let mut found_file = false; for file in all_files.into_iter() { - if !found_file && file != target_file && file != SettingsFile::Default { - continue; + if !found_file && file != SettingsFile::Default { + if file != target_file { + continue; + } + found_file = true; + if !include_target_file { + continue; + } } - found_file = true; if let SettingsFile::Project((worktree_id, ref path)) = file && let SettingsFile::Project((target_worktree_id, ref target_path)) = target_file diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index e43f881adfc7cb2564a51f60c5a047f403bf9a0b..ad3bd5fd30fba8f44ed9fc91cdf6bf641025d0ff 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -100,8 +100,15 @@ impl Copy for SettingField {} /// Helper for unimplemented settings, used in combination with `SettingField::unimplemented` /// to keep the setting around in the UI with valid pick and pick_mut implementations, but don't actually try to render it. /// TODO(settings_ui): In non-dev builds (`#[cfg(not(debug_assertions))]`) make this render as edit-in-json +#[derive(Clone, Copy)] struct UnimplementedSettingField; +impl PartialEq for UnimplementedSettingField { + fn eq(&self, _other: &Self) -> bool { + true + } +} + impl SettingField { /// Helper for settings with types that are not yet implemented. #[allow(unused)] @@ -119,9 +126,15 @@ trait AnySettingField { fn type_id(&self) -> TypeId; // Returns the file this value was set in and true, or File::Default and false to indicate it was not found in any file (missing default) fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool); + fn reset_to_default_fn( + &self, + current_file: &SettingsUiFile, + file_set_in: &settings::SettingsFile, + cx: &App, + ) -> Option>; } -impl AnySettingField for SettingField { +impl AnySettingField for SettingField { fn as_any(&self) -> &dyn Any { self } @@ -140,6 +153,47 @@ impl AnySettingField for SettingField { .get_value_from_file(file.to_settings(), self.pick); return (file, value.is_some()); } + + fn reset_to_default_fn( + &self, + current_file: &SettingsUiFile, + file_set_in: &settings::SettingsFile, + cx: &App, + ) -> Option> { + if file_set_in == &settings::SettingsFile::Default { + return None; + } + let this = *self; + let store = SettingsStore::global(cx); + let default_value = (this.pick)(store.raw_default_settings()); + let is_default = store + .get_content_for_file(file_set_in.clone()) + .map_or(&None, this.pick) + == default_value; + if is_default { + return None; + } + let current_file = current_file.clone(); + + return Some(Box::new(move |cx| { + let store = SettingsStore::global(cx); + let default_value = (this.pick)(store.raw_default_settings()); + let is_set_somewhere_other_than_default = store + .get_value_up_to_file(current_file.to_settings(), this.pick) + .0 + != settings::SettingsFile::Default; + let value_to_set = if is_set_somewhere_other_than_default { + default_value.clone() + } else { + None + }; + update_settings_file(current_file.clone(), cx, move |settings, _| { + *(this.pick_mut)(settings) = value_to_set; + }) + // todo(settings_ui): Don't log err + .log_err(); + })); + } } #[derive(Default, Clone)] @@ -652,7 +706,7 @@ fn render_settings_item( cx: &mut Context<'_, SettingsWindow>, ) -> Stateful
{ let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx); - let file_set_in = SettingsUiFile::from_settings(found_in_file); + let file_set_in = SettingsUiFile::from_settings(found_in_file.clone()); h_flex() .id(setting_item.title) @@ -667,6 +721,24 @@ fn render_settings_item( .w_full() .gap_1() .child(Label::new(SharedString::new_static(setting_item.title))) + .when_some( + setting_item + .field + .reset_to_default_fn(&file, &found_in_file, cx), + |this, reset_to_default| { + this.child( + IconButton::new("reset-to-default-btn", IconName::Undo) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .on_click({ + move |_, _, cx| { + reset_to_default(cx); + } + }) + .tooltip(Tooltip::text("Reset to default")), + ) + }, + ) .when_some( file_set_in.filter(|file_set_in| file_set_in != &file), |this, file_set_in| { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 8f7ef41108afd22a7f932e8ab6ed1b74078244ec..d7fadbd962a97c83d31438f43e800f9a4ff8c777 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -279,7 +279,7 @@ impl Component for Icon { ) } - fn preview(_window: &mut Window, _cx: &mut App) -> Option { + fn preview(_window: &mut Window, cx: &mut App) -> Option { Some( v_flex() .gap_6() @@ -314,6 +314,30 @@ impl Component for Icon { ), ], ), + example_group_with_title( + "All Icons", + vec![single_example( + "All Icons", + h_flex() + .image_cache(gpui::retain_all("all icons")) + .flex_wrap() + .gap_2() + .children(::iter().map( + |icon_name| { + h_flex() + .gap_1() + .border_1() + .rounded_md() + .px_2() + .py_1() + .border_color(Color::Muted.color(cx)) + .child(SharedString::new_static(icon_name.into())) + .child(Icon::new(icon_name).into_any_element()) + }, + )) + .into_any_element(), + )], + ), ]) .into_any_element(), )