settings_ui.rs

   1mod appearance_settings_controls;
   2
   3use std::{
   4    num::NonZeroU32,
   5    ops::{Not, Range},
   6    rc::Rc,
   7};
   8
   9use anyhow::Context as _;
  10use editor::{Editor, EditorSettingsControls};
  11use feature_flags::{FeatureFlag, FeatureFlagAppExt};
  12use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
  13use settings::{
  14    NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem,
  15    SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle, SettingsUiItemUnion,
  16    SettingsValue,
  17};
  18use smallvec::SmallVec;
  19use ui::{
  20    ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple,
  21    prelude::*,
  22};
  23use workspace::{
  24    Workspace,
  25    item::{Item, ItemEvent},
  26};
  27
  28use crate::appearance_settings_controls::AppearanceSettingsControls;
  29
  30pub struct SettingsUiFeatureFlag;
  31
  32impl FeatureFlag for SettingsUiFeatureFlag {
  33    const NAME: &'static str = "settings-ui";
  34}
  35
  36actions!(
  37    zed,
  38    [
  39        /// Opens settings UI.
  40        OpenSettingsUi
  41    ]
  42);
  43
  44pub fn open_settings_editor(
  45    workspace: &mut Workspace,
  46    _: &OpenSettingsUi,
  47    window: &mut Window,
  48    cx: &mut Context<Workspace>,
  49) {
  50    // todo(settings_ui) open in a local workspace if this is remote.
  51    let existing = workspace
  52        .active_pane()
  53        .read(cx)
  54        .items()
  55        .find_map(|item| item.downcast::<SettingsPage>());
  56
  57    if let Some(existing) = existing {
  58        workspace.activate_item(&existing, true, true, window, cx);
  59    } else {
  60        let settings_page = SettingsPage::new(workspace, cx);
  61        workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
  62    }
  63}
  64
  65pub fn init(cx: &mut App) {
  66    cx.observe_new(|workspace: &mut Workspace, _, _| {
  67        workspace.register_action_renderer(|div, _, _, cx| {
  68            let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsUi>()];
  69            let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
  70            command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
  71                if has_flag {
  72                    filter.show_action_types(&settings_ui_actions);
  73                } else {
  74                    filter.hide_action_types(&settings_ui_actions);
  75                }
  76            });
  77            if has_flag {
  78                div.on_action(cx.listener(open_settings_editor))
  79            } else {
  80                div
  81            }
  82        });
  83    })
  84    .detach();
  85}
  86
  87pub struct SettingsPage {
  88    focus_handle: FocusHandle,
  89    settings_tree: SettingsUiTree,
  90}
  91
  92impl SettingsPage {
  93    pub fn new(_workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
  94        cx.new(|cx| Self {
  95            focus_handle: cx.focus_handle(),
  96            settings_tree: SettingsUiTree::new(cx),
  97        })
  98    }
  99}
 100
 101impl EventEmitter<ItemEvent> for SettingsPage {}
 102
 103impl Focusable for SettingsPage {
 104    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 105        self.focus_handle.clone()
 106    }
 107}
 108
 109impl Item for SettingsPage {
 110    type Event = ItemEvent;
 111
 112    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 113        Some(Icon::new(IconName::Settings))
 114    }
 115
 116    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 117        "Settings".into()
 118    }
 119
 120    fn show_toolbar(&self) -> bool {
 121        false
 122    }
 123
 124    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
 125        f(*event)
 126    }
 127}
 128
 129// We want to iterate over the side bar with root groups
 130// - this is a loop over top level groups, and if any are expanded, recursively displaying their items
 131// - Should be able to get all items from a group (flatten a group)
 132// - Should be able to toggle/untoggle groups in UI (at least in sidebar)
 133// - Search should be available
 134//  - there should be an index of text -> item mappings, for using fuzzy::match
 135//   - Do we want to show the parent groups when a item is matched?
 136
 137struct UiEntry {
 138    title: SharedString,
 139    path: Option<SharedString>,
 140    documentation: Option<SharedString>,
 141    _depth: usize,
 142    // a
 143    //  b     < a descendant range < a total descendant range
 144    //    f   |                    |
 145    //    g   |                    |
 146    //  c     <                    |
 147    //    d                        |
 148    //    e                        <
 149    descendant_range: Range<usize>,
 150    total_descendant_range: Range<usize>,
 151    next_sibling: Option<usize>,
 152    // expanded: bool,
 153    render: Option<SettingsUiItemSingle>,
 154    dynamic_render: Option<SettingsUiItemUnion>,
 155    generate_items: Option<(
 156        SettingsUiItem,
 157        fn(&serde_json::Value, &App) -> Vec<SettingsUiEntryMetaData>,
 158        SmallVec<[SharedString; 1]>,
 159    )>,
 160}
 161
 162impl UiEntry {
 163    fn first_descendant_index(&self) -> Option<usize> {
 164        return self
 165            .descendant_range
 166            .is_empty()
 167            .not()
 168            .then_some(self.descendant_range.start);
 169    }
 170
 171    fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option<usize> {
 172        let first_descendant_index = self.first_descendant_index()?;
 173        let mut current_index = 0;
 174        let mut current_descendant_index = Some(first_descendant_index);
 175        while let Some(descendant_index) = current_descendant_index
 176            && current_index < n
 177        {
 178            current_index += 1;
 179            current_descendant_index = tree[descendant_index].next_sibling;
 180        }
 181        current_descendant_index
 182    }
 183}
 184
 185pub struct SettingsUiTree {
 186    root_entry_indices: Vec<usize>,
 187    entries: Vec<UiEntry>,
 188    active_entry_index: usize,
 189}
 190
 191fn build_tree_item(
 192    tree: &mut Vec<UiEntry>,
 193    entry: SettingsUiEntry,
 194    depth: usize,
 195    prev_index: Option<usize>,
 196) {
 197    // let tree: HashMap<Path, UiEntry>;
 198    let index = tree.len();
 199    tree.push(UiEntry {
 200        title: entry.title.into(),
 201        path: entry.path.map(SharedString::new_static),
 202        documentation: entry.documentation.map(SharedString::new_static),
 203        _depth: depth,
 204        descendant_range: index + 1..index + 1,
 205        total_descendant_range: index + 1..index + 1,
 206        render: None,
 207        next_sibling: None,
 208        dynamic_render: None,
 209        generate_items: None,
 210    });
 211    if let Some(prev_index) = prev_index {
 212        tree[prev_index].next_sibling = Some(index);
 213    }
 214    match entry.item {
 215        SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => {
 216            for group_item in group_items {
 217                let prev_index = tree[index]
 218                    .descendant_range
 219                    .is_empty()
 220                    .not()
 221                    .then_some(tree[index].descendant_range.end - 1);
 222                tree[index].descendant_range.end = tree.len() + 1;
 223                build_tree_item(tree, group_item, depth + 1, prev_index);
 224                tree[index].total_descendant_range.end = tree.len();
 225            }
 226        }
 227        SettingsUiItem::Single(item) => {
 228            tree[index].render = Some(item);
 229        }
 230        SettingsUiItem::Union(dynamic_render) => {
 231            // todo(settings_ui) take from item and store other fields instead of clone
 232            // will also require replacing usage in render_recursive so it can know
 233            // which options were actually rendered
 234            let options = dynamic_render.options.clone();
 235            tree[index].dynamic_render = Some(dynamic_render);
 236            for option in options {
 237                let Some(option) = option else { continue };
 238                let prev_index = tree[index]
 239                    .descendant_range
 240                    .is_empty()
 241                    .not()
 242                    .then_some(tree[index].descendant_range.end - 1);
 243                tree[index].descendant_range.end = tree.len() + 1;
 244                build_tree_item(tree, option, depth + 1, prev_index);
 245                tree[index].total_descendant_range.end = tree.len();
 246            }
 247        }
 248        SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap {
 249            item: generate_settings_ui_item,
 250            determine_items,
 251            defaults_path,
 252        }) => {
 253            tree[index].generate_items = Some((
 254                generate_settings_ui_item(),
 255                determine_items,
 256                defaults_path
 257                    .into_iter()
 258                    .copied()
 259                    .map(SharedString::new_static)
 260                    .collect(),
 261            ));
 262        }
 263        SettingsUiItem::None => {
 264            return;
 265        }
 266    }
 267}
 268
 269impl SettingsUiTree {
 270    pub fn new(cx: &App) -> Self {
 271        let settings_store = SettingsStore::global(cx);
 272        let mut tree = vec![];
 273        let mut root_entry_indices = vec![];
 274        for item in settings_store.settings_ui_items() {
 275            if matches!(item.item, SettingsUiItem::None)
 276            // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to
 277            // link them to other groups
 278            || matches!(item.item, SettingsUiItem::Single(_))
 279            {
 280                continue;
 281            }
 282
 283            let prev_root_entry_index = root_entry_indices.last().copied();
 284            root_entry_indices.push(tree.len());
 285            build_tree_item(&mut tree, item, 0, prev_root_entry_index);
 286        }
 287
 288        root_entry_indices.sort_by_key(|i| &tree[*i].title);
 289
 290        let active_entry_index = root_entry_indices[0];
 291        Self {
 292            entries: tree,
 293            root_entry_indices,
 294            active_entry_index,
 295        }
 296    }
 297
 298    // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree,
 299    // so that we can keep none/skip and still test in CI that all settings have
 300    #[cfg(feature = "test-support")]
 301    pub fn all_paths(&self, cx: &App) -> Vec<Vec<SharedString>> {
 302        fn all_paths_rec(
 303            tree: &[UiEntry],
 304            paths: &mut Vec<Vec<SharedString>>,
 305            current_path: &mut Vec<SharedString>,
 306            idx: usize,
 307            cx: &App,
 308        ) {
 309            let child = &tree[idx];
 310            let mut pushed_path = false;
 311            if let Some(path) = child.path.as_ref() {
 312                current_path.push(path.clone());
 313                paths.push(current_path.clone());
 314                pushed_path = true;
 315            }
 316            // todo(settings_ui): handle dynamic nodes here
 317            let selected_descendant_index = child
 318                .dynamic_render
 319                .as_ref()
 320                .map(|dynamic_render| {
 321                    read_settings_value_from_path(
 322                        SettingsStore::global(cx).raw_default_settings(),
 323                        &current_path,
 324                    )
 325                    .map(|value| (dynamic_render.determine_option)(value, cx))
 326                })
 327                .and_then(|selected_descendant_index| {
 328                    selected_descendant_index.map(|index| child.nth_descendant_index(tree, index))
 329                });
 330
 331            if let Some(selected_descendant_index) = selected_descendant_index {
 332                // just silently fail if we didn't find a setting value for the path
 333                if let Some(descendant_index) = selected_descendant_index {
 334                    all_paths_rec(tree, paths, current_path, descendant_index, cx);
 335                }
 336            } else if let Some(desc_idx) = child.first_descendant_index() {
 337                let mut desc_idx = Some(desc_idx);
 338                while let Some(descendant_index) = desc_idx {
 339                    all_paths_rec(&tree, paths, current_path, descendant_index, cx);
 340                    desc_idx = tree[descendant_index].next_sibling;
 341                }
 342            }
 343            if pushed_path {
 344                current_path.pop();
 345            }
 346        }
 347
 348        let mut paths = Vec::new();
 349        for &index in &self.root_entry_indices {
 350            all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx);
 351        }
 352        paths
 353    }
 354}
 355
 356fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<SettingsPage>) -> Div {
 357    let mut nav = v_flex().p_4().gap_2();
 358    for &index in &tree.root_entry_indices {
 359        nav = nav.child(
 360            div()
 361                .id(index)
 362                .on_click(cx.listener(move |settings, _, _, _| {
 363                    settings.settings_tree.active_entry_index = index;
 364                }))
 365                .child(
 366                    Label::new(tree.entries[index].title.clone())
 367                        .size(LabelSize::Large)
 368                        .when(tree.active_entry_index == index, |this| {
 369                            this.color(Color::Selected)
 370                        }),
 371                ),
 372        );
 373    }
 374    nav
 375}
 376
 377fn render_content(
 378    tree: &SettingsUiTree,
 379    window: &mut Window,
 380    cx: &mut Context<SettingsPage>,
 381) -> Div {
 382    let content = v_flex().size_full().gap_4();
 383
 384    let mut path = smallvec::smallvec![];
 385
 386    return render_recursive(
 387        &tree.entries,
 388        tree.active_entry_index,
 389        &mut path,
 390        content,
 391        &mut None,
 392        true,
 393        window,
 394        cx,
 395    );
 396}
 397
 398fn render_recursive(
 399    tree: &[UiEntry],
 400    index: usize,
 401    path: &mut SmallVec<[SharedString; 1]>,
 402    mut element: Div,
 403    fallback_path: &mut Option<SmallVec<[SharedString; 1]>>,
 404    render_next_title: bool,
 405    window: &mut Window,
 406    cx: &mut App,
 407) -> Div {
 408    let Some(child) = tree.get(index) else {
 409        return element
 410            .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error));
 411    };
 412
 413    if render_next_title {
 414        element = element.child(Label::new(child.title.clone()).size(LabelSize::Large));
 415    }
 416
 417    // todo(settings_ui): subgroups?
 418    let mut pushed_path = false;
 419    if let Some(child_path) = child.path.as_ref() {
 420        path.push(child_path.clone());
 421        if let Some(fallback_path) = fallback_path.as_mut() {
 422            fallback_path.push(child_path.clone());
 423        }
 424        pushed_path = true;
 425    }
 426    let settings_value = settings_value_from_settings_and_path(
 427        path.clone(),
 428        fallback_path.as_ref().map(|path| path.as_slice()),
 429        child.title.clone(),
 430        child.documentation.clone(),
 431        // PERF: how to structure this better? There feels like there's a way to avoid the clone
 432        // and every value lookup
 433        SettingsStore::global(cx).raw_user_settings(),
 434        SettingsStore::global(cx).raw_default_settings(),
 435    );
 436    if let Some(dynamic_render) = child.dynamic_render.as_ref() {
 437        let value = settings_value.read();
 438        let selected_index = (dynamic_render.determine_option)(value, cx);
 439        element = element.child(div().child(render_toggle_button_group_inner(
 440            settings_value.title.clone(),
 441            dynamic_render.labels,
 442            Some(selected_index),
 443            {
 444                let path = settings_value.path.clone();
 445                let defaults = dynamic_render.defaults.clone();
 446                move |idx, cx| {
 447                    if idx == selected_index {
 448                        return;
 449                    }
 450                    let default = defaults.get(idx).cloned().unwrap_or_default();
 451                    SettingsValue::write_value(&path, default, cx);
 452                }
 453            },
 454        )));
 455        // we don't add descendants for unit options, so we adjust the selected index
 456        // by the number of options we didn't add descendants for, to get the descendant index
 457        let selected_descendant_index = selected_index
 458            - dynamic_render.options[..selected_index]
 459                .iter()
 460                .filter(|option| option.is_none())
 461                .count();
 462        if dynamic_render.options[selected_index].is_some()
 463            && let Some(descendant_index) =
 464                child.nth_descendant_index(tree, selected_descendant_index)
 465        {
 466            element = render_recursive(
 467                tree,
 468                descendant_index,
 469                path,
 470                element,
 471                fallback_path,
 472                false,
 473                window,
 474                cx,
 475            );
 476        }
 477    } else if let Some((settings_ui_item, generate_items, defaults_path)) =
 478        child.generate_items.as_ref()
 479    {
 480        let generated_items = generate_items(settings_value.read(), cx);
 481        let mut ui_items = Vec::with_capacity(generated_items.len());
 482        for item in generated_items {
 483            let settings_ui_entry = SettingsUiEntry {
 484                path: None,
 485                title: "",
 486                documentation: None,
 487                item: settings_ui_item.clone(),
 488            };
 489            let prev_index = if ui_items.is_empty() {
 490                None
 491            } else {
 492                Some(ui_items.len() - 1)
 493            };
 494            let item_index = ui_items.len();
 495            build_tree_item(
 496                &mut ui_items,
 497                settings_ui_entry,
 498                child._depth + 1,
 499                prev_index,
 500            );
 501            if item_index < ui_items.len() {
 502                ui_items[item_index].path = None;
 503                ui_items[item_index].title = item.title.clone();
 504                ui_items[item_index].documentation = item.documentation.clone();
 505
 506                // push path instead of setting path on ui item so that the path isn't pushed to default_path as well
 507                // when we recurse
 508                path.push(item.path.clone());
 509                element = render_recursive(
 510                    &ui_items,
 511                    item_index,
 512                    path,
 513                    element,
 514                    &mut Some(defaults_path.clone()),
 515                    true,
 516                    window,
 517                    cx,
 518                );
 519                path.pop();
 520            }
 521        }
 522    } else if let Some(child_render) = child.render.as_ref() {
 523        element = element.child(div().child(render_item_single(
 524            settings_value,
 525            child_render,
 526            window,
 527            cx,
 528        )));
 529    } else if let Some(child_index) = child.first_descendant_index() {
 530        let mut index = Some(child_index);
 531        while let Some(sub_child_index) = index {
 532            element = render_recursive(
 533                tree,
 534                sub_child_index,
 535                path,
 536                element,
 537                fallback_path,
 538                true,
 539                window,
 540                cx,
 541            );
 542            index = tree[sub_child_index].next_sibling;
 543        }
 544    } else {
 545        element = element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
 546    }
 547
 548    if pushed_path {
 549        path.pop();
 550        if let Some(fallback_path) = fallback_path.as_mut() {
 551            fallback_path.pop();
 552        }
 553    }
 554    return element;
 555}
 556
 557impl Render for SettingsPage {
 558    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 559        let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new());
 560        div()
 561            .grid()
 562            .grid_cols(16)
 563            .p_4()
 564            .bg(cx.theme().colors().editor_background)
 565            .size_full()
 566            .child(
 567                div()
 568                    .id("settings-ui-nav")
 569                    .col_span(2)
 570                    .h_full()
 571                    .child(render_nav(&self.settings_tree, window, cx)),
 572            )
 573            .child(
 574                div().col_span(6).h_full().child(
 575                    render_content(&self.settings_tree, window, cx)
 576                        .id("settings-ui-content")
 577                        .track_scroll(scroll_handle.read(cx))
 578                        .overflow_y_scroll(),
 579                ),
 580            )
 581    }
 582}
 583
 584// todo(settings_ui): remove, only here as inspiration
 585#[allow(dead_code)]
 586fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement {
 587    v_flex()
 588        .p_4()
 589        .size_full()
 590        .gap_4()
 591        .child(Label::new("Settings").size(LabelSize::Large))
 592        .child(
 593            v_flex().gap_1().child(Label::new("Appearance")).child(
 594                v_flex()
 595                    .elevation_2(cx)
 596                    .child(AppearanceSettingsControls::new()),
 597            ),
 598        )
 599        .child(
 600            v_flex().gap_1().child(Label::new("Editor")).child(
 601                v_flex()
 602                    .elevation_2(cx)
 603                    .child(EditorSettingsControls::new()),
 604            ),
 605        )
 606}
 607
 608fn element_id_from_path(path: &[SharedString]) -> ElementId {
 609    if path.len() == 0 {
 610        panic!("Path length must not be zero");
 611    } else if path.len() == 1 {
 612        ElementId::Name(path[0].clone())
 613    } else {
 614        ElementId::from((
 615            ElementId::from(path[path.len() - 2].clone()),
 616            path[path.len() - 1].clone(),
 617        ))
 618    }
 619}
 620
 621fn render_item_single(
 622    settings_value: SettingsValue<serde_json::Value>,
 623    item: &SettingsUiItemSingle,
 624    window: &mut Window,
 625    cx: &mut App,
 626) -> AnyElement {
 627    match item {
 628        SettingsUiItemSingle::Custom(_) => div()
 629            .child(format!("Item: {}", settings_value.path.join(".")))
 630            .into_any_element(),
 631        SettingsUiItemSingle::SwitchField => {
 632            render_any_item(settings_value, render_switch_field, window, cx)
 633        }
 634        SettingsUiItemSingle::NumericStepper(num_type) => {
 635            render_any_numeric_stepper(settings_value, *num_type, window, cx)
 636        }
 637        SettingsUiItemSingle::ToggleGroup {
 638            variants: values,
 639            labels: titles,
 640        } => render_toggle_button_group(settings_value, values, titles, window, cx),
 641        SettingsUiItemSingle::DropDown { variants, labels } => {
 642            render_dropdown(settings_value, variants, labels, window, cx)
 643        }
 644        SettingsUiItemSingle::TextField => render_text_field(settings_value, window, cx),
 645    }
 646}
 647
 648pub fn read_settings_value_from_path<'a>(
 649    settings_contents: &'a serde_json::Value,
 650    path: &[impl AsRef<str>],
 651) -> Option<&'a serde_json::Value> {
 652    // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
 653    let Some((key, remaining)) = path.split_first() else {
 654        return Some(settings_contents);
 655    };
 656    let Some(value) = settings_contents.get(key.as_ref()) else {
 657        return None;
 658    };
 659
 660    read_settings_value_from_path(value, remaining)
 661}
 662
 663fn downcast_any_item<T: serde::de::DeserializeOwned>(
 664    settings_value: SettingsValue<serde_json::Value>,
 665) -> SettingsValue<T> {
 666    let value = settings_value.value.map(|value| {
 667        serde_json::from_value::<T>(value.clone())
 668            .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
 669            .with_context(|| format!("value is not a {}: {}", std::any::type_name::<T>(), value))
 670            .unwrap()
 671    });
 672    // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
 673    let default_value = serde_json::from_value::<T>(settings_value.default_value)
 674        .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
 675        .with_context(|| format!("value is not a {}", std::any::type_name::<T>()))
 676        .unwrap();
 677    let deserialized_setting_value = SettingsValue {
 678        title: settings_value.title,
 679        path: settings_value.path,
 680        documentation: settings_value.documentation,
 681        value,
 682        default_value,
 683    };
 684    deserialized_setting_value
 685}
 686
 687fn render_any_item<T: serde::de::DeserializeOwned>(
 688    settings_value: SettingsValue<serde_json::Value>,
 689    render_fn: impl Fn(SettingsValue<T>, &mut Window, &mut App) -> AnyElement + 'static,
 690    window: &mut Window,
 691    cx: &mut App,
 692) -> AnyElement {
 693    let deserialized_setting_value = downcast_any_item(settings_value);
 694    render_fn(deserialized_setting_value, window, cx)
 695}
 696
 697fn render_any_numeric_stepper(
 698    settings_value: SettingsValue<serde_json::Value>,
 699    num_type: NumType,
 700    window: &mut Window,
 701    cx: &mut App,
 702) -> AnyElement {
 703    match num_type {
 704        NumType::U64 => render_numeric_stepper::<u64>(
 705            downcast_any_item(settings_value),
 706            |n| u64::saturating_sub(n, 1),
 707            |n| u64::saturating_add(n, 1),
 708            |n| {
 709                serde_json::Number::try_from(n)
 710                    .context("Failed to convert u64 to serde_json::Number")
 711            },
 712            window,
 713            cx,
 714        ),
 715        NumType::U32 => render_numeric_stepper::<u32>(
 716            downcast_any_item(settings_value),
 717            |n| u32::saturating_sub(n, 1),
 718            |n| u32::saturating_add(n, 1),
 719            |n| {
 720                serde_json::Number::try_from(n)
 721                    .context("Failed to convert u32 to serde_json::Number")
 722            },
 723            window,
 724            cx,
 725        ),
 726        NumType::F32 => render_numeric_stepper::<f32>(
 727            downcast_any_item(settings_value),
 728            |a| a - 1.0,
 729            |a| a + 1.0,
 730            |n| {
 731                serde_json::Number::from_f64(n as f64)
 732                    .context("Failed to convert f32 to serde_json::Number")
 733            },
 734            window,
 735            cx,
 736        ),
 737        NumType::USIZE => render_numeric_stepper::<usize>(
 738            downcast_any_item(settings_value),
 739            |n| usize::saturating_sub(n, 1),
 740            |n| usize::saturating_add(n, 1),
 741            |n| {
 742                serde_json::Number::try_from(n)
 743                    .context("Failed to convert usize to serde_json::Number")
 744            },
 745            window,
 746            cx,
 747        ),
 748        NumType::U32NONZERO => render_numeric_stepper::<NonZeroU32>(
 749            downcast_any_item(settings_value),
 750            |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN),
 751            |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX),
 752            |n| {
 753                serde_json::Number::try_from(n.get())
 754                    .context("Failed to convert usize to serde_json::Number")
 755            },
 756            window,
 757            cx,
 758        ),
 759    }
 760}
 761
 762fn render_numeric_stepper<T: serde::de::DeserializeOwned + std::fmt::Display + Copy + 'static>(
 763    value: SettingsValue<T>,
 764    saturating_sub_1: fn(T) -> T,
 765    saturating_add_1: fn(T) -> T,
 766    to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
 767    _window: &mut Window,
 768    _cx: &mut App,
 769) -> AnyElement {
 770    let id = element_id_from_path(&value.path);
 771    let path = value.path.clone();
 772    let num = *value.read();
 773
 774    NumericStepper::new(
 775        id,
 776        num.to_string(),
 777        {
 778            let path = value.path;
 779            move |_, _, cx| {
 780                let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else {
 781                    return;
 782                };
 783                let new_value = serde_json::Value::Number(number);
 784                SettingsValue::write_value(&path, new_value, cx);
 785            }
 786        },
 787        move |_, _, cx| {
 788            let Some(number) = to_serde_number(saturating_add_1(num)).ok() else {
 789                return;
 790            };
 791
 792            let new_value = serde_json::Value::Number(number);
 793
 794            SettingsValue::write_value(&path, new_value, cx);
 795        },
 796    )
 797    .style(ui::NumericStepperStyle::Outlined)
 798    .into_any_element()
 799}
 800
 801fn render_switch_field(
 802    value: SettingsValue<bool>,
 803    _window: &mut Window,
 804    _cx: &mut App,
 805) -> AnyElement {
 806    let id = element_id_from_path(&value.path);
 807    let path = value.path.clone();
 808    SwitchField::new(
 809        id,
 810        value.title.clone(),
 811        value.documentation.clone(),
 812        match value.read() {
 813            true => ToggleState::Selected,
 814            false => ToggleState::Unselected,
 815        },
 816        move |toggle_state, _, cx| {
 817            let new_value = serde_json::Value::Bool(match toggle_state {
 818                ToggleState::Indeterminate => {
 819                    return;
 820                }
 821                ToggleState::Selected => true,
 822                ToggleState::Unselected => false,
 823            });
 824
 825            SettingsValue::write_value(&path, new_value, cx);
 826        },
 827    )
 828    .into_any_element()
 829}
 830
 831fn render_text_field(
 832    value: SettingsValue<serde_json::Value>,
 833    window: &mut Window,
 834    cx: &mut App,
 835) -> AnyElement {
 836    let value = downcast_any_item::<String>(value);
 837    let path = value.path.clone();
 838    let editor = window.use_state(cx, {
 839        let path = path.clone();
 840        move |window, cx| {
 841            let mut editor = Editor::single_line(window, cx);
 842
 843            cx.observe_global_in::<SettingsStore>(window, move |editor, window, cx| {
 844                let user_settings = SettingsStore::global(cx).raw_user_settings();
 845                if let Some(value) = read_settings_value_from_path(&user_settings, &path).cloned()
 846                    && let Some(value) = value.as_str()
 847                {
 848                    editor.set_text(value, window, cx);
 849                }
 850            })
 851            .detach();
 852
 853            editor.set_text(value.read().clone(), window, cx);
 854            editor
 855        }
 856    });
 857
 858    let weak_editor = editor.downgrade();
 859    let theme_colors = cx.theme().colors();
 860
 861    div()
 862        .child(editor)
 863        .bg(theme_colors.editor_background)
 864        .border_1()
 865        .rounded_lg()
 866        .border_color(theme_colors.border)
 867        .on_action::<menu::Confirm>({
 868            move |_, _, cx| {
 869                let new_value = weak_editor.read_with(cx, |editor, cx| editor.text(cx)).ok();
 870
 871                if let Some(new_value) = new_value {
 872                    SettingsValue::write_value(&path, serde_json::Value::String(new_value), cx);
 873                }
 874            }
 875        })
 876        .into_any_element()
 877}
 878
 879fn render_toggle_button_group(
 880    value: SettingsValue<serde_json::Value>,
 881    variants: &'static [&'static str],
 882    labels: &'static [&'static str],
 883    _: &mut Window,
 884    _: &mut App,
 885) -> AnyElement {
 886    let value = downcast_any_item::<String>(value);
 887    let active_value = value.read();
 888    let selected_idx = variants.iter().position(|v| v == &active_value);
 889
 890    return render_toggle_button_group_inner(value.title, labels, selected_idx, {
 891        let path = value.path.clone();
 892        move |variant_index, cx| {
 893            SettingsValue::write_value(
 894                &path,
 895                serde_json::Value::String(variants[variant_index].to_string()),
 896                cx,
 897            );
 898        }
 899    });
 900}
 901
 902fn render_dropdown(
 903    value: SettingsValue<serde_json::Value>,
 904    variants: &'static [&'static str],
 905    labels: &'static [&'static str],
 906    window: &mut Window,
 907    cx: &mut App,
 908) -> AnyElement {
 909    let value = downcast_any_item::<String>(value);
 910    let id = element_id_from_path(&value.path);
 911
 912    let menu = window.use_keyed_state(id.clone(), cx, |window, cx| {
 913        let path = value.path.clone();
 914        let handler = Rc::new(move |variant: &'static str, cx: &mut App| {
 915            SettingsValue::write_value(&path, serde_json::Value::String(variant.to_string()), cx);
 916        });
 917
 918        ContextMenu::build(window, cx, |mut menu, _, _| {
 919            for (label, variant) in labels.iter().zip(variants) {
 920                menu = menu.entry(*label, None, {
 921                    let handler = handler.clone();
 922                    move |_, cx| {
 923                        handler(variant, cx);
 924                    }
 925                });
 926            }
 927
 928            menu
 929        })
 930    });
 931
 932    DropdownMenu::new(id, value.read(), menu.read(cx).clone())
 933        .style(ui::DropdownStyle::Outlined)
 934        .into_any_element()
 935}
 936
 937fn render_toggle_button_group_inner(
 938    title: SharedString,
 939    labels: &'static [&'static str],
 940    selected_idx: Option<usize>,
 941    on_write: impl Fn(usize, &mut App) + 'static,
 942) -> AnyElement {
 943    fn make_toggle_group<const LEN: usize>(
 944        title: SharedString,
 945        selected_idx: Option<usize>,
 946        on_write: Rc<dyn Fn(usize, &mut App)>,
 947        labels: &'static [&'static str],
 948    ) -> AnyElement {
 949        let labels_array: [&'static str; LEN] = {
 950            let mut arr = ["unused"; LEN];
 951            arr.copy_from_slice(labels);
 952            arr
 953        };
 954
 955        let mut idx = 0;
 956        ToggleButtonGroup::single_row(
 957            title,
 958            labels_array.map(|label| {
 959                idx += 1;
 960                let on_write = on_write.clone();
 961                ToggleButtonSimple::new(label, move |_, _, cx| {
 962                    on_write(idx - 1, cx);
 963                })
 964            }),
 965        )
 966        .when_some(selected_idx, |this, ix| this.selected_index(ix))
 967        .style(ui::ToggleButtonGroupStyle::Filled)
 968        .into_any_element()
 969    }
 970
 971    let on_write = Rc::new(on_write);
 972
 973    macro_rules! templ_toggl_with_const_param {
 974        ($len:expr) => {
 975            if labels.len() == $len {
 976                return make_toggle_group::<$len>(title.clone(), selected_idx, on_write, labels);
 977            }
 978        };
 979    }
 980    templ_toggl_with_const_param!(1);
 981    templ_toggl_with_const_param!(2);
 982    templ_toggl_with_const_param!(3);
 983    templ_toggl_with_const_param!(4);
 984    templ_toggl_with_const_param!(5);
 985    templ_toggl_with_const_param!(6);
 986    unreachable!("Too many variants");
 987}
 988
 989fn settings_value_from_settings_and_path(
 990    path: SmallVec<[SharedString; 1]>,
 991    fallback_path: Option<&[SharedString]>,
 992    title: SharedString,
 993    documentation: Option<SharedString>,
 994    user_settings: &serde_json::Value,
 995    default_settings: &serde_json::Value,
 996) -> SettingsValue<serde_json::Value> {
 997    let default_value = read_settings_value_from_path(default_settings, &path)
 998        .or_else(|| {
 999            fallback_path.and_then(|fallback_path| {
1000                read_settings_value_from_path(default_settings, fallback_path)
1001            })
1002        })
1003        .with_context(|| format!("No default value for item at path {:?}", path.join(".")))
1004        .expect("Default value set for item")
1005        .clone();
1006
1007    let value = read_settings_value_from_path(user_settings, &path).cloned();
1008    let settings_value = SettingsValue {
1009        default_value,
1010        value,
1011        documentation,
1012        path,
1013        // todo(settings_ui) is title required inside SettingsValue?
1014        title,
1015    };
1016    return settings_value;
1017}