use gpui::{ AnyElement, App, Div, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, StyleRefinement, Styled, Window, div, prelude::*, }; use smallvec::SmallVec; use crate::theme::{ActiveTheme, ControlSize, Radius, TextSize}; macro_rules! container_parts { ($name:ident) => { impl Styled for $name { fn style(&mut self) -> &mut StyleRefinement { self.base.style() } } impl ParentElement for $name { fn extend(&mut self, elements: impl IntoIterator) { self.body.extend(elements); } } }; } fn heading( title: SharedString, description: Option, size: TextSize, cx: &App, ) -> impl IntoElement + use<> { let theme = cx.theme(); div() .flex() .flex_col() .gap_0p5() .min_w_0() .child( div() .text_size(theme.text_size(size)) .font_weight(FontWeight::SEMIBOLD) .text_color(theme.colors.fg) .child(title), ) .when_some(description, |block, text| { block.child( div() .text_size(theme.text_size(TextSize::Sm)) .text_color(theme.colors.fg_muted) .child(text), ) }) } /// Title row for a `Card`: title, note, one trailing action. #[derive(IntoElement)] pub struct CardHeader { title: SharedString, description: Option, action: Option, } impl CardHeader { pub fn new(title: impl Into) -> Self { Self { title: title.into(), description: None, action: None, } } pub fn description(mut self, text: impl Into) -> Self { self.description = Some(text.into()); self } pub fn action(mut self, action: impl IntoElement) -> Self { self.action = Some(action.into_any_element()); self } } impl RenderOnce for CardHeader { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { div() .flex() .items_start() .justify_between() .gap_4() .child(heading(self.title, self.description, TextSize::Md, cx)) .children(self.action) } } /// Flat surface: header, body, footer. Hairline, no shadow. #[derive(IntoElement)] pub struct Card { base: Div, header: Option, footer: Option, body: SmallVec<[AnyElement; 2]>, } impl Card { pub fn new() -> Self { Self { base: div(), header: None, footer: None, body: SmallVec::new(), } } pub fn header(mut self, header: CardHeader) -> Self { self.header = Some(header); self } pub fn footer(mut self, footer: impl IntoElement) -> Self { self.footer = Some(footer.into_any_element()); self } } impl Default for Card { fn default() -> Self { Self::new() } } container_parts!(Card); impl RenderOnce for Card { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); self.base .flex() .flex_col() .rounded(theme.radius(Radius::Lg)) .border_1() .border_color(theme.colors.border) .bg(theme.colors.surface) .when_some(self.header, |card, header| { card.child(div().px_5().pt_5().child(header)) }) .child(div().p_5().flex().flex_col().gap_3().children(self.body)) .when_some(self.footer, |card, footer| { card.child( div() .flex() .justify_end() .gap_2() .px_5() .py_3() .border_t_1() .border_color(theme.colors.border) .child(footer), ) }) } } /// App region with a header bar: title and actions. #[derive(IntoElement)] pub struct Panel { base: Div, title: SharedString, actions: SmallVec<[AnyElement; 2]>, body: SmallVec<[AnyElement; 2]>, } impl Panel { pub fn new(title: impl Into) -> Self { Self { base: div(), title: title.into(), actions: SmallVec::new(), body: SmallVec::new(), } } pub fn action(mut self, action: impl IntoElement) -> Self { self.actions.push(action.into_any_element()); self } } container_parts!(Panel); impl RenderOnce for Panel { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); self.base .flex() .flex_col() .overflow_hidden() .rounded(theme.radius(Radius::Md)) .border_1() .border_color(theme.colors.border) .bg(theme.colors.surface) .child( div() .flex() .items_center() .justify_between() .h(theme.control_height(ControlSize::Lg)) .pl_3() .pr_1() .border_b_1() .border_color(theme.colors.border) .child( div() .text_size(theme.text_size(TextSize::Sm)) .font_weight(FontWeight::SEMIBOLD) .text_color(theme.colors.fg_muted) .child(self.title), ) .child(div().flex().items_center().gap_0p5().children(self.actions)), ) .child(div().flex_1().children(self.body)) } } /// Titled block of a page, with an optional note and action. #[derive(IntoElement)] pub struct Section { base: Div, title: SharedString, description: Option, action: Option, body: SmallVec<[AnyElement; 2]>, } impl Section { pub fn new(title: impl Into) -> Self { Self { base: div(), title: title.into(), description: None, action: None, body: SmallVec::new(), } } pub fn description(mut self, text: impl Into) -> Self { self.description = Some(text.into()); self } pub fn action(mut self, action: impl IntoElement) -> Self { self.action = Some(action.into_any_element()); self } } container_parts!(Section); impl RenderOnce for Section { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { self.base .flex() .flex_col() .gap_4() .child( div() .flex() .items_end() .justify_between() .gap_4() .child(heading(self.title, self.description, TextSize::Lg, cx)) .children(self.action), ) .children(self.body) } } /// Named group of fields inside a hairline. #[derive(IntoElement)] pub struct Fieldset { base: Div, legend: SharedString, body: SmallVec<[AnyElement; 2]>, } impl Fieldset { pub fn new(legend: impl Into) -> Self { Self { base: div(), legend: legend.into(), body: SmallVec::new(), } } } container_parts!(Fieldset); impl RenderOnce for Fieldset { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); self.base .flex() .flex_col() .gap_2() .child( div() .text_size(theme.text_size(TextSize::Xs)) .font_weight(FontWeight::SEMIBOLD) .text_color(theme.colors.fg_muted) .child(self.legend), ) .child( div() .flex() .flex_col() .gap_3() .p_4() .rounded(theme.radius(Radius::Lg)) .border_1() .border_color(theme.colors.border) .children(self.body), ) } } /// Bordered region, no fill. #[derive(IntoElement)] pub struct Frame { base: Div, body: SmallVec<[AnyElement; 2]>, } impl Frame { pub fn new() -> Self { Self { base: div(), body: SmallVec::new(), } } } impl Default for Frame { fn default() -> Self { Self::new() } } container_parts!(Frame); impl RenderOnce for Frame { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); self.base .rounded(theme.radius(Radius::Lg)) .border_1() .border_color(theme.colors.border_strong) .p_4() .children(self.body) } } /// Recessed region on the sunken tone. #[derive(IntoElement)] pub struct Well { base: Div, body: SmallVec<[AnyElement; 2]>, } impl Well { pub fn new() -> Self { Self { base: div(), body: SmallVec::new(), } } } impl Default for Well { fn default() -> Self { Self::new() } } container_parts!(Well); impl RenderOnce for Well { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); self.base .rounded(theme.radius(Radius::Lg)) .bg(theme.colors.sunken) .border_1() .border_color(theme.colors.border) .p_4() .children(self.body) } }