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