Files
Ely-GPUI-Components/src/settings/section.rs
T
ZacharyZhang-NY 195f9964a0 Settings: controls give way, notices fold, and every edit needs its owner
A settings row's control slot shrinks to the box, so a control's text
wraps and a palette's row folds; the font size's sample wraps in a plain
block. A notice's boxes carry their channel's name and fold under the
kind's name when narrow. canvas::panel::editing takes the owner's
handler itself: the design panels, the structure editor, the index
manager and the privacy, notice and startup settings take theirs at
render and fail loud without one. Apply fails loud if pressed while the
proxy does not read. Tests cover Apply resting on a bad port, the way
back resting at the default size and a box pressed off.
2026-09-27 12:53:08 -04:00

226 lines
6.9 KiB
Rust

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<dyn Fn(&SharedString, &mut Window, &mut App)>;
/// 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<AnyElement>,
on_select: Option<OnKey>,
}
impl SettingsLayout {
/// `sections` are each a key, a name and an icon; `selected` names the one open.
pub fn new(
id: impl Into<ElementId>,
sections: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<SharedString>, IconName)>,
selected: impl Into<SharedString>,
) -> 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<SharedString>,
rows: Vec<AnyElement>,
}
impl SettingsSection {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
description: None,
rows: Vec::new(),
}
}
pub fn description(mut self, text: impl Into<SharedString>) -> 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<SharedString>,
control: Option<AnyElement>,
}
impl SettingsRow {
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
description: None,
control: None,
}
}
pub fn description(mut self, text: impl Into<SharedString>) -> 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)),
)
}
}