From 8c9311286903c4e5e6ec26e8b226a86f16b5fa4e Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 8 Sep 2025 19:14:36 -0500 Subject: [PATCH] settings_ui: Add Basic Implementation of Language Settings (#37803) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Anthony --- crates/language/src/language_settings.rs | 60 ++++- crates/settings/src/settings_store.rs | 4 +- crates/settings/src/settings_ui_core.rs | 47 +++- crates/settings_ui/src/settings_ui.rs | 244 +++++++++++++----- .../ui/src/components/button/toggle_button.rs | 17 +- 5 files changed, 278 insertions(+), 94 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index cb519e32ec..33dd6208a8 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -317,7 +317,6 @@ pub struct AllLanguageSettingsContent { pub defaults: LanguageSettingsContent, /// The settings for individual languages. #[serde(default)] - #[settings_ui(skip)] pub languages: LanguageToSettingsMap, /// Settings for associating file extensions and filenames /// with languages. @@ -331,6 +330,37 @@ pub struct AllLanguageSettingsContent { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LanguageToSettingsMap(pub HashMap); +impl SettingsUi for LanguageToSettingsMap { + fn settings_ui_item() -> settings::SettingsUiItem { + settings::SettingsUiItem::DynamicMap(settings::SettingsUiItemDynamicMap { + item: LanguageSettingsContent::settings_ui_item, + defaults_path: &[], + determine_items: |settings_value, cx| { + use settings::SettingsUiEntryMetaData; + + // todo(settings_ui): We should be using a global LanguageRegistry, but it's not implemented yet + _ = cx; + + let Some(settings_language_map) = settings_value.as_object() else { + return Vec::new(); + }; + let mut languages = Vec::with_capacity(settings_language_map.len()); + + for language_name in settings_language_map.keys().map(gpui::SharedString::from) { + languages.push(SettingsUiEntryMetaData { + title: language_name.clone(), + path: language_name, + // todo(settings_ui): Implement documentation for each language + // ideally based on the language's official docs from extension or builtin info + documentation: None, + }); + } + return languages; + }, + }) + } +} + inventory::submit! { ParameterizedJsonSchema { add_and_get_ref: |generator, params, _cx| { @@ -431,11 +461,13 @@ fn default_3() -> usize { /// The settings for a particular language. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)] +#[settings_ui(group = "Default")] pub struct LanguageSettingsContent { /// How many columns a tab should occupy. /// /// Default: 4 #[serde(default)] + #[settings_ui(skip)] pub tab_size: Option, /// Whether to indent lines using tab characters, as opposed to multiple /// spaces. @@ -466,6 +498,7 @@ pub struct LanguageSettingsContent { /// /// Default: [] #[serde(default)] + #[settings_ui(skip)] pub wrap_guides: Option>, /// Indent guide related settings. #[serde(default)] @@ -491,6 +524,7 @@ pub struct LanguageSettingsContent { /// /// Default: auto #[serde(default)] + #[settings_ui(skip)] pub formatter: Option, /// Zed's Prettier integration settings. /// Allows to enable/disable formatting with Prettier @@ -516,6 +550,7 @@ pub struct LanguageSettingsContent { /// /// Default: ["..."] #[serde(default)] + #[settings_ui(skip)] pub language_servers: Option>, /// Controls where the `editor::Rewrap` action is allowed for this language. /// @@ -538,6 +573,7 @@ pub struct LanguageSettingsContent { /// /// Default: [] #[serde(default)] + #[settings_ui(skip)] pub edit_predictions_disabled_in: Option>, /// Whether to show tabs and spaces in the editor. #[serde(default)] @@ -577,6 +613,7 @@ pub struct LanguageSettingsContent { /// These are not run if formatting is off. /// /// Default: {} (or {"source.organizeImports": true} for Go). + #[settings_ui(skip)] pub code_actions_on_format: Option>, /// Whether to perform linked edits of associated ranges, if the language server supports it. /// For example, when editing opening tag, the contents of the closing tag will be edited as well. @@ -610,11 +647,14 @@ pub struct LanguageSettingsContent { /// Preferred debuggers for this language. /// /// Default: [] + #[settings_ui(skip)] pub debuggers: Option>, } /// The behavior of `editor::Rewrap`. -#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] +#[derive( + Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, SettingsUi, +)] #[serde(rename_all = "snake_case")] pub enum RewrapBehavior { /// Only rewrap within comments. @@ -696,7 +736,7 @@ pub enum SoftWrap { } /// Controls the behavior of formatting files when they are saved. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, SettingsUi)] pub enum FormatOnSave { /// Files should be formatted on save. On, @@ -795,7 +835,7 @@ impl<'de> Deserialize<'de> for FormatOnSave { } /// Controls how whitespace should be displayedin the editor. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)] #[serde(rename_all = "snake_case")] pub enum ShowWhitespaceSetting { /// Draw whitespace only for the selected text. @@ -816,7 +856,7 @@ pub enum ShowWhitespaceSetting { } /// Controls which formatter should be used when formatting code. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, SettingsUi)] pub enum SelectedFormatter { /// Format files using Zed's Prettier integration (if applicable), /// or falling back to formatting via language server. @@ -1012,7 +1052,7 @@ pub enum IndentGuideBackgroundColoring { } /// The settings for inlay hints. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)] pub struct InlayHintSettings { /// Global switch to toggle hints on and off. /// @@ -1079,7 +1119,7 @@ fn scroll_debounce_ms() -> u64 { } /// The task settings for a particular language. -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema, SettingsUi)] pub struct LanguageTaskConfig { /// Extra task variables to set for a particular language. #[serde(default)] @@ -1622,7 +1662,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent /// Allows to enable/disable formatting with Prettier /// and configure default Prettier, used when no project-level Prettier installation is found. /// Prettier formatting is disabled by default. -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct PrettierSettings { /// Enables or disables formatting with Prettier for a given language. #[serde(default)] @@ -1635,15 +1675,17 @@ pub struct PrettierSettings { /// Forces Prettier integration to use specific plugins when formatting files with the language. /// The default Prettier will be installed with these plugins. #[serde(default)] + #[settings_ui(skip)] pub plugins: HashSet, /// Default Prettier options, in the format as in package.json section for Prettier. /// If project installs Prettier via its package.json, these options will be ignored. #[serde(flatten)] + #[settings_ui(skip)] pub options: HashMap, } -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)] pub struct JsxTagAutoCloseSettings { /// Enables or disables auto-closing of JSX tags. #[serde(default)] diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index cc0ebf10cd..f227071193 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -601,12 +601,12 @@ impl SettingsStore { pub fn update_settings_file_at_path( &self, fs: Arc, - path: &[&str], + path: &[impl AsRef], new_value: serde_json::Value, ) -> oneshot::Receiver> { let key_path = path .into_iter() - .cloned() + .map(AsRef::as_ref) .map(SharedString::new) .collect::>(); let update = move |mut old_text: String, cx: AsyncApp| { diff --git a/crates/settings/src/settings_ui_core.rs b/crates/settings/src/settings_ui_core.rs index e056dfbd5a..09505e63a3 100644 --- a/crates/settings/src/settings_ui_core.rs +++ b/crates/settings/src/settings_ui_core.rs @@ -1,8 +1,12 @@ -use std::any::TypeId; +use std::{ + any::TypeId, + num::{NonZeroU32, NonZeroUsize}, + rc::Rc, +}; use anyhow::Context as _; use fs::Fs; -use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, Window}; +use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, SharedString, Window}; use smallvec::SmallVec; use crate::SettingsStore; @@ -24,6 +28,7 @@ pub trait SettingsUi { } } +#[derive(Clone)] pub struct SettingsUiEntry { /// The path in the settings JSON file for this setting. Relative to parent /// None implies `#[serde(flatten)]` or `Settings::KEY.is_none()` for top level settings @@ -35,6 +40,7 @@ pub struct SettingsUiEntry { pub item: SettingsUiItem, } +#[derive(Clone)] pub enum SettingsUiItemSingle { SwitchField, /// A numeric stepper for a specific type of number @@ -52,13 +58,13 @@ pub enum SettingsUiItemSingle { /// Must be the same length as `variants` labels: &'static [&'static str], }, - Custom(Box, &mut Window, &mut App) -> AnyElement>), + Custom(Rc, &mut Window, &mut App) -> AnyElement>), } pub struct SettingsValue { - pub title: &'static str, - pub documentation: Option<&'static str>, - pub path: SmallVec<[&'static str; 1]>, + pub title: SharedString, + pub documentation: Option, + pub path: SmallVec<[SharedString; 1]>, pub value: Option, pub default_value: T, } @@ -73,7 +79,7 @@ impl SettingsValue { } impl SettingsValue { - pub fn write_value(path: &SmallVec<[&'static str; 1]>, value: serde_json::Value, cx: &mut App) { + pub fn write_value(path: &SmallVec<[SharedString; 1]>, value: serde_json::Value, cx: &mut App) { let settings_store = SettingsStore::global(cx); let fs = ::global(cx); @@ -90,7 +96,7 @@ impl SettingsValue { impl SettingsValue { pub fn write( - path: &SmallVec<[&'static str; 1]>, + path: &SmallVec<[SharedString; 1]>, value: T, cx: &mut App, ) -> Result<(), serde_json::Error> { @@ -99,19 +105,36 @@ impl SettingsValue { } } -pub struct SettingsUiItemDynamic { +#[derive(Clone)] +pub struct SettingsUiItemUnion { pub options: Vec, pub determine_option: fn(&serde_json::Value, &App) -> usize, } +pub struct SettingsUiEntryMetaData { + pub title: SharedString, + pub path: SharedString, + pub documentation: Option, +} + +#[derive(Clone)] +pub struct SettingsUiItemDynamicMap { + pub item: fn() -> SettingsUiItem, + pub determine_items: fn(&serde_json::Value, &App) -> Vec, + pub defaults_path: &'static [&'static str], +} + +#[derive(Clone)] pub struct SettingsUiItemGroup { pub items: Vec, } +#[derive(Clone)] pub enum SettingsUiItem { Group(SettingsUiItemGroup), Single(SettingsUiItemSingle), - Dynamic(SettingsUiItemDynamic), + Union(SettingsUiItemUnion), + DynamicMap(SettingsUiItemDynamicMap), None, } @@ -134,6 +157,7 @@ pub enum NumType { U32 = 1, F32 = 2, USIZE = 3, + U32NONZERO = 4, } pub static NUM_TYPE_NAMES: std::sync::LazyLock<[&'static str; NumType::COUNT]> = @@ -151,6 +175,7 @@ impl NumType { NumType::U32 => TypeId::of::(), NumType::F32 => TypeId::of::(), NumType::USIZE => TypeId::of::(), + NumType::U32NONZERO => TypeId::of::(), } } @@ -160,6 +185,7 @@ impl NumType { NumType::U32 => std::any::type_name::(), NumType::F32 => std::any::type_name::(), NumType::USIZE => std::any::type_name::(), + NumType::U32NONZERO => std::any::type_name::(), } } } @@ -185,3 +211,4 @@ numeric_stepper_for_num_type!(u32, U32); // todo(settings_ui) is there a better ui for f32? numeric_stepper_for_num_type!(f32, F32); numeric_stepper_for_num_type!(usize, USIZE); +numeric_stepper_for_num_type!(NonZeroUsize, U32NONZERO); diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 5fea6dfceb..9866dcdb3e 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1,6 +1,7 @@ mod appearance_settings_controls; use std::any::TypeId; +use std::num::NonZeroU32; use std::ops::{Not, Range}; use anyhow::Context as _; @@ -9,8 +10,9 @@ use editor::EditorSettingsControls; use feature_flags::{FeatureFlag, FeatureFlagViewExt}; use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions}; use settings::{ - NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic, - SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue, + NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem, + SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle, SettingsUiItemUnion, + SettingsValue, }; use smallvec::SmallVec; use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*}; @@ -135,9 +137,9 @@ impl Item for SettingsPage { // - Do we want to show the parent groups when a item is matched? struct UiEntry { - title: &'static str, - path: Option<&'static str>, - documentation: Option<&'static str>, + title: SharedString, + path: Option, + documentation: Option, _depth: usize, // a // b < a descendant range < a total descendant range @@ -154,6 +156,11 @@ struct UiEntry { /// For dynamic items this is a way to select a value from a list of values /// this is always none for non-dynamic items select_descendant: Option usize>, + generate_items: Option<( + SettingsUiItem, + fn(&serde_json::Value, &App) -> Vec, + SmallVec<[SharedString; 1]>, + )>, } impl UiEntry { @@ -193,15 +200,16 @@ fn build_tree_item( ) { let index = tree.len(); tree.push(UiEntry { - title: entry.title, - path: entry.path, - documentation: entry.documentation, + title: entry.title.into(), + path: entry.path.map(SharedString::new_static), + documentation: entry.documentation.map(SharedString::new_static), _depth: depth, descendant_range: index + 1..index + 1, total_descendant_range: index + 1..index + 1, render: None, next_sibling: None, select_descendant: None, + generate_items: None, }); if let Some(prev_index) = prev_index { tree[prev_index].next_sibling = Some(index); @@ -222,7 +230,7 @@ fn build_tree_item( SettingsUiItem::Single(item) => { tree[index].render = Some(item); } - SettingsUiItem::Dynamic(SettingsUiItemDynamic { + SettingsUiItem::Union(SettingsUiItemUnion { options, determine_option, }) => { @@ -238,6 +246,21 @@ fn build_tree_item( tree[index].total_descendant_range.end = tree.len(); } } + SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap { + item: generate_settings_ui_item, + determine_items, + defaults_path, + }) => { + tree[index].generate_items = Some(( + generate_settings_ui_item(), + determine_items, + defaults_path + .into_iter() + .copied() + .map(SharedString::new_static) + .collect(), + )); + } SettingsUiItem::None => { return; } @@ -263,7 +286,7 @@ impl SettingsUiTree { build_tree_item(&mut tree, item, 0, prev_root_entry_index); } - root_entry_indices.sort_by_key(|i| tree[*i].title); + root_entry_indices.sort_by_key(|i| &tree[*i].title); let active_entry_index = root_entry_indices[0]; Self { @@ -276,18 +299,18 @@ impl SettingsUiTree { // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree, // so that we can keep none/skip and still test in CI that all settings have #[cfg(feature = "test-support")] - pub fn all_paths(&self, cx: &App) -> Vec> { + pub fn all_paths(&self, cx: &App) -> Vec> { fn all_paths_rec( tree: &[UiEntry], - paths: &mut Vec>, - current_path: &mut Vec<&'static str>, + paths: &mut Vec>, + current_path: &mut Vec, idx: usize, cx: &App, ) { let child = &tree[idx]; let mut pushed_path = false; if let Some(path) = child.path.as_ref() { - current_path.push(path); + current_path.push(path.clone()); paths.push(current_path.clone()); pushed_path = true; } @@ -340,7 +363,7 @@ fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context, + path: &mut SmallVec<[SharedString; 1]>, mut element: Div, + // todo(settings_ui): can this be a ref without cx borrow issues? + fallback_path: &mut Option>, window: &mut Window, cx: &mut App, ) -> Div { - let Some(child) = tree.entries.get(index) else { + let Some(child) = tree.get(index) else { return element.child( Label::new(SharedString::new_static("No settings found")).color(Color::Error), ); }; - element = - element.child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large)); + element = element.child(Label::new(child.title.clone()).size(LabelSize::Large)); // todo(settings_ui): subgroups? let mut pushed_path = false; - if let Some(child_path) = child.path { - path.push(child_path); + if let Some(child_path) = child.path.as_ref() { + path.push(child_path.clone()); + if let Some(fallback_path) = fallback_path.as_mut() { + fallback_path.push(child_path.clone()); + } pushed_path = true; } + // let fallback_path_copy = fallback_path.cloned(); let settings_value = settings_value_from_settings_and_path( path.clone(), - child.title, - child.documentation, + fallback_path.as_ref().map(|path| path.as_slice()), + child.title.clone(), + child.documentation.clone(), // PERF: how to structure this better? There feels like there's a way to avoid the clone // and every value lookup SettingsStore::global(cx).raw_user_settings(), SettingsStore::global(cx).raw_default_settings(), ); if let Some(select_descendant) = child.select_descendant { - let selected_descendant = child - .nth_descendant_index(&tree.entries, select_descendant(settings_value.read(), cx)); + let selected_descendant = + child.nth_descendant_index(tree, select_descendant(settings_value.read(), cx)); if let Some(descendant_index) = selected_descendant { - element = render_recursive(&tree, descendant_index, path, element, window, cx); + element = render_recursive( + tree, + descendant_index, + path, + element, + fallback_path, + window, + cx, + ); } - } - if let Some(child_render) = child.render.as_ref() { + } else if let Some((settings_ui_item, generate_items, defaults_path)) = + child.generate_items.as_ref() + { + let generated_items = generate_items(settings_value.read(), cx); + let mut ui_items = Vec::with_capacity(generated_items.len()); + for item in generated_items { + let settings_ui_entry = SettingsUiEntry { + path: None, + title: "", + documentation: None, + item: settings_ui_item.clone(), + }; + let prev_index = if ui_items.is_empty() { + None + } else { + Some(ui_items.len() - 1) + }; + let item_index = ui_items.len(); + build_tree_item( + &mut ui_items, + settings_ui_entry, + child._depth + 1, + prev_index, + ); + if item_index < ui_items.len() { + ui_items[item_index].path = None; + ui_items[item_index].title = item.title.clone(); + ui_items[item_index].documentation = item.documentation.clone(); + + // push path instead of setting path on ui item so that the path isn't pushed to default_path as well + // when we recurse + path.push(item.path.clone()); + element = render_recursive( + &ui_items, + item_index, + path, + element, + &mut Some(defaults_path.clone()), + window, + cx, + ); + path.pop(); + } + } + } else if let Some(child_render) = child.render.as_ref() { element = element.child(div().child(render_item_single( settings_value, child_render, @@ -409,8 +489,16 @@ fn render_content( } else if let Some(child_index) = child.first_descendant_index() { let mut index = Some(child_index); while let Some(sub_child_index) = index { - element = render_recursive(tree, sub_child_index, path, element, window, cx); - index = tree.entries[sub_child_index].next_sibling; + element = render_recursive( + tree, + sub_child_index, + path, + element, + fallback_path, + window, + cx, + ); + index = tree[sub_child_index].next_sibling; } } else { element = @@ -419,15 +507,19 @@ fn render_content( if pushed_path { path.pop(); + if let Some(fallback_path) = fallback_path.as_mut() { + fallback_path.pop(); + } } return element; } return render_recursive( - tree, + &tree.entries, tree.active_entry_index, &mut path, content, + &mut None, window, cx, ); @@ -484,15 +576,15 @@ fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement { ) } -fn element_id_from_path(path: &[&'static str]) -> ElementId { +fn element_id_from_path(path: &[SharedString]) -> ElementId { if path.len() == 0 { panic!("Path length must not be zero"); } else if path.len() == 1 { - ElementId::Name(SharedString::new_static(path[0])) + ElementId::Name(path[0].clone()) } else { ElementId::from(( - ElementId::from(SharedString::new_static(path[path.len() - 2])), - SharedString::new_static(path[path.len() - 1]), + ElementId::from(path[path.len() - 2].clone()), + path[path.len() - 1].clone(), )) } } @@ -525,13 +617,13 @@ fn render_item_single( pub fn read_settings_value_from_path<'a>( settings_contents: &'a serde_json::Value, - path: &[&str], + path: &[impl AsRef], ) -> Option<&'a serde_json::Value> { // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested let Some((key, remaining)) = path.split_first() else { return Some(settings_contents); }; - let Some(value) = settings_contents.get(key) else { + let Some(value) = settings_contents.get(key.as_ref()) else { return None; }; @@ -541,13 +633,17 @@ pub fn read_settings_value_from_path<'a>( fn downcast_any_item( settings_value: SettingsValue, ) -> SettingsValue { - let value = settings_value - .value - .map(|value| serde_json::from_value::(value).expect("value is not a T")); + let value = settings_value.value.map(|value| { + serde_json::from_value::(value.clone()) + .with_context(|| format!("path: {:?}", settings_value.path.join("."))) + .with_context(|| format!("value is not a {}: {}", std::any::type_name::(), value)) + .unwrap() + }); // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values let default_value = serde_json::from_value::(settings_value.default_value) .with_context(|| format!("path: {:?}", settings_value.path.join("."))) - .expect("default value is not an Option"); + .with_context(|| format!("value is not a {}", std::any::type_name::())) + .unwrap(); let deserialized_setting_value = SettingsValue { title: settings_value.title, path: settings_value.path, @@ -577,8 +673,8 @@ fn render_any_numeric_stepper( match num_type { NumType::U64 => render_numeric_stepper::( downcast_any_item(settings_value), - u64::saturating_sub, - u64::saturating_add, + |n| u64::saturating_sub(n, 1), + |n| u64::saturating_add(n, 1), |n| { serde_json::Number::try_from(n) .context("Failed to convert u64 to serde_json::Number") @@ -588,8 +684,8 @@ fn render_any_numeric_stepper( ), NumType::U32 => render_numeric_stepper::( downcast_any_item(settings_value), - u32::saturating_sub, - u32::saturating_add, + |n| u32::saturating_sub(n, 1), + |n| u32::saturating_add(n, 1), |n| { serde_json::Number::try_from(n) .context("Failed to convert u32 to serde_json::Number") @@ -599,8 +695,8 @@ fn render_any_numeric_stepper( ), NumType::F32 => render_numeric_stepper::( downcast_any_item(settings_value), - |a, b| a - b, - |a, b| a + b, + |a| a - 1.0, + |a| a + 1.0, |n| { serde_json::Number::from_f64(n as f64) .context("Failed to convert f32 to serde_json::Number") @@ -610,8 +706,8 @@ fn render_any_numeric_stepper( ), NumType::USIZE => render_numeric_stepper::( downcast_any_item(settings_value), - usize::saturating_sub, - usize::saturating_add, + |n| usize::saturating_sub(n, 1), + |n| usize::saturating_add(n, 1), |n| { serde_json::Number::try_from(n) .context("Failed to convert usize to serde_json::Number") @@ -619,15 +715,24 @@ fn render_any_numeric_stepper( window, cx, ), + NumType::U32NONZERO => render_numeric_stepper::( + downcast_any_item(settings_value), + |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN), + |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX), + |n| { + serde_json::Number::try_from(n.get()) + .context("Failed to convert usize to serde_json::Number") + }, + window, + cx, + ), } } -fn render_numeric_stepper< - T: serde::de::DeserializeOwned + std::fmt::Display + Copy + From + 'static, ->( +fn render_numeric_stepper( value: SettingsValue, - saturating_sub: fn(T, T) -> T, - saturating_add: fn(T, T) -> T, + saturating_sub_1: fn(T) -> T, + saturating_add_1: fn(T) -> T, to_serde_number: fn(T) -> anyhow::Result, _window: &mut Window, _cx: &mut App, @@ -640,9 +745,9 @@ fn render_numeric_stepper< id, num.to_string(), { - let path = value.path.clone(); + let path = value.path; move |_, _, cx| { - let Some(number) = to_serde_number(saturating_sub(num, 1.into())).ok() else { + let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else { return; }; let new_value = serde_json::Value::Number(number); @@ -650,7 +755,7 @@ fn render_numeric_stepper< } }, move |_, _, cx| { - let Some(number) = to_serde_number(saturating_add(num, 1.into())).ok() else { + let Some(number) = to_serde_number(saturating_add_1(num)).ok() else { return; }; @@ -672,8 +777,8 @@ fn render_switch_field( let path = value.path.clone(); SwitchField::new( id, - SharedString::new_static(value.title), - value.documentation.map(SharedString::new_static), + value.title.clone(), + value.documentation.clone(), match value.read() { true => ToggleState::Selected, false => ToggleState::Unselected, @@ -703,7 +808,6 @@ fn render_toggle_button_group( let value = downcast_any_item::(value); fn make_toggle_group( - group_name: &'static str, value: SettingsValue, variants: &'static [&'static str], labels: &'static [&'static str], @@ -727,7 +831,7 @@ fn render_toggle_button_group( let mut idx = 0; ToggleButtonGroup::single_row( - group_name, + value.title.clone(), variants_array.map(|(variant, label)| { let path = value.path.clone(); idx += 1; @@ -748,7 +852,7 @@ fn render_toggle_button_group( macro_rules! templ_toggl_with_const_param { ($len:expr) => { if variants.len() == $len { - return make_toggle_group::<$len>(value.title, value, variants, labels); + return make_toggle_group::<$len>(value, variants, labels); } }; } @@ -762,13 +866,19 @@ fn render_toggle_button_group( } fn settings_value_from_settings_and_path( - path: SmallVec<[&'static str; 1]>, - title: &'static str, - documentation: Option<&'static str>, + path: SmallVec<[SharedString; 1]>, + fallback_path: Option<&[SharedString]>, + title: SharedString, + documentation: Option, user_settings: &serde_json::Value, default_settings: &serde_json::Value, ) -> SettingsValue { let default_value = read_settings_value_from_path(default_settings, &path) + .or_else(|| { + fallback_path.and_then(|fallback_path| { + read_settings_value_from_path(default_settings, fallback_path) + }) + }) .with_context(|| format!("No default value for item at path {:?}", path.join("."))) .expect("Default value set for item") .clone(); @@ -778,7 +888,7 @@ fn settings_value_from_settings_and_path( default_value, value, documentation, - path: path.clone(), + path, // todo(settings_ui) is title required inside SettingsValue? title, }; diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index 2a862f4876..a50918b109 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -425,7 +425,7 @@ pub struct ToggleButtonGroup where T: ButtonBuilder, { - group_name: &'static str, + group_name: SharedString, rows: [[T; COLS]; ROWS], style: ToggleButtonGroupStyle, size: ToggleButtonGroupSize, @@ -435,9 +435,9 @@ where } impl ToggleButtonGroup { - pub fn single_row(group_name: &'static str, buttons: [T; COLS]) -> Self { + pub fn single_row(group_name: impl Into, buttons: [T; COLS]) -> Self { Self { - group_name, + group_name: group_name.into(), rows: [buttons], style: ToggleButtonGroupStyle::Transparent, size: ToggleButtonGroupSize::Default, @@ -449,9 +449,13 @@ impl ToggleButtonGroup { } impl ToggleButtonGroup { - pub fn two_rows(group_name: &'static str, first_row: [T; COLS], second_row: [T; COLS]) -> Self { + pub fn two_rows( + group_name: impl Into, + first_row: [T; COLS], + second_row: [T; COLS], + ) -> Self { Self { - group_name, + group_name: group_name.into(), rows: [first_row, second_row], style: ToggleButtonGroupStyle::Transparent, size: ToggleButtonGroupSize::Default, @@ -512,6 +516,7 @@ impl RenderOnce fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let entries = self.rows.into_iter().enumerate().map(|(row_index, row)| { + let group_name = self.group_name.clone(); row.into_iter().enumerate().map(move |(col_index, button)| { let ButtonConfiguration { label, @@ -523,7 +528,7 @@ impl RenderOnce let entry_index = row_index * COLS + col_index; - ButtonLike::new((self.group_name, entry_index)) + ButtonLike::new((group_name.clone(), entry_index)) .full_width() .rounding(None) .when_some(self.tab_index, |this, tab_index| {