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::{
 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}