settings_ui.rs

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