use crate::{
IconName, Sizable, Size, StyledExt,
group_box::GroupBoxVariant,
input::{Input, InputState},
resizable::{h_resizable, resizable_panel},
setting::{SettingGroup, SettingPage},
sidebar::{Sidebar, SidebarMenu, SidebarMenuItem},
};
use gpui::{
App, AppContext as _, Axis, ElementId, Entity, IntoElement, ParentElement as _, Pixels,
RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _, px, relative,
};
use rust_i18n::t;
#[derive(IntoElement)]
pub struct Settings {
id: ElementId,
pages: Vec<SettingPage>,
group_variant: GroupBoxVariant,
size: Size,
sidebar_width: Pixels,
sidebar_style: StyleRefinement,
}
impl Settings {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
pages: vec![],
group_variant: GroupBoxVariant::default(),
size: Size::default(),
sidebar_width: px(250.0),
sidebar_style: StyleRefinement::default(),
}
}
pub fn sidebar_width(mut self, width: impl Into<Pixels>) -> Self {
self.sidebar_width = width.into();
self
}
pub fn page(mut self, page: SettingPage) -> Self {
self.pages.push(page);
self
}
pub fn pages(mut self, pages: impl IntoIterator<Item = SettingPage>) -> Self {
self.pages.extend(pages);
self
}
pub fn with_group_variant(mut self, variant: GroupBoxVariant) -> Self {
self.group_variant = variant;
self
}
pub fn sidebar_style(mut self, style: &StyleRefinement) -> Self {
self.sidebar_style = style.clone();
self
}
fn filtered_pages(&self, query: &str) -> Vec<SettingPage> {
self.pages
.iter()
.filter_map(|page| {
let filtered_groups: Vec<SettingGroup> = page
.groups
.iter()
.filter_map(|group| {
let mut group = group.clone();
group.items = group
.items
.iter()
.filter(|item| item.is_match(&query))
.cloned()
.collect();
if group.items.is_empty() {
None
} else {
Some(group)
}
})
.collect();
let mut page = page.clone();
page.groups = filtered_groups;
if page.groups.is_empty() {
None
} else {
Some(page)
}
})
.collect()
}
fn render_active_page(
&self,
state: &Entity<SettingsState>,
pages: &Vec<SettingPage>,
options: &RenderOptions,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let selected_index = state.read(cx).selected_index;
for (ix, page) in pages.into_iter().enumerate() {
if selected_index.page_ix == ix {
return page
.render(ix, state, &options, window, cx)
.into_any_element();
}
}
return div().into_any_element();
}
fn render_sidebar(
&self,
state: &Entity<SettingsState>,
pages: &Vec<SettingPage>,
_: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let selected_index = state.read(cx).selected_index;
let search_input = state.read(cx).search_input.clone();
Sidebar::left()
.w(relative(1.))
.border_0()
.refine_style(&self.sidebar_style)
.collapsed(false)
.header(
div()
.w_full()
.child(Input::new(&search_input).prefix(IconName::Search)),
)
.child(
SidebarMenu::new().children(pages.iter().enumerate().map(|(page_ix, page)| {
let is_page_active =
selected_index.page_ix == page_ix && selected_index.group_ix.is_none();
SidebarMenuItem::new(page.title.clone())
.default_open(page.default_open)
.active(is_page_active)
.on_click({
let state = state.clone();
move |_, _, cx| {
state.update(cx, |state, cx| {
state.selected_index = SelectIndex {
page_ix,
..Default::default()
};
cx.notify();
})
}
})
.when(page.groups.len() > 1, |this| {
this.children(
page.groups
.iter()
.filter(|g| g.title.is_some())
.enumerate()
.map(|(group_ix, group)| {
let is_active = selected_index.page_ix == page_ix
&& selected_index.group_ix == Some(group_ix);
let title = group.title.clone().unwrap_or_default();
SidebarMenuItem::new(title).active(is_active).on_click({
let state = state.clone();
move |_, _, cx| {
state.update(cx, |state, cx| {
state.selected_index = SelectIndex {
page_ix,
group_ix: Some(group_ix),
};
state.deferred_scroll_group_ix = Some(group_ix);
cx.notify();
})
}
})
}),
)
})
})),
)
}
}
impl Sizable for Settings {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
pub(super) struct SettingsState {
pub(super) selected_index: SelectIndex,
pub(super) deferred_scroll_group_ix: Option<usize>,
pub(super) search_input: Entity<InputState>,
}
#[derive(Clone, Copy)]
pub struct RenderOptions {
pub page_ix: usize,
pub group_ix: usize,
pub item_ix: usize,
pub size: Size,
pub group_variant: GroupBoxVariant,
pub layout: Axis,
}
#[derive(Clone, Copy, Default)]
pub(super) struct SelectIndex {
page_ix: usize,
group_ix: Option<usize>,
}
impl RenderOnce for Settings {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state(self.id.clone(), cx, |window, cx| {
let search_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(t!("Settings.search_placeholder"))
.default_value("")
});
SettingsState {
search_input,
selected_index: SelectIndex::default(),
deferred_scroll_group_ix: None,
}
});
let query = state.read(cx).search_input.read(cx).value();
let filtered_pages = self.filtered_pages(&query);
let options = RenderOptions {
page_ix: 0,
group_ix: 0,
item_ix: 0,
size: self.size,
group_variant: self.group_variant,
layout: Axis::Horizontal,
};
h_resizable(self.id.clone())
.child(
resizable_panel()
.size(self.sidebar_width)
.child(self.render_sidebar(&state, &filtered_pages, window, cx)),
)
.child(resizable_panel().child(self.render_active_page(
&state,
&filtered_pages,
&options,
window,
cx,
)))
}
}