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 ¤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 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}