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