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 SettingsUiItemArray, SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle,
16 SettingsUiItemUnion, SettingsValue,
17};
18use smallvec::SmallVec;
19use ui::{
20 ContextMenu, DropdownMenu, NumericStepper, SwitchField, TableInteractionState,
21 ToggleButtonGroup, ToggleButtonSimple, 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 array: Option<SettingsUiItemArray>,
161}
162
163impl UiEntry {
164 fn first_descendant_index(&self) -> Option<usize> {
165 return self
166 .descendant_range
167 .is_empty()
168 .not()
169 .then_some(self.descendant_range.start);
170 }
171
172 fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option<usize> {
173 let first_descendant_index = self.first_descendant_index()?;
174 let mut current_index = 0;
175 let mut current_descendant_index = Some(first_descendant_index);
176 while let Some(descendant_index) = current_descendant_index
177 && current_index < n
178 {
179 current_index += 1;
180 current_descendant_index = tree[descendant_index].next_sibling;
181 }
182 current_descendant_index
183 }
184}
185
186pub struct SettingsUiTree {
187 root_entry_indices: Vec<usize>,
188 entries: Vec<UiEntry>,
189 active_entry_index: usize,
190}
191
192fn build_tree_item(
193 tree: &mut Vec<UiEntry>,
194 entry: SettingsUiEntry,
195 depth: usize,
196 prev_index: Option<usize>,
197) {
198 // let tree: HashMap<Path, UiEntry>;
199 let index = tree.len();
200 tree.push(UiEntry {
201 title: entry.title.into(),
202 path: entry.path.map(SharedString::new_static),
203 documentation: entry.documentation.map(SharedString::new_static),
204 _depth: depth,
205 descendant_range: index + 1..index + 1,
206 total_descendant_range: index + 1..index + 1,
207 render: None,
208 next_sibling: None,
209 dynamic_render: None,
210 generate_items: None,
211 array: None,
212 });
213 if let Some(prev_index) = prev_index {
214 tree[prev_index].next_sibling = Some(index);
215 }
216 match entry.item {
217 SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => {
218 for group_item in group_items {
219 let prev_index = tree[index]
220 .descendant_range
221 .is_empty()
222 .not()
223 .then_some(tree[index].descendant_range.end - 1);
224 tree[index].descendant_range.end = tree.len() + 1;
225 build_tree_item(tree, group_item, depth + 1, prev_index);
226 tree[index].total_descendant_range.end = tree.len();
227 }
228 }
229 SettingsUiItem::Single(item) => {
230 tree[index].render = Some(item);
231 }
232 SettingsUiItem::Union(dynamic_render) => {
233 // todo(settings_ui) take from item and store other fields instead of clone
234 // will also require replacing usage in render_recursive so it can know
235 // which options were actually rendered
236 let options = dynamic_render.options.clone();
237 tree[index].dynamic_render = Some(dynamic_render);
238 for option in options {
239 let Some(option) = option else { continue };
240 let prev_index = tree[index]
241 .descendant_range
242 .is_empty()
243 .not()
244 .then_some(tree[index].descendant_range.end - 1);
245 tree[index].descendant_range.end = tree.len() + 1;
246 build_tree_item(tree, option, depth + 1, prev_index);
247 tree[index].total_descendant_range.end = tree.len();
248 }
249 }
250 SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap {
251 item: generate_settings_ui_item,
252 determine_items,
253 defaults_path,
254 }) => {
255 tree[index].generate_items = Some((
256 generate_settings_ui_item(),
257 determine_items,
258 defaults_path
259 .into_iter()
260 .copied()
261 .map(SharedString::new_static)
262 .collect(),
263 ));
264 }
265 SettingsUiItem::Array(array) => {
266 tree[index].array = Some(array);
267 }
268 SettingsUiItem::None => {
269 return;
270 }
271 }
272}
273
274impl SettingsUiTree {
275 pub fn new(cx: &App) -> Self {
276 let settings_store = SettingsStore::global(cx);
277 let mut tree = vec![];
278 let mut root_entry_indices = vec![];
279 for item in settings_store.settings_ui_items() {
280 if matches!(item.item, SettingsUiItem::None)
281 // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to
282 // link them to other groups
283 || matches!(item.item, SettingsUiItem::Single(_))
284 {
285 continue;
286 }
287
288 let prev_root_entry_index = root_entry_indices.last().copied();
289 root_entry_indices.push(tree.len());
290 build_tree_item(&mut tree, item, 0, prev_root_entry_index);
291 }
292
293 root_entry_indices.sort_by_key(|i| &tree[*i].title);
294
295 let active_entry_index = root_entry_indices[0];
296 Self {
297 entries: tree,
298 root_entry_indices,
299 active_entry_index,
300 }
301 }
302
303 // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree,
304 // so that we can keep none/skip and still test in CI that all settings have
305 #[cfg(feature = "test-support")]
306 pub fn all_paths(&self, cx: &App) -> Vec<Vec<SharedString>> {
307 fn all_paths_rec(
308 tree: &[UiEntry],
309 paths: &mut Vec<Vec<SharedString>>,
310 current_path: &mut Vec<SharedString>,
311 idx: usize,
312 cx: &App,
313 ) {
314 let child = &tree[idx];
315 let mut pushed_path = false;
316 if let Some(path) = child.path.as_ref() {
317 current_path.push(path.clone());
318 paths.push(current_path.clone());
319 pushed_path = true;
320 }
321 // todo(settings_ui): handle dynamic nodes here
322 let selected_descendant_index = child
323 .dynamic_render
324 .as_ref()
325 .map(|dynamic_render| {
326 read_settings_value_from_path(
327 SettingsStore::global(cx).raw_default_settings(),
328 ¤t_path,
329 )
330 .map(|value| (dynamic_render.determine_option)(value, cx))
331 })
332 .and_then(|selected_descendant_index| {
333 selected_descendant_index.map(|index| child.nth_descendant_index(tree, index))
334 });
335
336 if let Some(selected_descendant_index) = selected_descendant_index {
337 // just silently fail if we didn't find a setting value for the path
338 if let Some(descendant_index) = selected_descendant_index {
339 all_paths_rec(tree, paths, current_path, descendant_index, cx);
340 }
341 } else if let Some(desc_idx) = child.first_descendant_index() {
342 let mut desc_idx = Some(desc_idx);
343 while let Some(descendant_index) = desc_idx {
344 all_paths_rec(&tree, paths, current_path, descendant_index, cx);
345 desc_idx = tree[descendant_index].next_sibling;
346 }
347 }
348 if pushed_path {
349 current_path.pop();
350 }
351 }
352
353 let mut paths = Vec::new();
354 for &index in &self.root_entry_indices {
355 all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx);
356 }
357 paths
358 }
359}
360
361fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<SettingsPage>) -> Div {
362 let mut nav = v_flex().p_4().gap_2();
363 for &index in &tree.root_entry_indices {
364 nav = nav.child(
365 div()
366 .id(index)
367 .on_click(cx.listener(move |settings, _, _, _| {
368 settings.settings_tree.active_entry_index = index;
369 }))
370 .child(
371 Label::new(tree.entries[index].title.clone())
372 .size(LabelSize::Large)
373 .when(tree.active_entry_index == index, |this| {
374 this.color(Color::Selected)
375 }),
376 ),
377 );
378 }
379 nav
380}
381
382fn render_content(
383 tree: &SettingsUiTree,
384 window: &mut Window,
385 cx: &mut Context<SettingsPage>,
386) -> Div {
387 let content = v_flex().size_full().gap_4();
388
389 let mut path = smallvec::smallvec![];
390
391 return render_recursive(
392 &tree.entries,
393 tree.active_entry_index,
394 &mut path,
395 content,
396 None,
397 true,
398 window,
399 cx,
400 );
401}
402
403fn render_recursive(
404 tree: &[UiEntry],
405 entry_index: usize,
406 path: &mut SmallVec<[SharedString; 1]>,
407 mut element: Div,
408 parent_fallback_default_value: Option<&serde_json::Value>,
409 render_next_title: bool,
410 window: &mut Window,
411 cx: &mut App,
412) -> Div {
413 let Some(entry) = tree.get(entry_index) else {
414 return element
415 .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error));
416 };
417
418 if render_next_title {
419 element = element.child(Label::new(entry.title.clone()).size(LabelSize::Large));
420 }
421
422 // todo(settings_ui): subgroups?
423 let mut pushed_path = false;
424 let mut fallback_default_value = parent_fallback_default_value;
425 if let Some(child_path) = entry.path.as_ref() {
426 path.push(child_path.clone());
427 if let Some(fallback_value) = parent_fallback_default_value.as_ref() {
428 fallback_default_value =
429 read_settings_value_from_path(fallback_value, std::slice::from_ref(&child_path));
430 }
431 pushed_path = true;
432 }
433 let settings_value = settings_value_from_settings_and_path(
434 path.clone(),
435 fallback_default_value,
436 entry.title.clone(),
437 entry.documentation.clone(),
438 // PERF: how to structure this better? There feels like there's a way to avoid the clone
439 // and every value lookup
440 SettingsStore::global(cx).raw_user_settings(),
441 SettingsStore::global(cx).raw_default_settings(),
442 );
443 if let Some(dynamic_render) = entry.dynamic_render.as_ref() {
444 let value = settings_value.read();
445 let selected_index = (dynamic_render.determine_option)(value, cx);
446 element = element.child(div().child(render_toggle_button_group_inner(
447 settings_value.title.clone(),
448 dynamic_render.labels,
449 Some(selected_index),
450 {
451 let path = settings_value.path.clone();
452 let defaults = dynamic_render.defaults.clone();
453 move |idx, cx| {
454 if idx == selected_index {
455 return;
456 }
457 let default = defaults.get(idx).cloned().unwrap_or_default();
458 SettingsValue::write_value(&path, default, cx);
459 }
460 },
461 )));
462 // we don't add descendants for unit options, so we adjust the selected index
463 // by the number of options we didn't add descendants for, to get the descendant index
464 let selected_descendant_index = selected_index
465 - dynamic_render.options[..selected_index]
466 .iter()
467 .filter(|option| option.is_none())
468 .count();
469 if dynamic_render.options[selected_index].is_some()
470 && let Some(descendant_index) =
471 entry.nth_descendant_index(tree, selected_descendant_index)
472 {
473 element = render_recursive(
474 tree,
475 descendant_index,
476 path,
477 element,
478 fallback_default_value,
479 false,
480 window,
481 cx,
482 );
483 }
484 } else if let Some(array) = entry.array.as_ref() {
485 let generated_items = (array.determine_items)(settings_value.read(), cx);
486 let mut ui_items = Vec::with_capacity(generated_items.len());
487 let settings_ui_item = (array.item)();
488
489 let table_interaction_state =
490 window.use_keyed_state(("settings_ui_table", entry_index), cx, |_, cx| {
491 TableInteractionState::new(cx)
492 });
493
494 let mut table = ui::Table::<2>::new()
495 .column_widths([relative(0.1), relative(0.9)])
496 .header(["#", "Value"])
497 .interactable(&table_interaction_state)
498 .striped();
499 let mut row_count = 0;
500
501 // todo(settings_ui): Try to make the static item on these items built into the tree
502 // because we already propagate the path down so they don't all have to be recreated
503 for (index, item) in generated_items.iter().enumerate() {
504 let settings_ui_entry = SettingsUiEntry {
505 path: None,
506 title: "",
507 documentation: None,
508 item: settings_ui_item.clone(),
509 };
510 let prev_index = if ui_items.is_empty() {
511 None
512 } else {
513 Some(ui_items.len() - 1)
514 };
515 let item_index = ui_items.len();
516 build_tree_item(
517 &mut ui_items,
518 settings_ui_entry,
519 entry._depth + 1,
520 prev_index,
521 );
522 if item_index < ui_items.len() {
523 ui_items[item_index].path = None;
524 ui_items[item_index].title = item.title.clone();
525 ui_items[item_index].documentation = item.documentation.clone();
526
527 // push path instead of setting path on ui item so that the path isn't
528 // pushed to default_default_value as well when we recurse
529 path.push(item.path.clone());
530 dbg!(path.join("."));
531 let row_element = render_recursive(
532 &ui_items,
533 item_index,
534 path,
535 div(),
536 Some(&array.default_item),
537 false,
538 window,
539 cx,
540 );
541 path.pop();
542 table = table.row([index.to_string().into_any_element(), row_element.into_any()]);
543 row_count += 1;
544 }
545 }
546 table = table.row([
547 row_count.to_string().into_any_element(),
548 "create".into_any(),
549 ]);
550 element = element.child(div().child(table).debug());
551 } else if let Some((settings_ui_item, generate_items, defaults_path)) =
552 entry.generate_items.as_ref()
553 {
554 let generated_items = generate_items(settings_value.read(), cx);
555 let mut ui_items = Vec::with_capacity(generated_items.len());
556 let default_value = read_settings_value_from_path(
557 SettingsStore::global(cx).raw_default_settings(),
558 &defaults_path,
559 )
560 .cloned();
561 for item in generated_items {
562 let settings_ui_entry = SettingsUiEntry {
563 path: None,
564 title: "",
565 documentation: None,
566 item: settings_ui_item.clone(),
567 };
568 let prev_index = if ui_items.is_empty() {
569 None
570 } else {
571 Some(ui_items.len() - 1)
572 };
573 let item_index = ui_items.len();
574 build_tree_item(
575 &mut ui_items,
576 settings_ui_entry,
577 entry._depth + 1,
578 prev_index,
579 );
580 if item_index < ui_items.len() {
581 ui_items[item_index].path = None;
582 ui_items[item_index].title = item.title.clone();
583 ui_items[item_index].documentation = item.documentation.clone();
584
585 // push path instead of setting path on ui item so that the path isn't pushed to default_path as well
586 // when we recurse
587 path.push(item.path.clone());
588 element = render_recursive(
589 &ui_items,
590 item_index,
591 path,
592 element,
593 default_value.as_ref(),
594 true,
595 window,
596 cx,
597 );
598 path.pop();
599 }
600 }
601 } else if let Some(child_render) = entry.render.as_ref() {
602 element = element.child(div().child(render_item_single(
603 settings_value,
604 child_render,
605 window,
606 cx,
607 )));
608 } else if let Some(child_index) = entry.first_descendant_index() {
609 let mut index = Some(child_index);
610 while let Some(sub_child_index) = index {
611 element = render_recursive(
612 tree,
613 sub_child_index,
614 path,
615 element,
616 fallback_default_value,
617 true,
618 window,
619 cx,
620 );
621 index = tree[sub_child_index].next_sibling;
622 }
623 } else {
624 element = element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
625 }
626
627 if pushed_path {
628 path.pop();
629 }
630 return element;
631}
632
633impl Render for SettingsPage {
634 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
635 let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new());
636 div()
637 .grid()
638 .grid_cols(16)
639 .p_4()
640 .bg(cx.theme().colors().editor_background)
641 .size_full()
642 .child(
643 div()
644 .id("settings-ui-nav")
645 .col_span(2)
646 .h_full()
647 .child(render_nav(&self.settings_tree, window, cx)),
648 )
649 .child(
650 div().col_span(6).h_full().child(
651 render_content(&self.settings_tree, window, cx)
652 .id("settings-ui-content")
653 .track_scroll(scroll_handle.read(cx))
654 .overflow_y_scroll(),
655 ),
656 )
657 }
658}
659
660// todo(settings_ui): remove, only here as inspiration
661#[allow(dead_code)]
662fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement {
663 v_flex()
664 .p_4()
665 .size_full()
666 .gap_4()
667 .child(Label::new("Settings").size(LabelSize::Large))
668 .child(
669 v_flex().gap_1().child(Label::new("Appearance")).child(
670 v_flex()
671 .elevation_2(cx)
672 .child(AppearanceSettingsControls::new()),
673 ),
674 )
675 .child(
676 v_flex().gap_1().child(Label::new("Editor")).child(
677 v_flex()
678 .elevation_2(cx)
679 .child(EditorSettingsControls::new()),
680 ),
681 )
682}
683
684fn element_id_from_path(path: &[SharedString]) -> ElementId {
685 if path.len() == 0 {
686 panic!("Path length must not be zero");
687 } else if path.len() == 1 {
688 ElementId::Name(path[0].clone())
689 } else {
690 ElementId::from((
691 ElementId::from(path[path.len() - 2].clone()),
692 path[path.len() - 1].clone(),
693 ))
694 }
695}
696
697fn render_item_single(
698 settings_value: SettingsValue<serde_json::Value>,
699 item: &SettingsUiItemSingle,
700 window: &mut Window,
701 cx: &mut App,
702) -> AnyElement {
703 match item {
704 SettingsUiItemSingle::Custom(_) => div()
705 .child(format!("Item: {}", settings_value.path.join(".")))
706 .into_any_element(),
707 SettingsUiItemSingle::SwitchField => {
708 render_any_item(settings_value, render_switch_field, window, cx)
709 }
710 SettingsUiItemSingle::NumericStepper(num_type) => {
711 render_any_numeric_stepper(settings_value, *num_type, window, cx)
712 }
713 SettingsUiItemSingle::ToggleGroup {
714 variants: values,
715 labels: titles,
716 } => render_toggle_button_group(settings_value, values, titles, window, cx),
717 SettingsUiItemSingle::DropDown { variants, labels } => {
718 render_dropdown(settings_value, variants, labels, window, cx)
719 }
720 SettingsUiItemSingle::TextField => render_text_field(settings_value, window, cx),
721 }
722}
723
724pub fn read_settings_value_from_path<'a>(
725 settings_contents: &'a serde_json::Value,
726 path: &[impl AsRef<str>],
727) -> Option<&'a serde_json::Value> {
728 // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
729 let Some((key, remaining)) = path.split_first() else {
730 return Some(settings_contents);
731 };
732 let Some(value) = settings_contents.get(key.as_ref()) else {
733 return None;
734 };
735
736 read_settings_value_from_path(value, remaining)
737}
738
739fn downcast_any_item<T: serde::de::DeserializeOwned>(
740 settings_value: SettingsValue<serde_json::Value>,
741) -> SettingsValue<T> {
742 let value = settings_value.value.map(|value| {
743 serde_json::from_value::<T>(value.clone())
744 .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
745 .with_context(|| format!("value is not a {}: {}", std::any::type_name::<T>(), value))
746 .unwrap()
747 });
748 // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
749 let default_value = serde_json::from_value::<T>(settings_value.default_value)
750 .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
751 .with_context(|| format!("value is not a {}", std::any::type_name::<T>()))
752 .unwrap();
753 let deserialized_setting_value = SettingsValue {
754 title: settings_value.title,
755 path: settings_value.path,
756 documentation: settings_value.documentation,
757 value,
758 default_value,
759 };
760 deserialized_setting_value
761}
762
763fn render_any_item<T: serde::de::DeserializeOwned>(
764 settings_value: SettingsValue<serde_json::Value>,
765 render_fn: impl Fn(SettingsValue<T>, &mut Window, &mut App) -> AnyElement + 'static,
766 window: &mut Window,
767 cx: &mut App,
768) -> AnyElement {
769 let deserialized_setting_value = downcast_any_item(settings_value);
770 render_fn(deserialized_setting_value, window, cx)
771}
772
773fn render_any_numeric_stepper(
774 settings_value: SettingsValue<serde_json::Value>,
775 num_type: NumType,
776 window: &mut Window,
777 cx: &mut App,
778) -> AnyElement {
779 match num_type {
780 NumType::U64 => render_numeric_stepper::<u64>(
781 downcast_any_item(settings_value),
782 |n| u64::saturating_sub(n, 1),
783 |n| u64::saturating_add(n, 1),
784 |n| {
785 serde_json::Number::try_from(n)
786 .context("Failed to convert u64 to serde_json::Number")
787 },
788 window,
789 cx,
790 ),
791 NumType::U32 => render_numeric_stepper::<u32>(
792 downcast_any_item(settings_value),
793 |n| u32::saturating_sub(n, 1),
794 |n| u32::saturating_add(n, 1),
795 |n| {
796 serde_json::Number::try_from(n)
797 .context("Failed to convert u32 to serde_json::Number")
798 },
799 window,
800 cx,
801 ),
802 NumType::F32 => render_numeric_stepper::<f32>(
803 downcast_any_item(settings_value),
804 |a| a - 1.0,
805 |a| a + 1.0,
806 |n| {
807 serde_json::Number::from_f64(n as f64)
808 .context("Failed to convert f32 to serde_json::Number")
809 },
810 window,
811 cx,
812 ),
813 NumType::USIZE => render_numeric_stepper::<usize>(
814 downcast_any_item(settings_value),
815 |n| usize::saturating_sub(n, 1),
816 |n| usize::saturating_add(n, 1),
817 |n| {
818 serde_json::Number::try_from(n)
819 .context("Failed to convert usize to serde_json::Number")
820 },
821 window,
822 cx,
823 ),
824 NumType::U32NONZERO => render_numeric_stepper::<NonZeroU32>(
825 downcast_any_item(settings_value),
826 |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN),
827 |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX),
828 |n| {
829 serde_json::Number::try_from(n.get())
830 .context("Failed to convert usize to serde_json::Number")
831 },
832 window,
833 cx,
834 ),
835 }
836}
837
838fn render_numeric_stepper<T: serde::de::DeserializeOwned + std::fmt::Display + Copy + 'static>(
839 value: SettingsValue<T>,
840 saturating_sub_1: fn(T) -> T,
841 saturating_add_1: fn(T) -> T,
842 to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
843 _window: &mut Window,
844 _cx: &mut App,
845) -> AnyElement {
846 let id = element_id_from_path(&value.path);
847 let path = value.path.clone();
848 let num = *value.read();
849
850 NumericStepper::new(
851 id,
852 num.to_string(),
853 {
854 let path = value.path;
855 move |_, _, cx| {
856 let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else {
857 return;
858 };
859 let new_value = serde_json::Value::Number(number);
860 SettingsValue::write_value(&path, new_value, cx);
861 }
862 },
863 move |_, _, cx| {
864 let Some(number) = to_serde_number(saturating_add_1(num)).ok() else {
865 return;
866 };
867
868 let new_value = serde_json::Value::Number(number);
869
870 SettingsValue::write_value(&path, new_value, cx);
871 },
872 )
873 .style(ui::NumericStepperStyle::Outlined)
874 .into_any_element()
875}
876
877fn render_switch_field(
878 value: SettingsValue<bool>,
879 _window: &mut Window,
880 _cx: &mut App,
881) -> AnyElement {
882 let id = element_id_from_path(&value.path);
883 let path = value.path.clone();
884 SwitchField::new(
885 id,
886 value.title.clone(),
887 value.documentation.clone(),
888 match value.read() {
889 true => ToggleState::Selected,
890 false => ToggleState::Unselected,
891 },
892 move |toggle_state, _, cx| {
893 let new_value = serde_json::Value::Bool(match toggle_state {
894 ToggleState::Indeterminate => {
895 return;
896 }
897 ToggleState::Selected => true,
898 ToggleState::Unselected => false,
899 });
900
901 SettingsValue::write_value(&path, new_value, cx);
902 },
903 )
904 .into_any_element()
905}
906
907fn render_text_field(
908 value: SettingsValue<serde_json::Value>,
909 window: &mut Window,
910 cx: &mut App,
911) -> AnyElement {
912 let value = downcast_any_item::<String>(value);
913 let path = value.path.clone();
914 let current_text = value.read().clone();
915
916 let dirty = window.use_keyed_state((element_id_from_path(&path), "dirty"), cx, |_, _| false);
917
918 let editor = window.use_keyed_state((element_id_from_path(&path), "editor"), cx, {
919 let path = path.clone();
920 let dirty = dirty.clone();
921 move |window, cx| {
922 let mut editor = Editor::single_line(window, cx);
923 // editor.set_text(current_text, window, cx);
924
925 let dirty = dirty.downgrade();
926 cx.subscribe_self(move |_, event: &editor::EditorEvent, cx| match event {
927 editor::EditorEvent::BufferEdited => {
928 let Some(dirty) = dirty.upgrade() else { return };
929 dirty.write(cx, true);
930 }
931 _ => {}
932 })
933 .detach();
934 // cx.observe_global_in::<SettingsStore>(window, move |editor, window, cx| {
935 // let user_settings = SettingsStore::global(cx).raw_user_settings();
936 // if let Some(value) = read_settings_value_from_path(&user_settings, &path)
937 // .and_then(serde_json::Value::as_str)
938 // .map(str::to_string)
939 // {
940 // editor.set_text(value, window, cx);
941 // }
942 // // else {
943 // // editor.clear(window, cx);
944 // // }
945 // })
946 // .detach();
947
948 editor
949 }
950 });
951
952 // todo! WAAY to slow
953 editor.update(cx, |editor, cx| {
954 if &editor.text(cx) != ¤t_text && !*dirty.read(cx) {
955 editor.set_text(current_text, window, cx);
956 }
957 });
958
959 let weak_editor = editor.downgrade();
960 let theme_colors = cx.theme().colors();
961
962 div()
963 .child(editor)
964 .bg(theme_colors.editor_background)
965 .border_1()
966 .rounded_lg()
967 .border_color(theme_colors.border)
968 .on_action::<menu::Confirm>({
969 move |_, _, cx| {
970 let new_value = weak_editor.read_with(cx, |editor, cx| editor.text(cx)).ok();
971
972 if let Some(new_value) = new_value {
973 SettingsValue::write_value(&path, serde_json::Value::String(new_value), cx);
974 }
975 }
976 })
977 .into_any_element()
978}
979
980fn render_toggle_button_group(
981 value: SettingsValue<serde_json::Value>,
982 variants: &'static [&'static str],
983 labels: &'static [&'static str],
984 _: &mut Window,
985 _: &mut App,
986) -> AnyElement {
987 let value = downcast_any_item::<String>(value);
988 let active_value = value.read();
989 let selected_idx = variants.iter().position(|v| v == &active_value);
990
991 return render_toggle_button_group_inner(value.title, labels, selected_idx, {
992 let path = value.path.clone();
993 move |variant_index, cx| {
994 SettingsValue::write_value(
995 &path,
996 serde_json::Value::String(variants[variant_index].to_string()),
997 cx,
998 );
999 }
1000 });
1001}
1002
1003fn render_dropdown(
1004 value: SettingsValue<serde_json::Value>,
1005 variants: &'static [&'static str],
1006 labels: &'static [&'static str],
1007 window: &mut Window,
1008 cx: &mut App,
1009) -> AnyElement {
1010 let value = downcast_any_item::<String>(value);
1011 let id = element_id_from_path(&value.path);
1012
1013 let menu = window.use_keyed_state(id.clone(), cx, |window, cx| {
1014 let path = value.path.clone();
1015 let handler = Rc::new(move |variant: &'static str, cx: &mut App| {
1016 SettingsValue::write_value(&path, serde_json::Value::String(variant.to_string()), cx);
1017 });
1018
1019 ContextMenu::build(window, cx, |mut menu, _, _| {
1020 for (label, variant) in labels.iter().zip(variants) {
1021 menu = menu.entry(*label, None, {
1022 let handler = handler.clone();
1023 move |_, cx| {
1024 handler(variant, cx);
1025 }
1026 });
1027 }
1028
1029 menu
1030 })
1031 });
1032
1033 DropdownMenu::new(id, value.read(), menu.read(cx).clone())
1034 .style(ui::DropdownStyle::Outlined)
1035 .into_any_element()
1036}
1037
1038fn render_toggle_button_group_inner(
1039 title: SharedString,
1040 labels: &'static [&'static str],
1041 selected_idx: Option<usize>,
1042 on_write: impl Fn(usize, &mut App) + 'static,
1043) -> AnyElement {
1044 fn make_toggle_group<const LEN: usize>(
1045 title: SharedString,
1046 selected_idx: Option<usize>,
1047 on_write: Rc<dyn Fn(usize, &mut App)>,
1048 labels: &'static [&'static str],
1049 ) -> AnyElement {
1050 let labels_array: [&'static str; LEN] = {
1051 let mut arr = ["unused"; LEN];
1052 arr.copy_from_slice(labels);
1053 arr
1054 };
1055
1056 let mut idx = 0;
1057 ToggleButtonGroup::single_row(
1058 title,
1059 labels_array.map(|label| {
1060 idx += 1;
1061 let on_write = on_write.clone();
1062 ToggleButtonSimple::new(label, move |_, _, cx| {
1063 on_write(idx - 1, cx);
1064 })
1065 }),
1066 )
1067 .when_some(selected_idx, |this, ix| this.selected_index(ix))
1068 .style(ui::ToggleButtonGroupStyle::Filled)
1069 .into_any_element()
1070 }
1071
1072 let on_write = Rc::new(on_write);
1073
1074 macro_rules! templ_toggl_with_const_param {
1075 ($len:expr) => {
1076 if labels.len() == $len {
1077 return make_toggle_group::<$len>(title.clone(), selected_idx, on_write, labels);
1078 }
1079 };
1080 }
1081 templ_toggl_with_const_param!(1);
1082 templ_toggl_with_const_param!(2);
1083 templ_toggl_with_const_param!(3);
1084 templ_toggl_with_const_param!(4);
1085 templ_toggl_with_const_param!(5);
1086 templ_toggl_with_const_param!(6);
1087 unreachable!("Too many variants");
1088}
1089
1090fn settings_value_from_settings_and_path(
1091 path: SmallVec<[SharedString; 1]>,
1092 fallback_value: Option<&serde_json::Value>,
1093 title: SharedString,
1094 documentation: Option<SharedString>,
1095 user_settings: &serde_json::Value,
1096 default_settings: &serde_json::Value,
1097) -> SettingsValue<serde_json::Value> {
1098 let default_value = read_settings_value_from_path(default_settings, &path)
1099 .or_else(|| fallback_value)
1100 .with_context(|| format!("No default value for item at path {:?}", path.join(".")))
1101 .expect("Default value set for item")
1102 .clone();
1103
1104 let value = read_settings_value_from_path(user_settings, &path).cloned();
1105 let settings_value = SettingsValue {
1106 default_value,
1107 value,
1108 documentation,
1109 path,
1110 // todo(settings_ui) is title required inside SettingsValue?
1111 title,
1112 };
1113 return settings_value;
1114}