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