1mod appearance_settings_controls;
2
3use std::any::TypeId;
4use std::collections::VecDeque;
5use std::ops::{Not, Range};
6
7use anyhow::Context as _;
8use command_palette_hooks::CommandPaletteFilter;
9use editor::EditorSettingsControls;
10use feature_flags::{FeatureFlag, FeatureFlagViewExt};
11use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, actions};
12use settings::{
13 NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic,
14 SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue,
15};
16use smallvec::SmallVec;
17use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*};
18use workspace::{
19 Workspace,
20 item::{Item, ItemEvent},
21 with_active_or_new_workspace,
22};
23
24use crate::appearance_settings_controls::AppearanceSettingsControls;
25
26pub struct SettingsUiFeatureFlag;
27
28impl FeatureFlag for SettingsUiFeatureFlag {
29 const NAME: &'static str = "settings-ui";
30}
31
32actions!(
33 zed,
34 [
35 /// Opens the settings editor.
36 OpenSettingsEditor
37 ]
38);
39
40pub fn init(cx: &mut App) {
41 cx.on_action(|_: &OpenSettingsEditor, cx| {
42 with_active_or_new_workspace(cx, move |workspace, window, cx| {
43 let existing = workspace
44 .active_pane()
45 .read(cx)
46 .items()
47 .find_map(|item| item.downcast::<SettingsPage>());
48
49 if let Some(existing) = existing {
50 workspace.activate_item(&existing, true, true, window, cx);
51 } else {
52 let settings_page = SettingsPage::new(workspace, cx);
53 workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
54 }
55 });
56 });
57
58 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
59 let Some(window) = window else {
60 return;
61 };
62
63 let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
64
65 CommandPaletteFilter::update_global(cx, |filter, _cx| {
66 filter.hide_action_types(&settings_ui_actions);
67 });
68
69 cx.observe_flag::<SettingsUiFeatureFlag, _>(
70 window,
71 move |is_enabled, _workspace, _, cx| {
72 if is_enabled {
73 CommandPaletteFilter::update_global(cx, |filter, _cx| {
74 filter.show_action_types(settings_ui_actions.iter());
75 });
76 } else {
77 CommandPaletteFilter::update_global(cx, |filter, _cx| {
78 filter.hide_action_types(&settings_ui_actions);
79 });
80 }
81 },
82 )
83 .detach();
84 })
85 .detach();
86}
87
88pub struct SettingsPage {
89 focus_handle: FocusHandle,
90 settings_tree: SettingsUiTree,
91}
92
93impl SettingsPage {
94 pub fn new(_workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
95 cx.new(|cx| Self {
96 focus_handle: cx.focus_handle(),
97 settings_tree: SettingsUiTree::new(cx),
98 })
99 }
100}
101
102impl EventEmitter<ItemEvent> for SettingsPage {}
103
104impl Focusable for SettingsPage {
105 fn focus_handle(&self, _cx: &App) -> FocusHandle {
106 self.focus_handle.clone()
107 }
108}
109
110impl Item for SettingsPage {
111 type Event = ItemEvent;
112
113 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
114 Some(Icon::new(IconName::Settings))
115 }
116
117 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
118 "Settings".into()
119 }
120
121 fn show_toolbar(&self) -> bool {
122 false
123 }
124
125 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
126 f(*event)
127 }
128}
129
130// We want to iterate over the side bar with root groups
131// - this is a loop over top level groups, and if any are expanded, recursively displaying their items
132// - Should be able to get all items from a group (flatten a group)
133// - Should be able to toggle/untoggle groups in UI (at least in sidebar)
134// - Search should be available
135// - there should be an index of text -> item mappings, for using fuzzy::match
136// - Do we want to show the parent groups when a item is matched?
137
138struct UiEntry {
139 title: &'static str,
140 path: Option<&'static str>,
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 select_descendant: Option<fn(&serde_json::Value, &mut App) -> usize>,
155}
156
157impl UiEntry {
158 fn first_descendant_index(&self) -> Option<usize> {
159 return self
160 .descendant_range
161 .is_empty()
162 .not()
163 .then_some(self.descendant_range.start);
164 }
165
166 fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option<usize> {
167 let first_descendant_index = self.first_descendant_index()?;
168 let mut current_index = 0;
169 let mut current_descendant_index = Some(first_descendant_index);
170 while let Some(descendant_index) = current_descendant_index
171 && current_index < n
172 {
173 current_index += 1;
174 current_descendant_index = tree[descendant_index].next_sibling;
175 }
176 current_descendant_index
177 }
178}
179
180struct SettingsUiTree {
181 root_entry_indices: Vec<usize>,
182 entries: Vec<UiEntry>,
183 active_entry_index: usize,
184}
185
186fn build_tree_item(
187 tree: &mut Vec<UiEntry>,
188 entry: SettingsUiEntry,
189 depth: usize,
190 prev_index: Option<usize>,
191) {
192 let index = tree.len();
193 tree.push(UiEntry {
194 title: entry.title,
195 path: entry.path,
196 _depth: depth,
197 descendant_range: index + 1..index + 1,
198 total_descendant_range: index + 1..index + 1,
199 render: None,
200 next_sibling: None,
201 select_descendant: None,
202 });
203 if let Some(prev_index) = prev_index {
204 tree[prev_index].next_sibling = Some(index);
205 }
206 match entry.item {
207 SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => {
208 for group_item in group_items {
209 let prev_index = tree[index]
210 .descendant_range
211 .is_empty()
212 .not()
213 .then_some(tree[index].descendant_range.end - 1);
214 tree[index].descendant_range.end = tree.len() + 1;
215 build_tree_item(tree, group_item, depth + 1, prev_index);
216 tree[index].total_descendant_range.end = tree.len();
217 }
218 }
219 SettingsUiItem::Single(item) => {
220 tree[index].render = Some(item);
221 }
222 SettingsUiItem::Dynamic(SettingsUiItemDynamic {
223 options,
224 determine_option,
225 }) => {
226 tree[index].select_descendant = Some(determine_option);
227 for option in options {
228 let prev_index = tree[index]
229 .descendant_range
230 .is_empty()
231 .not()
232 .then_some(tree[index].descendant_range.end - 1);
233 tree[index].descendant_range.end = tree.len() + 1;
234 build_tree_item(tree, option, depth + 1, prev_index);
235 tree[index].total_descendant_range.end = tree.len();
236 }
237 }
238 SettingsUiItem::None => {
239 return;
240 }
241 }
242}
243
244impl SettingsUiTree {
245 fn new(cx: &App) -> Self {
246 let settings_store = SettingsStore::global(cx);
247 let mut tree = vec![];
248 let mut root_entry_indices = vec![];
249 for item in settings_store.settings_ui_items() {
250 if matches!(item.item, SettingsUiItem::None)
251 // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to
252 // link them to other groups
253 || matches!(item.item, SettingsUiItem::Single(_))
254 {
255 continue;
256 }
257
258 let prev_root_entry_index = root_entry_indices.last().copied();
259 root_entry_indices.push(tree.len());
260 build_tree_item(&mut tree, item, 0, prev_root_entry_index);
261 }
262
263 root_entry_indices.sort_by_key(|i| tree[*i].title);
264
265 let active_entry_index = root_entry_indices[0];
266 Self {
267 entries: tree,
268 root_entry_indices,
269 active_entry_index,
270 }
271 }
272}
273
274fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<SettingsPage>) -> Div {
275 let mut nav = v_flex().p_4().gap_2();
276 for &index in &tree.root_entry_indices {
277 nav = nav.child(
278 div()
279 .id(index)
280 .on_click(cx.listener(move |settings, _, _, _| {
281 settings.settings_tree.active_entry_index = index;
282 }))
283 .child(
284 Label::new(SharedString::new_static(tree.entries[index].title))
285 .size(LabelSize::Large)
286 .when(tree.active_entry_index == index, |this| {
287 this.color(Color::Selected)
288 }),
289 ),
290 );
291 }
292 nav
293}
294
295fn render_content(
296 tree: &SettingsUiTree,
297 window: &mut Window,
298 cx: &mut Context<SettingsPage>,
299) -> impl IntoElement {
300 let Some(active_entry) = tree.entries.get(tree.active_entry_index) else {
301 return div()
302 .size_full()
303 .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error));
304 };
305 let mut content = v_flex().size_full().gap_4().overflow_hidden();
306
307 let mut path = smallvec::smallvec![];
308 if let Some(active_entry_path) = active_entry.path {
309 path.push(active_entry_path);
310 }
311 let mut entry_index_queue = VecDeque::new();
312
313 if let Some(child_index) = active_entry.first_descendant_index() {
314 entry_index_queue.push_back(child_index);
315 let mut index = child_index;
316 while let Some(next_sibling_index) = tree.entries[index].next_sibling {
317 entry_index_queue.push_back(next_sibling_index);
318 index = next_sibling_index;
319 }
320 };
321
322 while let Some(index) = entry_index_queue.pop_front() {
323 // todo(settings_ui): subgroups?
324 let child = &tree.entries[index];
325 let mut pushed_path = false;
326 if let Some(child_path) = child.path {
327 path.push(child_path);
328 pushed_path = true;
329 }
330 let settings_value = settings_value_from_settings_and_path(
331 path.clone(),
332 child.title,
333 // PERF: how to structure this better? There feels like there's a way to avoid the clone
334 // and every value lookup
335 SettingsStore::global(cx).raw_user_settings(),
336 SettingsStore::global(cx).raw_default_settings(),
337 );
338 if let Some(select_descendant) = child.select_descendant {
339 let selected_descendant = select_descendant(settings_value.read(), cx);
340 if let Some(descendant_index) =
341 child.nth_descendant_index(&tree.entries, selected_descendant)
342 {
343 entry_index_queue.push_front(descendant_index);
344 }
345 }
346 if pushed_path {
347 path.pop();
348 }
349 let Some(child_render) = child.render.as_ref() else {
350 continue;
351 };
352 content = content.child(
353 div()
354 .child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large))
355 .child(render_item_single(settings_value, child_render, window, cx)),
356 );
357 }
358
359 return content;
360}
361
362impl Render for SettingsPage {
363 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
364 div()
365 .grid()
366 .grid_cols(16)
367 .p_4()
368 .bg(cx.theme().colors().editor_background)
369 .size_full()
370 .child(
371 div()
372 .col_span(2)
373 .h_full()
374 .child(render_nav(&self.settings_tree, window, cx)),
375 )
376 .child(div().col_span(4).h_full().child(render_content(
377 &self.settings_tree,
378 window,
379 cx,
380 )))
381 }
382}
383
384// todo(settings_ui): remove, only here as inspiration
385#[allow(dead_code)]
386fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement {
387 v_flex()
388 .p_4()
389 .size_full()
390 .gap_4()
391 .child(Label::new("Settings").size(LabelSize::Large))
392 .child(
393 v_flex().gap_1().child(Label::new("Appearance")).child(
394 v_flex()
395 .elevation_2(cx)
396 .child(AppearanceSettingsControls::new()),
397 ),
398 )
399 .child(
400 v_flex().gap_1().child(Label::new("Editor")).child(
401 v_flex()
402 .elevation_2(cx)
403 .child(EditorSettingsControls::new()),
404 ),
405 )
406}
407
408fn element_id_from_path(path: &[&'static str]) -> ElementId {
409 if path.len() == 0 {
410 panic!("Path length must not be zero");
411 } else if path.len() == 1 {
412 ElementId::Name(SharedString::new_static(path[0]))
413 } else {
414 ElementId::from((
415 ElementId::from(SharedString::new_static(path[path.len() - 2])),
416 SharedString::new_static(path[path.len() - 1]),
417 ))
418 }
419}
420
421fn render_item_single(
422 settings_value: SettingsValue<serde_json::Value>,
423 item: &SettingsUiItemSingle,
424 window: &mut Window,
425 cx: &mut App,
426) -> AnyElement {
427 match item {
428 SettingsUiItemSingle::Custom(_) => div()
429 .child(format!("Item: {}", settings_value.path.join(".")))
430 .into_any_element(),
431 SettingsUiItemSingle::SwitchField => {
432 render_any_item(settings_value, render_switch_field, window, cx)
433 }
434 SettingsUiItemSingle::NumericStepper(num_type) => {
435 render_any_numeric_stepper(settings_value, *num_type, window, cx)
436 }
437 SettingsUiItemSingle::ToggleGroup {
438 variants: values,
439 labels: titles,
440 } => render_toggle_button_group(settings_value, values, titles, window, cx),
441 SettingsUiItemSingle::DropDown { .. } => {
442 unimplemented!("This")
443 }
444 }
445}
446
447fn read_settings_value_from_path<'a>(
448 settings_contents: &'a serde_json::Value,
449 path: &[&'static str],
450) -> Option<&'a serde_json::Value> {
451 // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
452 let Some((key, remaining)) = path.split_first() else {
453 return Some(settings_contents);
454 };
455 let Some(value) = settings_contents.get(key) else {
456 return None;
457 };
458
459 read_settings_value_from_path(value, remaining)
460}
461
462fn downcast_any_item<T: serde::de::DeserializeOwned>(
463 settings_value: SettingsValue<serde_json::Value>,
464) -> SettingsValue<T> {
465 let value = settings_value
466 .value
467 .map(|value| serde_json::from_value::<T>(value).expect("value is not a T"));
468 // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
469 let default_value = serde_json::from_value::<T>(settings_value.default_value)
470 .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
471 .expect("default value is not an Option<T>");
472 let deserialized_setting_value = SettingsValue {
473 title: settings_value.title,
474 path: settings_value.path,
475 value,
476 default_value,
477 };
478 deserialized_setting_value
479}
480
481fn render_any_item<T: serde::de::DeserializeOwned>(
482 settings_value: SettingsValue<serde_json::Value>,
483 render_fn: impl Fn(SettingsValue<T>, &mut Window, &mut App) -> AnyElement + 'static,
484 window: &mut Window,
485 cx: &mut App,
486) -> AnyElement {
487 let deserialized_setting_value = downcast_any_item(settings_value);
488 render_fn(deserialized_setting_value, window, cx)
489}
490
491fn render_any_numeric_stepper(
492 settings_value: SettingsValue<serde_json::Value>,
493 num_type: NumType,
494 window: &mut Window,
495 cx: &mut App,
496) -> AnyElement {
497 match num_type {
498 NumType::U64 => render_numeric_stepper::<u64>(
499 downcast_any_item(settings_value),
500 u64::saturating_sub,
501 u64::saturating_add,
502 |n| {
503 serde_json::Number::try_from(n)
504 .context("Failed to convert u64 to serde_json::Number")
505 },
506 window,
507 cx,
508 ),
509 NumType::U32 => render_numeric_stepper::<u32>(
510 downcast_any_item(settings_value),
511 u32::saturating_sub,
512 u32::saturating_add,
513 |n| {
514 serde_json::Number::try_from(n)
515 .context("Failed to convert u32 to serde_json::Number")
516 },
517 window,
518 cx,
519 ),
520 NumType::F32 => render_numeric_stepper::<f32>(
521 downcast_any_item(settings_value),
522 |a, b| a - b,
523 |a, b| a + b,
524 |n| {
525 serde_json::Number::from_f64(n as f64)
526 .context("Failed to convert f32 to serde_json::Number")
527 },
528 window,
529 cx,
530 ),
531 }
532}
533
534fn render_numeric_stepper<
535 T: serde::de::DeserializeOwned + std::fmt::Display + Copy + From<u8> + 'static,
536>(
537 value: SettingsValue<T>,
538 saturating_sub: fn(T, T) -> T,
539 saturating_add: fn(T, T) -> T,
540 to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
541 _window: &mut Window,
542 _cx: &mut App,
543) -> AnyElement {
544 let id = element_id_from_path(&value.path);
545 let path = value.path.clone();
546 let num = *value.read();
547
548 NumericStepper::new(
549 id,
550 num.to_string(),
551 {
552 let path = value.path.clone();
553 move |_, _, cx| {
554 let Some(number) = to_serde_number(saturating_sub(num, 1.into())).ok() else {
555 return;
556 };
557 let new_value = serde_json::Value::Number(number);
558 SettingsValue::write_value(&path, new_value, cx);
559 }
560 },
561 move |_, _, cx| {
562 let Some(number) = to_serde_number(saturating_add(num, 1.into())).ok() else {
563 return;
564 };
565
566 let new_value = serde_json::Value::Number(number);
567
568 SettingsValue::write_value(&path, new_value, cx);
569 },
570 )
571 .style(ui::NumericStepperStyle::Outlined)
572 .into_any_element()
573}
574
575fn render_switch_field(
576 value: SettingsValue<bool>,
577 _window: &mut Window,
578 _cx: &mut App,
579) -> AnyElement {
580 let id = element_id_from_path(&value.path);
581 let path = value.path.clone();
582 SwitchField::new(
583 id,
584 SharedString::new_static(value.title),
585 None,
586 match value.read() {
587 true => ToggleState::Selected,
588 false => ToggleState::Unselected,
589 },
590 move |toggle_state, _, cx| {
591 let new_value = serde_json::Value::Bool(match toggle_state {
592 ToggleState::Indeterminate => {
593 return;
594 }
595 ToggleState::Selected => true,
596 ToggleState::Unselected => false,
597 });
598
599 SettingsValue::write_value(&path, new_value, cx);
600 },
601 )
602 .into_any_element()
603}
604
605fn render_toggle_button_group(
606 value: SettingsValue<serde_json::Value>,
607 variants: &'static [&'static str],
608 labels: &'static [&'static str],
609 _: &mut Window,
610 _: &mut App,
611) -> AnyElement {
612 let value = downcast_any_item::<String>(value);
613
614 fn make_toggle_group<const LEN: usize>(
615 group_name: &'static str,
616 value: SettingsValue<String>,
617 variants: &'static [&'static str],
618 labels: &'static [&'static str],
619 ) -> AnyElement {
620 let mut variants_array: [(&'static str, &'static str); LEN] = [("unused", "unused"); LEN];
621 for i in 0..LEN {
622 variants_array[i] = (variants[i], labels[i]);
623 }
624 let active_value = value.read();
625
626 let selected_idx = variants_array
627 .iter()
628 .enumerate()
629 .find_map(|(idx, (variant, _))| {
630 if variant == &active_value {
631 Some(idx)
632 } else {
633 None
634 }
635 });
636
637 let mut idx = 0;
638 ToggleButtonGroup::single_row(
639 group_name,
640 variants_array.map(|(variant, label)| {
641 let path = value.path.clone();
642 idx += 1;
643 ToggleButtonSimple::new(label, move |_, _, cx| {
644 SettingsValue::write_value(
645 &path,
646 serde_json::Value::String(variant.to_string()),
647 cx,
648 );
649 })
650 }),
651 )
652 .when_some(selected_idx, |this, ix| this.selected_index(ix))
653 .style(ui::ToggleButtonGroupStyle::Filled)
654 .into_any_element()
655 }
656
657 macro_rules! templ_toggl_with_const_param {
658 ($len:expr) => {
659 if variants.len() == $len {
660 return make_toggle_group::<$len>(value.title, value, variants, labels);
661 }
662 };
663 }
664 templ_toggl_with_const_param!(1);
665 templ_toggl_with_const_param!(2);
666 templ_toggl_with_const_param!(3);
667 templ_toggl_with_const_param!(4);
668 templ_toggl_with_const_param!(5);
669 templ_toggl_with_const_param!(6);
670 unreachable!("Too many variants");
671}
672
673fn settings_value_from_settings_and_path(
674 path: SmallVec<[&'static str; 1]>,
675 title: &'static str,
676 user_settings: &serde_json::Value,
677 default_settings: &serde_json::Value,
678) -> SettingsValue<serde_json::Value> {
679 let default_value = read_settings_value_from_path(default_settings, &path)
680 .with_context(|| format!("No default value for item at path {:?}", path.join(".")))
681 .expect("Default value set for item")
682 .clone();
683
684 let value = read_settings_value_from_path(user_settings, &path).cloned();
685 let settings_value = SettingsValue {
686 default_value,
687 value,
688 path: path.clone(),
689 // todo(settings_ui) is title required inside SettingsValue?
690 title,
691 };
692 return settings_value;
693}