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