settings_ui.rs

  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}