use std::rc::Rc; use gpui::{ AnyElement, App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, }; use crate::{ lists::{ListItem, SelectableList}, primitives::{Icon, IconName}, theme::{ActiveTheme, IconSize, TextSize}, }; type OnKey = Rc; /// A settings page: its sections listed down the side with the one open beside them, the list going above the page in a narrow box. Up and Down walk the list and Enter opens a section. #[derive(IntoElement)] pub struct SettingsLayout { id: ElementId, sections: Vec<(SharedString, SharedString, IconName)>, selected: SharedString, page: Option, on_select: Option, } impl SettingsLayout { /// `sections` are each a key, a name and an icon; `selected` names the one open. pub fn new( id: impl Into, sections: impl IntoIterator, impl Into, IconName)>, selected: impl Into, ) -> Self { let sections: Vec<_> = sections .into_iter() .map(|(key, name, icon)| (key.into(), name.into(), icon)) .collect(); let selected = selected.into(); assert!( sections.iter().any(|(key, _, _)| *key == selected), "settings layout: no section {selected}" ); Self { id: id.into(), sections, selected, page: None, on_select: None, } } /// What the open section shows. pub fn page(mut self, page: impl IntoElement) -> Self { self.page = Some(page.into_any_element()); self } pub fn on_select( mut self, handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static, ) -> Self { self.on_select = Some(Rc::new(handler)); self } } impl RenderOnce for SettingsLayout { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let on_select = self .on_select .unwrap_or_else(|| panic!("settings layout {:?} has no on_select", self.id)); let theme = cx.theme(); let muted = theme.colors.fg_muted; let list = self.sections.iter().fold( SelectableList::new((self.id.clone(), "sections")).selected([self.selected.clone()]), |list, (key, name, icon)| { list.row( key.clone(), ListItem::new((self.id.clone(), format!("section-{key}")), name.clone()) .leading(Icon::new(*icon).size(IconSize::Sm).color(muted)), ) }, ); let (picked, opened) = (on_select.clone(), on_select); let list = list .on_change(move |keys, window, cx| { if let Some(key) = keys.first() { log::info!("settings: open {key}"); picked(key, window, cx); } }) .on_activate(move |key, window, cx| { opened(key, window, cx); }); div() .flex() .flex_wrap() .items_start() .gap_6() .child( div() .flex_1() .min_w(theme.label_width()) .max_w(theme.label_width() * 1.25) .child(list), ) .child( div() .flex_1() .min_w(theme.label_width()) .children(self.page), ) } } /// A titled group of settings rows, with a line of context under the title. #[derive(IntoElement)] pub struct SettingsSection { title: SharedString, description: Option, rows: Vec, } impl SettingsSection { pub fn new(title: impl Into) -> Self { Self { title: title.into(), description: None, rows: Vec::new(), } } pub fn description(mut self, text: impl Into) -> Self { self.description = Some(text.into()); self } pub fn row(mut self, row: impl IntoElement) -> Self { self.rows.push(row.into_any_element()); self } } impl RenderOnce for SettingsSection { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); div() .flex() .flex_col() .gap_1() .child(div().font_weight(FontWeight::SEMIBOLD).child(self.title)) .children(self.description.map(|text| { div() .text_size(theme.text_size(TextSize::Sm)) .text_color(theme.colors.fg_muted) .child(text) })) .child(div().flex().flex_col().pt_2().children(self.rows)) } } /// One setting: its name and a line on what it does, and its control at the end of the line, or below them where the line is too narrow. #[derive(IntoElement)] pub struct SettingsRow { title: SharedString, description: Option, control: Option, } impl SettingsRow { pub fn new(title: impl Into) -> Self { Self { title: title.into(), description: None, control: None, } } pub fn description(mut self, text: impl Into) -> Self { self.description = Some(text.into()); self } pub fn control(mut self, control: impl IntoElement) -> Self { self.control = Some(control.into_any_element()); self } } impl RenderOnce for SettingsRow { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); div() .flex() .flex_wrap() .items_center() .justify_between() .gap_x_4() .gap_y_2() .py_3() .border_b_1() .border_color(theme.colors.border) .child( div() .flex_1() .min_w(theme.label_width()) .flex() .flex_col() .child( div() .text_size(theme.text_size(TextSize::Sm)) .child(self.title), ) .children(self.description.map(|text| { div() .text_size(theme.text_size(TextSize::Xs)) .text_color(theme.colors.fg_muted) .child(text) })), ) .children( self.control .map(|control| div().flex_none().min_w_0().max_w_full().child(control)), ) } }