mod appearance_settings_controls; use std::{ num::NonZeroU32, ops::{Not, Range}, rc::Rc, }; use anyhow::Context as _; use editor::{Editor, EditorSettingsControls}; use feature_flags::{FeatureFlag, FeatureFlagAppExt}; use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions}; use settings::{ NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem, SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle, SettingsUiItemUnion, SettingsValue, }; use smallvec::SmallVec; use ui::{ ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*, }; use workspace::{ Workspace, item::{Item, ItemEvent}, }; use crate::appearance_settings_controls::AppearanceSettingsControls; pub struct SettingsUiFeatureFlag; impl FeatureFlag for SettingsUiFeatureFlag { const NAME: &'static str = "settings-ui"; } actions!( zed, [ /// Opens settings UI. OpenSettingsUi ] ); pub fn open_settings_editor( workspace: &mut Workspace, _: &OpenSettingsUi, window: &mut Window, cx: &mut Context, ) { // todo(settings_ui) open in a local workspace if this is remote. let existing = workspace .active_pane() .read(cx) .items() .find_map(|item| item.downcast::()); if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); } else { let settings_page = SettingsPage::new(workspace, cx); workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx) } } pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { workspace.register_action_renderer(|div, _, _, cx| { let settings_ui_actions = [std::any::TypeId::of::()]; let has_flag = cx.has_flag::(); command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| { if has_flag { filter.show_action_types(&settings_ui_actions); } else { filter.hide_action_types(&settings_ui_actions); } }); if has_flag { div.on_action(cx.listener(open_settings_editor)) } else { div } }); }) .detach(); } pub struct SettingsPage { focus_handle: FocusHandle, settings_tree: SettingsUiTree, } impl SettingsPage { pub fn new(_workspace: &Workspace, cx: &mut Context) -> Entity { cx.new(|cx| Self { focus_handle: cx.focus_handle(), settings_tree: SettingsUiTree::new(cx), }) } } impl EventEmitter for SettingsPage {} impl Focusable for SettingsPage { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Item for SettingsPage { type Event = ItemEvent; fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { Some(Icon::new(IconName::Settings)) } fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { "Settings".into() } fn show_toolbar(&self) -> bool { false } fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { f(*event) } } // We want to iterate over the side bar with root groups // - this is a loop over top level groups, and if any are expanded, recursively displaying their items // - Should be able to get all items from a group (flatten a group) // - Should be able to toggle/untoggle groups in UI (at least in sidebar) // - Search should be available // - there should be an index of text -> item mappings, for using fuzzy::match // - Do we want to show the parent groups when a item is matched? struct UiEntry { title: SharedString, path: Option, documentation: Option, _depth: usize, // a // b < a descendant range < a total descendant range // f | | // g | | // c < | // d | // e < descendant_range: Range, total_descendant_range: Range, next_sibling: Option, // expanded: bool, render: Option, dynamic_render: Option, generate_items: Option<( SettingsUiItem, fn(&serde_json::Value, &App) -> Vec, SmallVec<[SharedString; 1]>, )>, } impl UiEntry { fn first_descendant_index(&self) -> Option { return self .descendant_range .is_empty() .not() .then_some(self.descendant_range.start); } fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option { let first_descendant_index = self.first_descendant_index()?; let mut current_index = 0; let mut current_descendant_index = Some(first_descendant_index); while let Some(descendant_index) = current_descendant_index && current_index < n { current_index += 1; current_descendant_index = tree[descendant_index].next_sibling; } current_descendant_index } } pub struct SettingsUiTree { root_entry_indices: Vec, entries: Vec, active_entry_index: usize, } fn build_tree_item( tree: &mut Vec, entry: SettingsUiEntry, depth: usize, prev_index: Option, ) { // let tree: HashMap; let index = tree.len(); tree.push(UiEntry { title: entry.title.into(), path: entry.path.map(SharedString::new_static), documentation: entry.documentation.map(SharedString::new_static), _depth: depth, descendant_range: index + 1..index + 1, total_descendant_range: index + 1..index + 1, render: None, next_sibling: None, dynamic_render: None, generate_items: None, }); if let Some(prev_index) = prev_index { tree[prev_index].next_sibling = Some(index); } match entry.item { SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => { for group_item in group_items { let prev_index = tree[index] .descendant_range .is_empty() .not() .then_some(tree[index].descendant_range.end - 1); tree[index].descendant_range.end = tree.len() + 1; build_tree_item(tree, group_item, depth + 1, prev_index); tree[index].total_descendant_range.end = tree.len(); } } SettingsUiItem::Single(item) => { tree[index].render = Some(item); } SettingsUiItem::Union(dynamic_render) => { // todo(settings_ui) take from item and store other fields instead of clone // will also require replacing usage in render_recursive so it can know // which options were actually rendered let options = dynamic_render.options.clone(); tree[index].dynamic_render = Some(dynamic_render); for option in options { let Some(option) = option else { continue }; let prev_index = tree[index] .descendant_range .is_empty() .not() .then_some(tree[index].descendant_range.end - 1); tree[index].descendant_range.end = tree.len() + 1; build_tree_item(tree, option, depth + 1, prev_index); tree[index].total_descendant_range.end = tree.len(); } } SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap { item: generate_settings_ui_item, determine_items, defaults_path, }) => { tree[index].generate_items = Some(( generate_settings_ui_item(), determine_items, defaults_path .into_iter() .copied() .map(SharedString::new_static) .collect(), )); } SettingsUiItem::None => { return; } } } impl SettingsUiTree { pub fn new(cx: &App) -> Self { let settings_store = SettingsStore::global(cx); let mut tree = vec![]; let mut root_entry_indices = vec![]; for item in settings_store.settings_ui_items() { if matches!(item.item, SettingsUiItem::None) // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to // link them to other groups || matches!(item.item, SettingsUiItem::Single(_)) { continue; } let prev_root_entry_index = root_entry_indices.last().copied(); root_entry_indices.push(tree.len()); build_tree_item(&mut tree, item, 0, prev_root_entry_index); } root_entry_indices.sort_by_key(|i| &tree[*i].title); let active_entry_index = root_entry_indices[0]; Self { entries: tree, root_entry_indices, active_entry_index, } } // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree, // so that we can keep none/skip and still test in CI that all settings have #[cfg(feature = "test-support")] pub fn all_paths(&self, cx: &App) -> Vec> { fn all_paths_rec( tree: &[UiEntry], paths: &mut Vec>, current_path: &mut Vec, idx: usize, cx: &App, ) { let child = &tree[idx]; let mut pushed_path = false; if let Some(path) = child.path.as_ref() { current_path.push(path.clone()); paths.push(current_path.clone()); pushed_path = true; } // todo(settings_ui): handle dynamic nodes here let selected_descendant_index = child .dynamic_render .as_ref() .map(|dynamic_render| { read_settings_value_from_path( SettingsStore::global(cx).raw_default_settings(), ¤t_path, ) .map(|value| (dynamic_render.determine_option)(value, cx)) }) .and_then(|selected_descendant_index| { selected_descendant_index.map(|index| child.nth_descendant_index(tree, index)) }); if let Some(selected_descendant_index) = selected_descendant_index { // just silently fail if we didn't find a setting value for the path if let Some(descendant_index) = selected_descendant_index { all_paths_rec(tree, paths, current_path, descendant_index, cx); } } else if let Some(desc_idx) = child.first_descendant_index() { let mut desc_idx = Some(desc_idx); while let Some(descendant_index) = desc_idx { all_paths_rec(&tree, paths, current_path, descendant_index, cx); desc_idx = tree[descendant_index].next_sibling; } } if pushed_path { current_path.pop(); } } let mut paths = Vec::new(); for &index in &self.root_entry_indices { all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx); } paths } } fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context) -> Div { let mut nav = v_flex().p_4().gap_2(); for &index in &tree.root_entry_indices { nav = nav.child( div() .id(index) .on_click(cx.listener(move |settings, _, _, _| { settings.settings_tree.active_entry_index = index; })) .child( Label::new(tree.entries[index].title.clone()) .size(LabelSize::Large) .when(tree.active_entry_index == index, |this| { this.color(Color::Selected) }), ), ); } nav } fn render_content( tree: &SettingsUiTree, window: &mut Window, cx: &mut Context, ) -> Div { let content = v_flex().size_full().gap_4(); let mut path = smallvec::smallvec![]; return render_recursive( &tree.entries, tree.active_entry_index, &mut path, content, &mut None, true, window, cx, ); } fn render_recursive( tree: &[UiEntry], index: usize, path: &mut SmallVec<[SharedString; 1]>, mut element: Div, fallback_path: &mut Option>, render_next_title: bool, window: &mut Window, cx: &mut App, ) -> Div { let Some(child) = tree.get(index) else { return element .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error)); }; if render_next_title { element = element.child(Label::new(child.title.clone()).size(LabelSize::Large)); } // todo(settings_ui): subgroups? let mut pushed_path = false; if let Some(child_path) = child.path.as_ref() { path.push(child_path.clone()); if let Some(fallback_path) = fallback_path.as_mut() { fallback_path.push(child_path.clone()); } pushed_path = true; } let settings_value = settings_value_from_settings_and_path( path.clone(), fallback_path.as_ref().map(|path| path.as_slice()), child.title.clone(), child.documentation.clone(), // PERF: how to structure this better? There feels like there's a way to avoid the clone // and every value lookup SettingsStore::global(cx).raw_user_settings(), SettingsStore::global(cx).raw_default_settings(), ); if let Some(dynamic_render) = child.dynamic_render.as_ref() { let value = settings_value.read(); let selected_index = (dynamic_render.determine_option)(value, cx); element = element.child(div().child(render_toggle_button_group_inner( settings_value.title.clone(), dynamic_render.labels, Some(selected_index), { let path = settings_value.path.clone(); let defaults = dynamic_render.defaults.clone(); move |idx, cx| { if idx == selected_index { return; } let default = defaults.get(idx).cloned().unwrap_or_default(); SettingsValue::write_value(&path, default, cx); } }, ))); // we don't add descendants for unit options, so we adjust the selected index // by the number of options we didn't add descendants for, to get the descendant index let selected_descendant_index = selected_index - dynamic_render.options[..selected_index] .iter() .filter(|option| option.is_none()) .count(); if dynamic_render.options[selected_index].is_some() && let Some(descendant_index) = child.nth_descendant_index(tree, selected_descendant_index) { element = render_recursive( tree, descendant_index, path, element, fallback_path, false, window, cx, ); } } else if let Some((settings_ui_item, generate_items, defaults_path)) = child.generate_items.as_ref() { let generated_items = generate_items(settings_value.read(), cx); let mut ui_items = Vec::with_capacity(generated_items.len()); for item in generated_items { let settings_ui_entry = SettingsUiEntry { path: None, title: "", documentation: None, item: settings_ui_item.clone(), }; let prev_index = if ui_items.is_empty() { None } else { Some(ui_items.len() - 1) }; let item_index = ui_items.len(); build_tree_item( &mut ui_items, settings_ui_entry, child._depth + 1, prev_index, ); if item_index < ui_items.len() { ui_items[item_index].path = None; ui_items[item_index].title = item.title.clone(); ui_items[item_index].documentation = item.documentation.clone(); // push path instead of setting path on ui item so that the path isn't pushed to default_path as well // when we recurse path.push(item.path.clone()); element = render_recursive( &ui_items, item_index, path, element, &mut Some(defaults_path.clone()), true, window, cx, ); path.pop(); } } } else if let Some(child_render) = child.render.as_ref() { element = element.child(div().child(render_item_single( settings_value, child_render, window, cx, ))); } else if let Some(child_index) = child.first_descendant_index() { let mut index = Some(child_index); while let Some(sub_child_index) = index { element = render_recursive( tree, sub_child_index, path, element, fallback_path, true, window, cx, ); index = tree[sub_child_index].next_sibling; } } else { element = element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted))) } if pushed_path { path.pop(); if let Some(fallback_path) = fallback_path.as_mut() { fallback_path.pop(); } } return element; } impl Render for SettingsPage { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new()); div() .grid() .grid_cols(16) .p_4() .bg(cx.theme().colors().editor_background) .size_full() .child( div() .id("settings-ui-nav") .col_span(2) .h_full() .child(render_nav(&self.settings_tree, window, cx)), ) .child( div().col_span(6).h_full().child( render_content(&self.settings_tree, window, cx) .id("settings-ui-content") .track_scroll(scroll_handle.read(cx)) .overflow_y_scroll(), ), ) } } // todo(settings_ui): remove, only here as inspiration #[allow(dead_code)] fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement { v_flex() .p_4() .size_full() .gap_4() .child(Label::new("Settings").size(LabelSize::Large)) .child( v_flex().gap_1().child(Label::new("Appearance")).child( v_flex() .elevation_2(cx) .child(AppearanceSettingsControls::new()), ), ) .child( v_flex().gap_1().child(Label::new("Editor")).child( v_flex() .elevation_2(cx) .child(EditorSettingsControls::new()), ), ) } fn element_id_from_path(path: &[SharedString]) -> ElementId { if path.len() == 0 { panic!("Path length must not be zero"); } else if path.len() == 1 { ElementId::Name(path[0].clone()) } else { ElementId::from(( ElementId::from(path[path.len() - 2].clone()), path[path.len() - 1].clone(), )) } } fn render_item_single( settings_value: SettingsValue, item: &SettingsUiItemSingle, window: &mut Window, cx: &mut App, ) -> AnyElement { match item { SettingsUiItemSingle::Custom(_) => div() .child(format!("Item: {}", settings_value.path.join("."))) .into_any_element(), SettingsUiItemSingle::SwitchField => { render_any_item(settings_value, render_switch_field, window, cx) } SettingsUiItemSingle::NumericStepper(num_type) => { render_any_numeric_stepper(settings_value, *num_type, window, cx) } SettingsUiItemSingle::ToggleGroup { variants: values, labels: titles, } => render_toggle_button_group(settings_value, values, titles, window, cx), SettingsUiItemSingle::DropDown { variants, labels } => { render_dropdown(settings_value, variants, labels, window, cx) } SettingsUiItemSingle::TextField => render_text_field(settings_value, window, cx), } } pub fn read_settings_value_from_path<'a>( settings_contents: &'a serde_json::Value, path: &[impl AsRef], ) -> Option<&'a serde_json::Value> { // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested let Some((key, remaining)) = path.split_first() else { return Some(settings_contents); }; let Some(value) = settings_contents.get(key.as_ref()) else { return None; }; read_settings_value_from_path(value, remaining) } fn downcast_any_item( settings_value: SettingsValue, ) -> SettingsValue { let value = settings_value.value.map(|value| { serde_json::from_value::(value.clone()) .with_context(|| format!("path: {:?}", settings_value.path.join("."))) .with_context(|| format!("value is not a {}: {}", std::any::type_name::(), value)) .unwrap() }); // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values let default_value = serde_json::from_value::(settings_value.default_value) .with_context(|| format!("path: {:?}", settings_value.path.join("."))) .with_context(|| format!("value is not a {}", std::any::type_name::())) .unwrap(); let deserialized_setting_value = SettingsValue { title: settings_value.title, path: settings_value.path, documentation: settings_value.documentation, value, default_value, }; deserialized_setting_value } fn render_any_item( settings_value: SettingsValue, render_fn: impl Fn(SettingsValue, &mut Window, &mut App) -> AnyElement + 'static, window: &mut Window, cx: &mut App, ) -> AnyElement { let deserialized_setting_value = downcast_any_item(settings_value); render_fn(deserialized_setting_value, window, cx) } fn render_any_numeric_stepper( settings_value: SettingsValue, num_type: NumType, window: &mut Window, cx: &mut App, ) -> AnyElement { match num_type { NumType::U64 => render_numeric_stepper::( downcast_any_item(settings_value), |n| u64::saturating_sub(n, 1), |n| u64::saturating_add(n, 1), |n| { serde_json::Number::try_from(n) .context("Failed to convert u64 to serde_json::Number") }, window, cx, ), NumType::U32 => render_numeric_stepper::( downcast_any_item(settings_value), |n| u32::saturating_sub(n, 1), |n| u32::saturating_add(n, 1), |n| { serde_json::Number::try_from(n) .context("Failed to convert u32 to serde_json::Number") }, window, cx, ), NumType::F32 => render_numeric_stepper::( downcast_any_item(settings_value), |a| a - 1.0, |a| a + 1.0, |n| { serde_json::Number::from_f64(n as f64) .context("Failed to convert f32 to serde_json::Number") }, window, cx, ), NumType::USIZE => render_numeric_stepper::( downcast_any_item(settings_value), |n| usize::saturating_sub(n, 1), |n| usize::saturating_add(n, 1), |n| { serde_json::Number::try_from(n) .context("Failed to convert usize to serde_json::Number") }, window, cx, ), NumType::U32NONZERO => render_numeric_stepper::( downcast_any_item(settings_value), |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN), |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX), |n| { serde_json::Number::try_from(n.get()) .context("Failed to convert usize to serde_json::Number") }, window, cx, ), } } fn render_numeric_stepper( value: SettingsValue, saturating_sub_1: fn(T) -> T, saturating_add_1: fn(T) -> T, to_serde_number: fn(T) -> anyhow::Result, _window: &mut Window, _cx: &mut App, ) -> AnyElement { let id = element_id_from_path(&value.path); let path = value.path.clone(); let num = *value.read(); NumericStepper::new( id, num.to_string(), { let path = value.path; move |_, _, cx| { let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else { return; }; let new_value = serde_json::Value::Number(number); SettingsValue::write_value(&path, new_value, cx); } }, move |_, _, cx| { let Some(number) = to_serde_number(saturating_add_1(num)).ok() else { return; }; let new_value = serde_json::Value::Number(number); SettingsValue::write_value(&path, new_value, cx); }, ) .style(ui::NumericStepperStyle::Outlined) .into_any_element() } fn render_switch_field( value: SettingsValue, _window: &mut Window, _cx: &mut App, ) -> AnyElement { let id = element_id_from_path(&value.path); let path = value.path.clone(); SwitchField::new( id, value.title.clone(), value.documentation.clone(), match value.read() { true => ToggleState::Selected, false => ToggleState::Unselected, }, move |toggle_state, _, cx| { let new_value = serde_json::Value::Bool(match toggle_state { ToggleState::Indeterminate => { return; } ToggleState::Selected => true, ToggleState::Unselected => false, }); SettingsValue::write_value(&path, new_value, cx); }, ) .into_any_element() } fn render_text_field( value: SettingsValue, window: &mut Window, cx: &mut App, ) -> AnyElement { let value = downcast_any_item::(value); let path = value.path.clone(); let editor = window.use_state(cx, { let path = path.clone(); move |window, cx| { let mut editor = Editor::single_line(window, cx); cx.observe_global_in::(window, move |editor, window, cx| { let user_settings = SettingsStore::global(cx).raw_user_settings(); if let Some(value) = read_settings_value_from_path(&user_settings, &path).cloned() && let Some(value) = value.as_str() { editor.set_text(value, window, cx); } }) .detach(); editor.set_text(value.read().clone(), window, cx); editor } }); let weak_editor = editor.downgrade(); let theme_colors = cx.theme().colors(); div() .child(editor) .bg(theme_colors.editor_background) .border_1() .rounded_lg() .border_color(theme_colors.border) .on_action::({ move |_, _, cx| { let new_value = weak_editor.read_with(cx, |editor, cx| editor.text(cx)).ok(); if let Some(new_value) = new_value { SettingsValue::write_value(&path, serde_json::Value::String(new_value), cx); } } }) .into_any_element() } fn render_toggle_button_group( value: SettingsValue, variants: &'static [&'static str], labels: &'static [&'static str], _: &mut Window, _: &mut App, ) -> AnyElement { let value = downcast_any_item::(value); let active_value = value.read(); let selected_idx = variants.iter().position(|v| v == &active_value); return render_toggle_button_group_inner(value.title, labels, selected_idx, { let path = value.path.clone(); move |variant_index, cx| { SettingsValue::write_value( &path, serde_json::Value::String(variants[variant_index].to_string()), cx, ); } }); } fn render_dropdown( value: SettingsValue, variants: &'static [&'static str], labels: &'static [&'static str], window: &mut Window, cx: &mut App, ) -> AnyElement { let value = downcast_any_item::(value); let id = element_id_from_path(&value.path); let menu = window.use_state(cx, |window, cx| { let path = value.path.clone(); let handler = Rc::new(move |variant: &'static str, cx: &mut App| { SettingsValue::write_value(&path, serde_json::Value::String(variant.to_string()), cx); }); ContextMenu::build(window, cx, |mut menu, _, _| { for (label, variant) in labels.iter().zip(variants) { menu = menu.entry(*label, None, { let handler = handler.clone(); move |_, cx| { handler(variant, cx); } }); } menu }) }); DropdownMenu::new(id, value.read(), menu.read(cx).clone()) .style(ui::DropdownStyle::Outlined) .into_any_element() } fn render_toggle_button_group_inner( title: SharedString, labels: &'static [&'static str], selected_idx: Option, on_write: impl Fn(usize, &mut App) + 'static, ) -> AnyElement { fn make_toggle_group( title: SharedString, selected_idx: Option, on_write: Rc, labels: &'static [&'static str], ) -> AnyElement { let labels_array: [&'static str; LEN] = { let mut arr = ["unused"; LEN]; arr.copy_from_slice(labels); arr }; let mut idx = 0; ToggleButtonGroup::single_row( title, labels_array.map(|label| { idx += 1; let on_write = on_write.clone(); ToggleButtonSimple::new(label, move |_, _, cx| { on_write(idx - 1, cx); }) }), ) .when_some(selected_idx, |this, ix| this.selected_index(ix)) .style(ui::ToggleButtonGroupStyle::Filled) .into_any_element() } let on_write = Rc::new(on_write); macro_rules! templ_toggl_with_const_param { ($len:expr) => { if labels.len() == $len { return make_toggle_group::<$len>(title.clone(), selected_idx, on_write, labels); } }; } templ_toggl_with_const_param!(1); templ_toggl_with_const_param!(2); templ_toggl_with_const_param!(3); templ_toggl_with_const_param!(4); templ_toggl_with_const_param!(5); templ_toggl_with_const_param!(6); unreachable!("Too many variants"); } fn settings_value_from_settings_and_path( path: SmallVec<[SharedString; 1]>, fallback_path: Option<&[SharedString]>, title: SharedString, documentation: Option, user_settings: &serde_json::Value, default_settings: &serde_json::Value, ) -> SettingsValue { let default_value = read_settings_value_from_path(default_settings, &path) .or_else(|| { fallback_path.and_then(|fallback_path| { read_settings_value_from_path(default_settings, fallback_path) }) }) .with_context(|| format!("No default value for item at path {:?}", path.join("."))) .expect("Default value set for item") .clone(); let value = read_settings_value_from_path(user_settings, &path).cloned(); let settings_value = SettingsValue { default_value, value, documentation, path, // todo(settings_ui) is title required inside SettingsValue? title, }; return settings_value; }