settings_ui.rs

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