settings_ui: Implement sub pages (#39484)

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle
2025-10-03 19:59:46 +00:00
committed by GitHub
parent 6707ff3b50
commit f0f0beb42f
+182 -21
View File
@@ -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();