settings_ui: Implement sub pages (#39484)
Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user