use std::rc::Rc; use gpui::{ AnyElement, App, ElementId, FontWeight, InteractiveElement as _, IntoElement, ParentElement, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled, Window, div, prelude::FluentBuilder as _, }; 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: OnKey, } 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, on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static, ) -> Self { let sections: Vec<_> = sections .into_iter() .map(|(key, name, icon)| (key.into(), name.into(), icon)) .collect(); let selected = selected.into(); if !sections.iter().any(|(key, _, _)| *key == selected) { log::error!("settings layout: no section {selected}; none open"); } Self { id: id.into(), sections, selected, page: None, on_select: Rc::new(on_select), } } /// What the open section shows. pub fn page(mut self, page: impl IntoElement) -> Self { self.page = Some(page.into_any_element()); self } } impl RenderOnce for SettingsLayout { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let on_select = self.on_select; 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(); // The row names its control for assistive tech. let id = SharedString::from(format!("settings-row-{}", self.title)); div() .id(id) .role(Role::Group) .aria_label(self.title.clone()) .when_some(self.description.clone(), |row, text| { row.aria_description(text) }) .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)), ) } }