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_6()
83                    .py_4()
84                    .border_b_1()
85                    .border_color(semantic::border(cx))
86                    .child(header),
87            )
88        })
89        .child(v_flex().p_6().gap_4().children(self.children))
90        .when_some(self.footer, |this, footer| {
91            this.child(
92                div()
93                    .px_6()
94                    .py_4()
95                    .border_t_1()
96                    .border_color(semantic::border(cx))
97                    .child(footer),
98            )
99        })
100    }
101}
102
103impl Component for Card {
104    fn scope() -> ComponentScope {
105        ComponentScope::Layout
106    }
107
108    fn description() -> Option<&'static str> {
109        Some("A surface container for grouping content, with optional header and footer.")
110    }
111
112    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
113        Some(
114            h_flex()
115                .gap_4()
116                .child(
117                    Card::new()
118                        .header(Label::new("Bordered"))
119                        .child(Label::new("Body content"))
120                        .footer(Label::new("Footer")),
121                )
122                .child(
123                    Card::new()
124                        .variant(CardVariant::Elevated)
125                        .header(Label::new("Elevated"))
126                        .child(Label::new("With shadow")),
127                )
128                .child(
129                    Card::new()
130                        .variant(CardVariant::Flat)
131                        .child(Label::new("Flat card")),
132                )
133                .into_any_element(),
134        )
135    }
136}