settings_ui: Implement sub pages (#39484)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/settings_ui/src/settings_ui.rs | 203 ++++++++++++++++++++++++++---
1 file changed, 182 insertions(+), 21 deletions(-)

Detailed changes

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> {
                 // }),
             ],
         },
+        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<NavBarEntry>,
     list_handle: UniformListScrollHandle,
     search_matches: Vec<Vec<bool>>,
+    /// 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<SubPage>,
+}
+
+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<SettingsWindow>,
     ) -> 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<dyn Fn(&mut SettingsWindow, &mut Window, &mut App) -> 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<SettingsWindow>) -> Stateful<Div> {
-        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<SettingsWindow>) -> 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<SettingsWindow>,
+    ) {
+        self.sub_page_stack.push(SubPage {
+            link: sub_page_link,
+            section_header,
+        });
+        cx.notify();
+    }
+
+    fn pop_sub_page(&mut self, cx: &mut Context<SettingsWindow>) {
+        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();