settings_ui.rs

  1mod appearance_settings_controls;
  2
  3use std::any::TypeId;
  4use std::ops::{Not, Range};
  5
  6use anyhow::Context as _;
  7use command_palette_hooks::CommandPaletteFilter;
  8use editor::EditorSettingsControls;
  9use feature_flags::{FeatureFlag, FeatureFlagViewExt};
 10use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
 11use settings::{
 12    NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic,
 13    SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue,
 14};
 15use smallvec::SmallVec;
 16use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*};
 17use workspace::{
 18    Workspace,
 19    item::{Item, ItemEvent},
 20    with_active_or_new_workspace,
 21};
 22
 23use crate::appearance_settings_controls::AppearanceSettingsControls;
 24
 25pub struct SettingsUiFeatureFlag;
 26
 27impl FeatureFlag for SettingsUiFeatureFlag {
 28    const NAME: &'static str = "settings-ui";
 29}
 30
 31actions!(
 32    zed,
 33    [
 34        /// Opens the settings editor.
 35        OpenSettingsEditor
 36    ]
 37);
 38
 39pub fn init(cx: &mut App) {
 40    cx.on_action(|_: &OpenSettingsEditor, cx| {
 41        with_active_or_new_workspace(cx, move |workspace, window, cx| {
 42            let existing = workspace
 43                .active_pane()
 44                .read(cx)
 45                .items()
 46                .find_map(|item| item.downcast::<SettingsPage>());
 47
 48            if let Some(existing) = existing {
 49                workspace.activate_item(&existing, true, true, window, cx);
 50            } else {
 51                let settings_page = SettingsPage::new(workspace, cx);
 52                workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
 53            }
 54        });
 55    });
 56
 57    cx.observe_new(|_workspace: &mut Workspace, window, cx| {
 58        let Some(window) = window else {
 59            return;
 60        };
 61
 62        let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
 63
 64        CommandPaletteFilter::update_global(cx, |filter, _cx| {
 65            filter.hide_action_types(&settings_ui_actions);
 66        });
 67
 68        cx.observe_flag::<SettingsUiFeatureFlag, _>(
 69            window,
 70            move |is_enabled, _workspace, _, cx| {
 71                if is_enabled {
 72                    CommandPaletteFilter::update_global(cx, |filter, _cx| {
 73                        filter.show_action_types(&settings_ui_actions);
 74                    });
 75                } else {
 76                    CommandPaletteFilter::update_global(cx, |filter, _cx| {
 77                        filter.hide_action_types(&settings_ui_actions);
 78                    });
 79                }
 80            },
 81        )
 82        .detach();
 83    })
 84    .detach();
 85}
 86
 87pub struct SettingsPage {
 88    focus_handle: FocusHandle,
 89    settings_tree: SettingsUiTree,
 90}
 91
 92impl SettingsPage {
 93    pub fn new(_workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
 94        cx.new(|cx| Self {
 95            focus_handle: cx.focus_handle(),
 96            settings_tree: SettingsUiTree::new(cx),
 97        })
 98    }
 99}
100
101impl EventEmitter<ItemEvent> for SettingsPage {}
102
103impl Focusable for SettingsPage {
104    fn focus_handle(&self, _cx: &App) -> FocusHandle {
105        self.focus_handle.clone()
106    }
107}
108
109impl Item for SettingsPage {
110    type Event = ItemEvent;
111
112    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
113        Some(Icon::new(IconName::Settings))
114    }
115
116    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
117        "Settings".into()
118    }
119
120    fn show_toolbar(&self) -> bool {
121        false
122    }
123
124    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
125        f(*event)
126    }
127}
128
129// We want to iterate over the side bar with root groups
130// - this is a loop over top level groups, and if any are expanded, recursively displaying their items
131// - Should be able to get all items from a group (flatten a group)
132// - Should be able to toggle/untoggle groups in UI (at least in sidebar)
133// - Search should be available
134//  - there should be an index of text -> item mappings, for using fuzzy::match
135//   - Do we want to show the parent groups when a item is matched?
136
137struct UiEntry {
138    title: &'static str,
139    path: Option<&'static str>,
140    documentation: Option<&'static str>,
141    _depth: usize,
142    // a
143    //  b     < a descendant range < a total descendant range
144    //    f   |                    |
145    //    g   |                    |
146    //  c     <                    |
147    //    d                        |
148    //    e                        <
149    descendant_range: Range<usize>,
150    total_descendant_range: Range<usize>,
151    next_sibling: Option<usize>,
152    // expanded: bool,
153    render: Option<SettingsUiItemSingle>,
154    /// For dynamic items this is a way to select a value from a list of values
155    /// this is always none for non-dynamic items
156    select_descendant: Option<fn(&serde_json::Value, &App) -> usize>,
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 index = tree.len();
195    tree.push(UiEntry {
196        title: entry.title,
197        path: entry.path,
198        documentation: entry.documentation,
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        select_descendant: None,
205    });
206    if let Some(prev_index) = prev_index {
207        tree[prev_index].next_sibling = Some(index);
208    }
209    match entry.item {
210        SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => {
211            for group_item in group_items {
212                let prev_index = tree[index]
213                    .descendant_range
214                    .is_empty()
215                    .not()
216                    .then_some(tree[index].descendant_range.end - 1);
217                tree[index].descendant_range.end = tree.len() + 1;
218                build_tree_item(tree, group_item, depth + 1, prev_index);
219                tree[index].total_descendant_range.end = tree.len();
220            }
221        }
222        SettingsUiItem::Single(item) => {
223            tree[index].render = Some(item);
224        }
225        SettingsUiItem::Dynamic(SettingsUiItemDynamic {
226            options,
227            determine_option,
228        }) => {
229            tree[index].select_descendant = Some(determine_option);
230            for option in options {
231                let prev_index = tree[index]
232                    .descendant_range
233                    .is_empty()
234                    .not()
235                    .then_some(tree[index].descendant_range.end - 1);
236                tree[index].descendant_range.end = tree.len() + 1;
237                build_tree_item(tree, option, depth + 1, prev_index);
238                tree[index].total_descendant_range.end = tree.len();
239            }
240        }
241        SettingsUiItem::None => {
242            return;
243        }
244    }
245}
246
247impl SettingsUiTree {
248    pub fn new(cx: &App) -> Self {
249        let settings_store = SettingsStore::global(cx);
250        let mut tree = vec![];
251        let mut root_entry_indices = vec![];
252        for item in settings_store.settings_ui_items() {
253            if matches!(item.item, SettingsUiItem::None)
254            // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to
255            // link them to other groups
256            || matches!(item.item, SettingsUiItem::Single(_))
257            {
258                continue;
259            }
260
261            let prev_root_entry_index = root_entry_indices.last().copied();
262            root_entry_indices.push(tree.len());
263            build_tree_item(&mut tree, item, 0, prev_root_entry_index);
264        }
265
266        root_entry_indices.sort_by_key(|i| tree[*i].title);
267
268        let active_entry_index = root_entry_indices[0];
269        Self {
270            entries: tree,
271            root_entry_indices,
272            active_entry_index,
273        }
274    }
275
276    // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree,
277    // so that we can keep none/skip and still test in CI that all settings have
278    #[cfg(feature = "test-support")]
279    pub fn all_paths(&self, cx: &App) -> Vec<Vec<&'static str>> {
280        fn all_paths_rec(
281            tree: &[UiEntry],
282            paths: &mut Vec<Vec<&'static str>>,
283            current_path: &mut Vec<&'static str>,
284            idx: usize,
285            cx: &App,
286        ) {
287            let child = &tree[idx];
288            let mut pushed_path = false;
289            if let Some(path) = child.path.as_ref() {
290                current_path.push(path);
291                paths.push(current_path.clone());
292                pushed_path = true;
293            }
294            // todo(settings_ui): handle dynamic nodes here
295            let selected_descendant_index = child
296                .select_descendant
297                .map(|select_descendant| {
298                    read_settings_value_from_path(
299                        SettingsStore::global(cx).raw_default_settings(),
300                        &current_path,
301                    )
302                    .map(|value| select_descendant(value, cx))
303                })
304                .and_then(|selected_descendant_index| {
305                    selected_descendant_index.map(|index| child.nth_descendant_index(tree, index))
306                });
307
308            if let Some(selected_descendant_index) = selected_descendant_index {
309                // just silently fail if we didn't find a setting value for the path
310                if let Some(descendant_index) = selected_descendant_index {
311                    all_paths_rec(tree, paths, current_path, descendant_index, cx);
312                }
313            } else if let Some(desc_idx) = child.first_descendant_index() {
314                let mut desc_idx = Some(desc_idx);
315                while let Some(descendant_index) = desc_idx {
316                    all_paths_rec(&tree, paths, current_path, descendant_index, cx);
317                    desc_idx = tree[descendant_index].next_sibling;
318                }
319            }
320            if pushed_path {
321                current_path.pop();
322            }
323        }
324
325        let mut paths = Vec::new();
326        for &index in &self.root_entry_indices {
327            all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx);
328        }
329        paths
330    }
331}
332
333fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<SettingsPage>) -> Div {
334    let mut nav = v_flex().p_4().gap_2();
335    for &index in &tree.root_entry_indices {
336        nav = nav.child(
337            div()
338                .id(index)
339                .on_click(cx.listener(move |settings, _, _, _| {
340                    settings.settings_tree.active_entry_index = index;
341                }))
342                .child(
343                    Label::new(SharedString::new_static(tree.entries[index].title))
344                        .size(LabelSize::Large)
345                        .when(tree.active_entry_index == index, |this| {
346                            this.color(Color::Selected)
347                        }),
348                ),
349        );
350    }
351    nav
352}
353
354fn render_content(
355    tree: &SettingsUiTree,
356    window: &mut Window,
357    cx: &mut Context<SettingsPage>,
358) -> Div {
359    let content = v_flex().size_full().gap_4();
360
361    let mut path = smallvec::smallvec![];
362
363    fn render_recursive(
364        tree: &SettingsUiTree,
365        index: usize,
366        path: &mut SmallVec<[&'static str; 1]>,
367        mut element: Div,
368        window: &mut Window,
369        cx: &mut App,
370    ) -> Div {
371        let Some(child) = tree.entries.get(index) else {
372            return element.child(
373                Label::new(SharedString::new_static("No settings found")).color(Color::Error),
374            );
375        };
376
377        element =
378            element.child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large));
379
380        // todo(settings_ui): subgroups?
381        let mut pushed_path = false;
382        if let Some(child_path) = child.path {
383            path.push(child_path);
384            pushed_path = true;
385        }
386        let settings_value = settings_value_from_settings_and_path(
387            path.clone(),
388            child.title,
389            child.documentation,
390            // PERF: how to structure this better? There feels like there's a way to avoid the clone
391            // and every value lookup
392            SettingsStore::global(cx).raw_user_settings(),
393            SettingsStore::global(cx).raw_default_settings(),
394        );
395        if let Some(select_descendant) = child.select_descendant {
396            let selected_descendant = child
397                .nth_descendant_index(&tree.entries, select_descendant(settings_value.read(), cx));
398            if let Some(descendant_index) = selected_descendant {
399                element = render_recursive(&tree, descendant_index, path, element, window, cx);
400            }
401        }
402        if let Some(child_render) = child.render.as_ref() {
403            element = element.child(div().child(render_item_single(
404                settings_value,
405                child_render,
406                window,
407                cx,
408            )));
409        } else if let Some(child_index) = child.first_descendant_index() {
410            let mut index = Some(child_index);
411            while let Some(sub_child_index) = index {
412                element = render_recursive(tree, sub_child_index, path, element, window, cx);
413                index = tree.entries[sub_child_index].next_sibling;
414            }
415        } else {
416            element =
417                element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
418        }
419
420        if pushed_path {
421            path.pop();
422        }
423        return element;
424    }
425
426    return render_recursive(
427        tree,
428        tree.active_entry_index,
429        &mut path,
430        content,
431        window,
432        cx,
433    );
434}
435
436impl Render for SettingsPage {
437    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
438        let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new());
439        div()
440            .grid()
441            .grid_cols(16)
442            .p_4()
443            .bg(cx.theme().colors().editor_background)
444            .size_full()
445            .child(
446                div()
447                    .id("settings-ui-nav")
448                    .col_span(2)
449                    .h_full()
450                    .child(render_nav(&self.settings_tree, window, cx)),
451            )
452            .child(
453                div().col_span(6).h_full().child(
454                    render_content(&self.settings_tree, window, cx)
455                        .id("settings-ui-content")
456                        .track_scroll(scroll_handle.read(cx))
457                        .overflow_y_scroll(),
458                ),
459            )
460    }
461}
462
463// todo(settings_ui): remove, only here as inspiration
464#[allow(dead_code)]
465fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement {
466    v_flex()
467        .p_4()
468        .size_full()
469        .gap_4()
470        .child(Label::new("Settings").size(LabelSize::Large))
471        .child(
472            v_flex().gap_1().child(Label::new("Appearance")).child(
473                v_flex()
474                    .elevation_2(cx)
475                    .child(AppearanceSettingsControls::new()),
476            ),
477        )
478        .child(
479            v_flex().gap_1().child(Label::new("Editor")).child(
480                v_flex()
481                    .elevation_2(cx)
482                    .child(EditorSettingsControls::new()),
483            ),
484        )
485}
486
487fn element_id_from_path(path: &[&'static str]) -> ElementId {
488    if path.len() == 0 {
489        panic!("Path length must not be zero");
490    } else if path.len() == 1 {
491        ElementId::Name(SharedString::new_static(path[0]))
492    } else {
493        ElementId::from((
494            ElementId::from(SharedString::new_static(path[path.len() - 2])),
495            SharedString::new_static(path[path.len() - 1]),
496        ))
497    }
498}
499
500fn render_item_single(
501    settings_value: SettingsValue<serde_json::Value>,
502    item: &SettingsUiItemSingle,
503    window: &mut Window,
504    cx: &mut App,
505) -> AnyElement {
506    match item {
507        SettingsUiItemSingle::Custom(_) => div()
508            .child(format!("Item: {}", settings_value.path.join(".")))
509            .into_any_element(),
510        SettingsUiItemSingle::SwitchField => {
511            render_any_item(settings_value, render_switch_field, window, cx)
512        }
513        SettingsUiItemSingle::NumericStepper(num_type) => {
514            render_any_numeric_stepper(settings_value, *num_type, window, cx)
515        }
516        SettingsUiItemSingle::ToggleGroup {
517            variants: values,
518            labels: titles,
519        } => render_toggle_button_group(settings_value, values, titles, window, cx),
520        SettingsUiItemSingle::DropDown { .. } => {
521            unimplemented!("This")
522        }
523    }
524}
525
526pub fn read_settings_value_from_path<'a>(
527    settings_contents: &'a serde_json::Value,
528    path: &[&str],
529) -> Option<&'a serde_json::Value> {
530    // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
531    let Some((key, remaining)) = path.split_first() else {
532        return Some(settings_contents);
533    };
534    let Some(value) = settings_contents.get(key) else {
535        return None;
536    };
537
538    read_settings_value_from_path(value, remaining)
539}
540
541fn downcast_any_item<T: serde::de::DeserializeOwned>(
542    settings_value: SettingsValue<serde_json::Value>,
543) -> SettingsValue<T> {
544    let value = settings_value
545        .value
546        .map(|value| serde_json::from_value::<T>(value).expect("value is not a T"));
547    // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
548    let default_value = serde_json::from_value::<T>(settings_value.default_value)
549        .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
550        .expect("default value is not an Option<T>");
551    let deserialized_setting_value = SettingsValue {
552        title: settings_value.title,
553        path: settings_value.path,
554        documentation: settings_value.documentation,
555        value,
556        default_value,
557    };
558    deserialized_setting_value
559}
560
561fn render_any_item<T: serde::de::DeserializeOwned>(
562    settings_value: SettingsValue<serde_json::Value>,
563    render_fn: impl Fn(SettingsValue<T>, &mut Window, &mut App) -> AnyElement + 'static,
564    window: &mut Window,
565    cx: &mut App,
566) -> AnyElement {
567    let deserialized_setting_value = downcast_any_item(settings_value);
568    render_fn(deserialized_setting_value, window, cx)
569}
570
571fn render_any_numeric_stepper(
572    settings_value: SettingsValue<serde_json::Value>,
573    num_type: NumType,
574    window: &mut Window,
575    cx: &mut App,
576) -> AnyElement {
577    match num_type {
578        NumType::U64 => render_numeric_stepper::<u64>(
579            downcast_any_item(settings_value),
580            u64::saturating_sub,
581            u64::saturating_add,
582            |n| {
583                serde_json::Number::try_from(n)
584                    .context("Failed to convert u64 to serde_json::Number")
585            },
586            window,
587            cx,
588        ),
589        NumType::U32 => render_numeric_stepper::<u32>(
590            downcast_any_item(settings_value),
591            u32::saturating_sub,
592            u32::saturating_add,
593            |n| {
594                serde_json::Number::try_from(n)
595                    .context("Failed to convert u32 to serde_json::Number")
596            },
597            window,
598            cx,
599        ),
600        NumType::F32 => render_numeric_stepper::<f32>(
601            downcast_any_item(settings_value),
602            |a, b| a - b,
603            |a, b| a + b,
604            |n| {
605                serde_json::Number::from_f64(n as f64)
606                    .context("Failed to convert f32 to serde_json::Number")
607            },
608            window,
609            cx,
610        ),
611        NumType::USIZE => render_numeric_stepper::<usize>(
612            downcast_any_item(settings_value),
613            usize::saturating_sub,
614            usize::saturating_add,
615            |n| {
616                serde_json::Number::try_from(n)
617                    .context("Failed to convert usize to serde_json::Number")
618            },
619            window,
620            cx,
621        ),
622    }
623}
624
625fn render_numeric_stepper<
626    T: serde::de::DeserializeOwned + std::fmt::Display + Copy + From<u8> + 'static,
627>(
628    value: SettingsValue<T>,
629    saturating_sub: fn(T, T) -> T,
630    saturating_add: fn(T, T) -> T,
631    to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
632    _window: &mut Window,
633    _cx: &mut App,
634) -> AnyElement {
635    let id = element_id_from_path(&value.path);
636    let path = value.path.clone();
637    let num = *value.read();
638
639    NumericStepper::new(
640        id,
641        num.to_string(),
642        {
643            let path = value.path.clone();
644            move |_, _, cx| {
645                let Some(number) = to_serde_number(saturating_sub(num, 1.into())).ok() else {
646                    return;
647                };
648                let new_value = serde_json::Value::Number(number);
649                SettingsValue::write_value(&path, new_value, cx);
650            }
651        },
652        move |_, _, cx| {
653            let Some(number) = to_serde_number(saturating_add(num, 1.into())).ok() else {
654                return;
655            };
656
657            let new_value = serde_json::Value::Number(number);
658
659            SettingsValue::write_value(&path, new_value, cx);
660        },
661    )
662    .style(ui::NumericStepperStyle::Outlined)
663    .into_any_element()
664}
665
666fn render_switch_field(
667    value: SettingsValue<bool>,
668    _window: &mut Window,
669    _cx: &mut App,
670) -> AnyElement {
671    let id = element_id_from_path(&value.path);
672    let path = value.path.clone();
673    SwitchField::new(
674        id,
675        SharedString::new_static(value.title),
676        value.documentation.map(SharedString::new_static),
677        match value.read() {
678            true => ToggleState::Selected,
679            false => ToggleState::Unselected,
680        },
681        move |toggle_state, _, cx| {
682            let new_value = serde_json::Value::Bool(match toggle_state {
683                ToggleState::Indeterminate => {
684                    return;
685                }
686                ToggleState::Selected => true,
687                ToggleState::Unselected => false,
688            });
689
690            SettingsValue::write_value(&path, new_value, cx);
691        },
692    )
693    .into_any_element()
694}
695
696fn render_toggle_button_group(
697    value: SettingsValue<serde_json::Value>,
698    variants: &'static [&'static str],
699    labels: &'static [&'static str],
700    _: &mut Window,
701    _: &mut App,
702) -> AnyElement {
703    let value = downcast_any_item::<String>(value);
704
705    fn make_toggle_group<const LEN: usize>(
706        group_name: &'static str,
707        value: SettingsValue<String>,
708        variants: &'static [&'static str],
709        labels: &'static [&'static str],
710    ) -> AnyElement {
711        let mut variants_array: [(&'static str, &'static str); LEN] = [("unused", "unused"); LEN];
712        for i in 0..LEN {
713            variants_array[i] = (variants[i], labels[i]);
714        }
715        let active_value = value.read();
716
717        let selected_idx = variants_array
718            .iter()
719            .enumerate()
720            .find_map(|(idx, (variant, _))| {
721                if variant == &active_value {
722                    Some(idx)
723                } else {
724                    None
725                }
726            });
727
728        let mut idx = 0;
729        ToggleButtonGroup::single_row(
730            group_name,
731            variants_array.map(|(variant, label)| {
732                let path = value.path.clone();
733                idx += 1;
734                ToggleButtonSimple::new(label, move |_, _, cx| {
735                    SettingsValue::write_value(
736                        &path,
737                        serde_json::Value::String(variant.to_string()),
738                        cx,
739                    );
740                })
741            }),
742        )
743        .when_some(selected_idx, |this, ix| this.selected_index(ix))
744        .style(ui::ToggleButtonGroupStyle::Filled)
745        .into_any_element()
746    }
747
748    macro_rules! templ_toggl_with_const_param {
749        ($len:expr) => {
750            if variants.len() == $len {
751                return make_toggle_group::<$len>(value.title, value, variants, labels);
752            }
753        };
754    }
755    templ_toggl_with_const_param!(1);
756    templ_toggl_with_const_param!(2);
757    templ_toggl_with_const_param!(3);
758    templ_toggl_with_const_param!(4);
759    templ_toggl_with_const_param!(5);
760    templ_toggl_with_const_param!(6);
761    unreachable!("Too many variants");
762}
763
764fn settings_value_from_settings_and_path(
765    path: SmallVec<[&'static str; 1]>,
766    title: &'static str,
767    documentation: Option<&'static str>,
768    user_settings: &serde_json::Value,
769    default_settings: &serde_json::Value,
770) -> SettingsValue<serde_json::Value> {
771    let default_value = read_settings_value_from_path(default_settings, &path)
772        .with_context(|| format!("No default value for item at path {:?}", path.join(".")))
773        .expect("Default value set for item")
774        .clone();
775
776    let value = read_settings_value_from_path(user_settings, &path).cloned();
777    let settings_value = SettingsValue {
778        default_value,
779        value,
780        documentation,
781        path: path.clone(),
782        // todo(settings_ui) is title required inside SettingsValue?
783        title,
784    };
785    return settings_value;
786}