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