Skip to main content

ui/components/
card.rs

1use gpui::AnyElement;
2use smallvec::SmallVec;
3
4use crate::prelude::*;
5
6/// Visual style of a [`Card`].
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum CardVariant {
9    /// Surface with a 1px border and no shadow.
10    #[default]
11    Bordered,
12    /// Surface with a medium drop shadow and no border.
13    Elevated,
14    /// Plain surface, no border or shadow.
15    Flat,
16}
17
18/// A surface container for grouping related content, with optional header and
19/// footer regions. Neutrals are theme-driven (dark/light aware).
20#[derive(IntoElement, RegisterComponent)]
21pub struct Card {
22    variant: CardVariant,
23    header: Option<AnyElement>,
24    footer: Option<AnyElement>,
25    children: SmallVec<[AnyElement; 2]>,
26}
27
28impl Card {
29    pub fn new() -> Self {
30        Self {
31            variant: CardVariant::default(),
32            header: None,
33            footer: None,
34            children: SmallVec::new(),
35        }
36    }
37
38    pub fn variant(mut self, variant: CardVariant) -> Self {
39        self.variant = variant;
40        self
41    }
42
43    pub fn header(mut self, header: impl IntoElement) -> Self {
44        self.header = Some(header.into_any_element());
45        self
46    }
47
48    pub fn footer(mut self, footer: impl IntoElement) -> Self {
49        self.footer = Some(footer.into_any_element());
50        self
51    }
52}
53
54impl Default for Card {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl ParentElement for Card {
61    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
62        self.children.extend(elements);
63    }
64}
65
66impl RenderOnce for Card {
67    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
68        let mut base = v_flex()
69            .bg(semantic::surface(cx))
70            .rounded_lg()
71            .text_color(semantic::text(cx));
72
73        base = match self.variant {
74            CardVariant::Bordered => base.border_1().border_color(semantic::border(cx)),
75            CardVariant::Elevated => base.shadow_level(Shadow::Md),
76            CardVariant::Flat => base,
77        };
78
79        base.when_some(self.header, |this, header| {
80            this.child(
81                div()
82                    .px(DynamicSpacing::Base24.px(cx))
83                    .py(DynamicSpacing::Base16.px(cx))
84                    .border_b_1()
85                    .border_color(semantic::border(cx))
86                    .child(header),
87            )
88        })
89        .child(
90            v_flex()
91                .p(DynamicSpacing::Base24.px(cx))
92                .gap(DynamicSpacing::Base16.rems(cx))
93                .children(self.children),
94        )
95        .when_some(self.footer, |this, footer| {
96            this.child(
97                div()
98                    .px(DynamicSpacing::Base24.px(cx))
99                    .py(DynamicSpacing::Base16.px(cx))
100                    .border_t_1()
101                    .border_color(semantic::border(cx))
102                    .child(footer),
103            )
104        })
105    }
106}
107
108impl Component for Card {
109    fn scope() -> ComponentScope {
110        ComponentScope::Layout
111    }
112
113    fn description() -> Option<&'static str> {
114        Some("A surface container for grouping content, with optional header and footer.")
115    }
116
117    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
118        Some(
119            h_flex()
120                .gap(DynamicSpacing::Base16.rems(cx))
121                .child(
122                    Card::new()
123                        .header(Label::new("Bordered"))
124                        .child(Label::new("Body content"))
125                        .footer(Label::new("Footer")),
126                )
127                .child(
128                    Card::new()
129                        .variant(CardVariant::Elevated)
130                        .header(Label::new("Elevated"))
131                        .child(Label::new("With shadow")),
132                )
133                .child(
134                    Card::new()
135                        .variant(CardVariant::Flat)
136                        .child(Label::new("Flat card")),
137                )
138                .into_any_element(),
139        )
140    }
141}