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::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 ¤t_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 }
616}
617
618pub fn read_settings_value_from_path<'a>(
619 settings_contents: &'a serde_json::Value,
620 path: &[impl AsRef<str>],
621) -> Option<&'a serde_json::Value> {
622 // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
623 let Some((key, remaining)) = path.split_first() else {
624 return Some(settings_contents);
625 };
626 let Some(value) = settings_contents.get(key.as_ref()) else {
627 return None;
628 };
629
630 read_settings_value_from_path(value, remaining)
631}
632
633fn downcast_any_item<T: serde::de::DeserializeOwned>(
634 settings_value: SettingsValue<serde_json::Value>,
635) -> SettingsValue<T> {
636 let value = settings_value.value.map(|value| {
637 serde_json::from_value::<T>(value.clone())
638 .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
639 .with_context(|| format!("value is not a {}: {}", std::any::type_name::<T>(), value))
640 .unwrap()
641 });
642 // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
643 let default_value = serde_json::from_value::<T>(settings_value.default_value)
644 .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
645 .with_context(|| format!("value is not a {}", std::any::type_name::<T>()))
646 .unwrap();
647 let deserialized_setting_value = SettingsValue {
648 title: settings_value.title,
649 path: settings_value.path,
650 documentation: settings_value.documentation,
651 value,
652 default_value,
653 };
654 deserialized_setting_value
655}
656
657fn render_any_item<T: serde::de::DeserializeOwned>(
658 settings_value: SettingsValue<serde_json::Value>,
659 render_fn: impl Fn(SettingsValue<T>, &mut Window, &mut App) -> AnyElement + 'static,
660 window: &mut Window,
661 cx: &mut App,
662) -> AnyElement {
663 let deserialized_setting_value = downcast_any_item(settings_value);
664 render_fn(deserialized_setting_value, window, cx)
665}
666
667fn render_any_numeric_stepper(
668 settings_value: SettingsValue<serde_json::Value>,
669 num_type: NumType,
670 window: &mut Window,
671 cx: &mut App,
672) -> AnyElement {
673 match num_type {
674 NumType::U64 => render_numeric_stepper::<u64>(
675 downcast_any_item(settings_value),
676 |n| u64::saturating_sub(n, 1),
677 |n| u64::saturating_add(n, 1),
678 |n| {
679 serde_json::Number::try_from(n)
680 .context("Failed to convert u64 to serde_json::Number")
681 },
682 window,
683 cx,
684 ),
685 NumType::U32 => render_numeric_stepper::<u32>(
686 downcast_any_item(settings_value),
687 |n| u32::saturating_sub(n, 1),
688 |n| u32::saturating_add(n, 1),
689 |n| {
690 serde_json::Number::try_from(n)
691 .context("Failed to convert u32 to serde_json::Number")
692 },
693 window,
694 cx,
695 ),
696 NumType::F32 => render_numeric_stepper::<f32>(
697 downcast_any_item(settings_value),
698 |a| a - 1.0,
699 |a| a + 1.0,
700 |n| {
701 serde_json::Number::from_f64(n as f64)
702 .context("Failed to convert f32 to serde_json::Number")
703 },
704 window,
705 cx,
706 ),
707 NumType::USIZE => render_numeric_stepper::<usize>(
708 downcast_any_item(settings_value),
709 |n| usize::saturating_sub(n, 1),
710 |n| usize::saturating_add(n, 1),
711 |n| {
712 serde_json::Number::try_from(n)
713 .context("Failed to convert usize to serde_json::Number")
714 },
715 window,
716 cx,
717 ),
718 NumType::U32NONZERO => render_numeric_stepper::<NonZeroU32>(
719 downcast_any_item(settings_value),
720 |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN),
721 |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX),
722 |n| {
723 serde_json::Number::try_from(n.get())
724 .context("Failed to convert usize to serde_json::Number")
725 },
726 window,
727 cx,
728 ),
729 }
730}
731
732fn render_numeric_stepper<T: serde::de::DeserializeOwned + std::fmt::Display + Copy + 'static>(
733 value: SettingsValue<T>,
734 saturating_sub_1: fn(T) -> T,
735 saturating_add_1: fn(T) -> T,
736 to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
737 _window: &mut Window,
738 _cx: &mut App,
739) -> AnyElement {
740 let id = element_id_from_path(&value.path);
741 let path = value.path.clone();
742 let num = *value.read();
743
744 NumericStepper::new(
745 id,
746 num.to_string(),
747 {
748 let path = value.path;
749 move |_, _, cx| {
750 let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else {
751 return;
752 };
753 let new_value = serde_json::Value::Number(number);
754 SettingsValue::write_value(&path, new_value, cx);
755 }
756 },
757 move |_, _, cx| {
758 let Some(number) = to_serde_number(saturating_add_1(num)).ok() else {
759 return;
760 };
761
762 let new_value = serde_json::Value::Number(number);
763
764 SettingsValue::write_value(&path, new_value, cx);
765 },
766 )
767 .style(ui::NumericStepperStyle::Outlined)
768 .into_any_element()
769}
770
771fn render_switch_field(
772 value: SettingsValue<bool>,
773 _window: &mut Window,
774 _cx: &mut App,
775) -> AnyElement {
776 let id = element_id_from_path(&value.path);
777 let path = value.path.clone();
778 SwitchField::new(
779 id,
780 value.title.clone(),
781 value.documentation.clone(),
782 match value.read() {
783 true => ToggleState::Selected,
784 false => ToggleState::Unselected,
785 },
786 move |toggle_state, _, cx| {
787 let new_value = serde_json::Value::Bool(match toggle_state {
788 ToggleState::Indeterminate => {
789 return;
790 }
791 ToggleState::Selected => true,
792 ToggleState::Unselected => false,
793 });
794
795 SettingsValue::write_value(&path, new_value, cx);
796 },
797 )
798 .into_any_element()
799}
800
801fn render_toggle_button_group(
802 value: SettingsValue<serde_json::Value>,
803 variants: &'static [&'static str],
804 labels: &'static [&'static str],
805 _: &mut Window,
806 _: &mut App,
807) -> AnyElement {
808 let value = downcast_any_item::<String>(value);
809
810 fn make_toggle_group<const LEN: usize>(
811 value: SettingsValue<String>,
812 variants: &'static [&'static str],
813 labels: &'static [&'static str],
814 ) -> AnyElement {
815 let mut variants_array: [(&'static str, &'static str); LEN] = [("unused", "unused"); LEN];
816 for i in 0..LEN {
817 variants_array[i] = (variants[i], labels[i]);
818 }
819 let active_value = value.read();
820
821 let selected_idx = variants_array
822 .iter()
823 .enumerate()
824 .find_map(|(idx, (variant, _))| {
825 if variant == &active_value {
826 Some(idx)
827 } else {
828 None
829 }
830 });
831
832 let mut idx = 0;
833 ToggleButtonGroup::single_row(
834 value.title.clone(),
835 variants_array.map(|(variant, label)| {
836 let path = value.path.clone();
837 idx += 1;
838 ToggleButtonSimple::new(label, move |_, _, cx| {
839 SettingsValue::write_value(
840 &path,
841 serde_json::Value::String(variant.to_string()),
842 cx,
843 );
844 })
845 }),
846 )
847 .when_some(selected_idx, |this, ix| this.selected_index(ix))
848 .style(ui::ToggleButtonGroupStyle::Filled)
849 .into_any_element()
850 }
851
852 macro_rules! templ_toggl_with_const_param {
853 ($len:expr) => {
854 if variants.len() == $len {
855 return make_toggle_group::<$len>(value, variants, labels);
856 }
857 };
858 }
859 templ_toggl_with_const_param!(1);
860 templ_toggl_with_const_param!(2);
861 templ_toggl_with_const_param!(3);
862 templ_toggl_with_const_param!(4);
863 templ_toggl_with_const_param!(5);
864 templ_toggl_with_const_param!(6);
865 unreachable!("Too many variants");
866}
867
868fn settings_value_from_settings_and_path(
869 path: SmallVec<[SharedString; 1]>,
870 fallback_path: Option<&[SharedString]>,
871 title: SharedString,
872 documentation: Option<SharedString>,
873 user_settings: &serde_json::Value,
874 default_settings: &serde_json::Value,
875) -> SettingsValue<serde_json::Value> {
876 let default_value = read_settings_value_from_path(default_settings, &path)
877 .or_else(|| {
878 fallback_path.and_then(|fallback_path| {
879 read_settings_value_from_path(default_settings, fallback_path)
880 })
881 })
882 .with_context(|| format!("No default value for item at path {:?}", path.join(".")))
883 .expect("Default value set for item")
884 .clone();
885
886 let value = read_settings_value_from_path(user_settings, &path).cloned();
887 let settings_value = SettingsValue {
888 default_value,
889 value,
890 documentation,
891 path,
892 // todo(settings_ui) is title required inside SettingsValue?
893 title,
894 };
895 return settings_value;
896}