diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index a62f8ef7bf2a59cb1cb0b9bc9e047a313ada85a5..370ea20b9a0e67f4d67dd494df188ff2b74fa027 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -6,7 +6,7 @@ use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use fuzzy::StringMatchCandidate; use gpui::{ App, AppContext as _, Context, Div, Entity, Global, IntoElement, ReadGlobal as _, Render, - ScrollHandle, Stateful, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowHandle, + ScrollHandle, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowHandle, WindowOptions, actions, div, point, px, size, uniform_list, }; use project::WorktreeId; @@ -1358,6 +1358,39 @@ fn user_settings_data() -> Vec { // }), ], }, + SettingsPage { + title: "Languages & Frameworks", + expanded: false, + items: vec![ + SettingsPageItem::SectionHeader("General"), + SettingsPageItem::SettingItem(SettingItem { + title: "Enable Language Server", + description: "Whether to use language servers to provide code intelligence", + field: Box::new(SettingField { + pick: |settings_content| { + &settings_content + .project + .all_languages + .defaults + .enable_language_server + }, + pick_mut: |settings_content| { + &mut settings_content + .project + .all_languages + .defaults + .enable_language_server + }, + }), + metadata: None, + }), + SettingsPageItem::SectionHeader("Languages"), + SettingsPageItem::SubPageLink(SubPageLink { + title: "JSON", + render: Rc::new(|_, _, _| "A settings page!".into_any_element()), + }), + ], + }, SettingsPage { title: "Workbench & Window", expanded: false, @@ -2742,6 +2775,15 @@ pub struct SettingsWindow { navbar_entries: Vec, list_handle: UniformListScrollHandle, search_matches: Vec>, + /// The current sub page path that is selected. + /// If this is empty the selected page is rendered, + /// otherwise the last sub page gets rendered. + sub_page_stack: Vec, +} + +struct SubPage { + link: SubPageLink, + section_header: &'static str, } #[derive(PartialEq, Debug)] @@ -2761,6 +2803,7 @@ struct SettingsPage { enum SettingsPageItem { SectionHeader(&'static str), SettingItem(SettingItem), + SubPageLink(SubPageLink), } impl std::fmt::Debug for SettingsPageItem { @@ -2770,6 +2813,9 @@ impl std::fmt::Debug for SettingsPageItem { SettingsPageItem::SettingItem(setting_item) => { write!(f, "SettingItem({})", setting_item.title) } + SettingsPageItem::SubPageLink(sub_page_link) => { + write!(f, "SubPageLink({})", sub_page_link.title) + } } } } @@ -2778,9 +2824,10 @@ impl SettingsPageItem { fn render( &self, file: SettingsUiFile, + section_header: &'static str, is_last: bool, window: &mut Window, - cx: &mut App, + cx: &mut Context, ) -> AnyElement { match self { SettingsPageItem::SectionHeader(header) => v_flex() @@ -2850,6 +2897,36 @@ impl SettingsPageItem { )) .into_any_element() } + SettingsPageItem::SubPageLink(sub_page_link) => h_flex() + .id(sub_page_link.title) + .w_full() + .gap_2() + .flex_wrap() + .justify_between() + .when(!is_last, |this| { + this.pb_4() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + }) + .child( + v_flex().max_w_1_2().flex_shrink().child( + Label::new(SharedString::new_static(sub_page_link.title)) + .size(LabelSize::Default), + ), + ) + .child( + Button::new(("sub-page".into(), sub_page_link.title), "Configure") + .icon(Some(IconName::ChevronRight)) + .icon_position(Some(IconPosition::End)) + .style(ButtonStyle::Outlined), + ) + .on_click({ + let sub_page_link = sub_page_link.clone(); + cx.listener(move |this, _, _, cx| { + this.push_sub_page(sub_page_link.clone(), section_header, cx) + }) + }) + .into_any_element(), } } } @@ -2873,6 +2950,18 @@ impl PartialEq for SettingItem { } } +#[derive(Clone)] +struct SubPageLink { + title: &'static str, + render: Rc AnyElement>, +} + +impl PartialEq for SubPageLink { + fn eq(&self, other: &Self) -> bool { + self.title == other.title + } +} + #[allow(unused)] #[derive(Clone, PartialEq)] enum SettingsUiFile { @@ -2953,6 +3042,7 @@ impl SettingsWindow { search_bar, search_task: None, search_matches: vec![], + sub_page_stack: vec![], }; this.fetch_files(cx); @@ -3063,6 +3153,9 @@ impl SettingsWindow { candidates.push(StringMatchCandidate::new(key_index, header)); header_index = item_index; } + SettingsPageItem::SubPageLink(sub_page_link) => { + candidates.push(StringMatchCandidate::new(key_index, sub_page_link.title)); + } } key_lut.push(ItemKey { page_index, @@ -3239,23 +3332,81 @@ impl SettingsWindow { }) } - fn render_page(&self, window: &mut Window, cx: &mut Context) -> Stateful
{ - let items: Vec<_> = self.page_items().collect(); - let items_len = items.len(); + fn render_sub_page_breadcrumbs(&self) -> impl IntoElement { + let mut items = vec![]; + items.push(self.current_page().title); + items.extend( + self.sub_page_stack + .iter() + .flat_map(|page| [page.section_header, page.link.title]), + ); - v_flex() + let last = items.pop().unwrap(); + h_flex() + .gap_1() + .children( + items + .into_iter() + .flat_map(|item| [item, "/"]) + .map(|item| Label::new(item).color(Color::Muted)), + ) + .child(Label::new(last)) + } + + fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> Div { + let mut page = v_flex() + .w_full() + .pt_4() + .px_6() + .gap_4() + .bg(cx.theme().colors().editor_background); + let mut page_content = v_flex() .id("settings-ui-page") .gap_4() - .children(items.into_iter().enumerate().map(|(index, item)| { - let is_last = index == items_len - 1; - item.render(self.current_file.clone(), is_last, window, cx) - })) .overflow_y_scroll() .track_scroll( window .use_state(cx, |_, _| ScrollHandle::default()) .read(cx), - ) + ); + if self.sub_page_stack.len() == 0 { + page = page.child(self.render_files(window, cx)); + + let items: Vec<_> = self.page_items().collect(); + let items_len = items.len(); + let mut section_header = None; + + page_content = + page_content.children(items.into_iter().enumerate().map(|(index, item)| { + let is_last = index == items_len - 1; + if let SettingsPageItem::SectionHeader(header) = item { + section_header = Some(*header); + } + item.render( + self.current_file.clone(), + section_header.expect("All items rendered after a section header"), + is_last, + window, + cx, + ) + })) + } else { + page = page.child( + h_flex() + .gap_2() + .child(IconButton::new("back-btn", IconName::ChevronLeft).on_click( + cx.listener(|this, _, _, cx| { + this.pop_sub_page(cx); + }), + )) + .child(self.render_sub_page_breadcrumbs()), + ); + + let active_page_render_fn = self.sub_page_stack.last().unwrap().link.render.clone(); + page_content = page_content.child((active_page_render_fn)(self, window, cx)); + } + + return page.child(page_content); } fn current_page_index(&self) -> usize { @@ -3282,6 +3433,24 @@ impl SettingsWindow { fn is_navbar_entry_selected(&self, ix: usize) -> bool { ix == self.navbar_entry } + + fn push_sub_page( + &mut self, + sub_page_link: SubPageLink, + section_header: &'static str, + cx: &mut Context, + ) { + self.sub_page_stack.push(SubPage { + link: sub_page_link, + section_header, + }); + cx.notify(); + } + + fn pop_sub_page(&mut self, cx: &mut Context) { + self.sub_page_stack.pop(); + cx.notify(); + } } impl Render for SettingsWindow { @@ -3296,16 +3465,7 @@ impl Render for SettingsWindow { .bg(cx.theme().colors().background) .text_color(cx.theme().colors().text) .child(self.render_nav(window, cx)) - .child( - v_flex() - .w_full() - .pt_4() - .px_6() - .gap_4() - .bg(cx.theme().colors().editor_background) - .child(self.render_files(window, cx)) - .child(self.render_page(window, cx)), - ) + .child(self.render_page(window, cx)) } } @@ -3637,6 +3797,7 @@ mod test { list_handle: UniformListScrollHandle::default(), search_matches, search_task: None, + sub_page_stack: vec![], }; settings_window.build_navbar();