settings_ui: Add Basic Implementation of Language Settings (#37803)

Ben Kunkle and Anthony created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Anthony <anthony@zed.dev>

Change summary

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 +++++++++++++----
crates/ui/src/components/button/toggle_button.rs |  17 
5 files changed, 278 insertions(+), 94 deletions(-)

Detailed changes

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<LanguageName, LanguageSettingsContent>);
 
+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<NonZeroU32>,
     /// 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<Vec<usize>>,
     /// Indent guide related settings.
     #[serde(default)]
@@ -491,6 +524,7 @@ pub struct LanguageSettingsContent {
     ///
     /// Default: auto
     #[serde(default)]
+    #[settings_ui(skip)]
     pub formatter: Option<SelectedFormatter>,
     /// 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<Vec<String>>,
     /// 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<Vec<String>>,
     /// 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<HashMap<String, bool>>,
     /// Whether to perform linked edits of associated ranges, if the language server supports it.
     /// For example, when editing opening <html> tag, the contents of the closing </html> 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<Vec<String>>,
 }
 
 /// 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<String>,
 
     /// 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<String, serde_json::Value>,
 }
 
-#[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)]

crates/settings/src/settings_store.rs 🔗

@@ -601,12 +601,12 @@ impl SettingsStore {
     pub fn update_settings_file_at_path(
         &self,
         fs: Arc<dyn Fs>,
-        path: &[&str],
+        path: &[impl AsRef<str>],
         new_value: serde_json::Value,
     ) -> oneshot::Receiver<Result<()>> {
         let key_path = path
             .into_iter()
-            .cloned()
+            .map(AsRef::as_ref)
             .map(SharedString::new)
             .collect::<Vec<_>>();
         let update = move |mut old_text: String, cx: AsyncApp| {

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<dyn Fn(SettingsValue<serde_json::Value>, &mut Window, &mut App) -> AnyElement>),
+    Custom(Rc<dyn Fn(SettingsValue<serde_json::Value>, &mut Window, &mut App) -> AnyElement>),
 }
 
 pub struct SettingsValue<T> {
-    pub title: &'static str,
-    pub documentation: Option<&'static str>,
-    pub path: SmallVec<[&'static str; 1]>,
+    pub title: SharedString,
+    pub documentation: Option<SharedString>,
+    pub path: SmallVec<[SharedString; 1]>,
     pub value: Option<T>,
     pub default_value: T,
 }
@@ -73,7 +79,7 @@ impl<T> SettingsValue<T> {
 }
 
 impl SettingsValue<serde_json::Value> {
-    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 = <dyn Fs>::global(cx);
 
@@ -90,7 +96,7 @@ impl SettingsValue<serde_json::Value> {
 
 impl<T: serde::Serialize> SettingsValue<T> {
     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<T: serde::Serialize> SettingsValue<T> {
     }
 }
 
-pub struct SettingsUiItemDynamic {
+#[derive(Clone)]
+pub struct SettingsUiItemUnion {
     pub options: Vec<SettingsUiEntry>,
     pub determine_option: fn(&serde_json::Value, &App) -> usize,
 }
 
+pub struct SettingsUiEntryMetaData {
+    pub title: SharedString,
+    pub path: SharedString,
+    pub documentation: Option<SharedString>,
+}
+
+#[derive(Clone)]
+pub struct SettingsUiItemDynamicMap {
+    pub item: fn() -> SettingsUiItem,
+    pub determine_items: fn(&serde_json::Value, &App) -> Vec<SettingsUiEntryMetaData>,
+    pub defaults_path: &'static [&'static str],
+}
+
+#[derive(Clone)]
 pub struct SettingsUiItemGroup {
     pub items: Vec<SettingsUiEntry>,
 }
 
+#[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::<u32>(),
             NumType::F32 => TypeId::of::<f32>(),
             NumType::USIZE => TypeId::of::<usize>(),
+            NumType::U32NONZERO => TypeId::of::<NonZeroU32>(),
         }
     }
 
@@ -160,6 +185,7 @@ impl NumType {
             NumType::U32 => std::any::type_name::<u32>(),
             NumType::F32 => std::any::type_name::<f32>(),
             NumType::USIZE => std::any::type_name::<usize>(),
+            NumType::U32NONZERO => std::any::type_name::<NonZeroU32>(),
         }
     }
 }
@@ -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);

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<SharedString>,
+    documentation: Option<SharedString>,
     _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<fn(&serde_json::Value, &App) -> usize>,
+    generate_items: Option<(
+        SettingsUiItem,
+        fn(&serde_json::Value, &App) -> Vec<SettingsUiEntryMetaData>,
+        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<Vec<&'static str>> {
+    pub fn all_paths(&self, cx: &App) -> Vec<Vec<SharedString>> {
         fn all_paths_rec(
             tree: &[UiEntry],
-            paths: &mut Vec<Vec<&'static str>>,
-            current_path: &mut Vec<&'static str>,
+            paths: &mut Vec<Vec<SharedString>>,
+            current_path: &mut Vec<SharedString>,
             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<Sett
                     settings.settings_tree.active_entry_index = index;
                 }))
                 .child(
-                    Label::new(SharedString::new_static(tree.entries[index].title))
+                    Label::new(tree.entries[index].title.clone())
                         .size(LabelSize::Large)
                         .when(tree.active_entry_index == index, |this| {
                             this.color(Color::Selected)
@@ -361,45 +384,102 @@ fn render_content(
     let mut path = smallvec::smallvec![];
 
     fn render_recursive(
-        tree: &SettingsUiTree,
+        tree: &[UiEntry],
         index: usize,
-        path: &mut SmallVec<[&'static str; 1]>,
+        path: &mut SmallVec<[SharedString; 1]>,
         mut element: Div,
+        // todo(settings_ui): can this be a ref without cx borrow issues?
+        fallback_path: &mut Option<SmallVec<[SharedString; 1]>>,
         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<str>],
 ) -> 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<T: serde::de::DeserializeOwned>(
     settings_value: SettingsValue<serde_json::Value>,
 ) -> SettingsValue<T> {
-    let value = settings_value
-        .value
-        .map(|value| serde_json::from_value::<T>(value).expect("value is not a T"));
+    let value = settings_value.value.map(|value| {
+        serde_json::from_value::<T>(value.clone())
+            .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
+            .with_context(|| format!("value is not a {}: {}", std::any::type_name::<T>(), 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::<T>(settings_value.default_value)
         .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
-        .expect("default value is not an Option<T>");
+        .with_context(|| format!("value is not a {}", std::any::type_name::<T>()))
+        .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::<u64>(
             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::<u32>(
             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::<f32>(
             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::<usize>(
             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::<NonZeroU32>(
+            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<u8> + 'static,
->(
+fn render_numeric_stepper<T: serde::de::DeserializeOwned + std::fmt::Display + Copy + 'static>(
     value: SettingsValue<T>,
-    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<serde_json::Number>,
     _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::<String>(value);
 
     fn make_toggle_group<const LEN: usize>(
-        group_name: &'static str,
         value: SettingsValue<String>,
         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<SharedString>,
     user_settings: &serde_json::Value,
     default_settings: &serde_json::Value,
 ) -> SettingsValue<serde_json::Value> {
     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,
     };

crates/ui/src/components/button/toggle_button.rs 🔗

@@ -425,7 +425,7 @@ pub struct ToggleButtonGroup<T, const COLS: usize = 3, const ROWS: usize = 1>
 where
     T: ButtonBuilder,
 {
-    group_name: &'static str,
+    group_name: SharedString,
     rows: [[T; COLS]; ROWS],
     style: ToggleButtonGroupStyle,
     size: ToggleButtonGroupSize,
@@ -435,9 +435,9 @@ where
 }
 
 impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
-    pub fn single_row(group_name: &'static str, buttons: [T; COLS]) -> Self {
+    pub fn single_row(group_name: impl Into<SharedString>, 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<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
 }
 
 impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS, 2> {
-    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<SharedString>,
+        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<T: ButtonBuilder, const COLS: usize, const ROWS: usize> 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<T: ButtonBuilder, const COLS: usize, const ROWS: usize> 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| {