Skip to main content

liora_components/
card.rs

1use gpui::{
2    AnyElement, App, Component, IntoElement, Pixels, RenderOnce, SharedString, Window, div,
3    prelude::*, px,
4};
5use liora_core::{Config, stable_unique_id};
6
7pub struct Card {
8    title: Option<SharedString>,
9    header: Option<AnyElement>,
10    footer: Option<AnyElement>,
11    body: AnyElement,
12    hoverable: bool,
13    shadow: bool,
14    width: Option<Pixels>,
15    shrink: bool,
16}
17
18impl Card {
19    pub fn new(body: impl IntoElement) -> Self {
20        Self {
21            title: None,
22            header: None,
23            footer: None,
24            body: body.into_any_element(),
25            hoverable: false,
26            shadow: true,
27            width: None,
28            shrink: true,
29        }
30    }
31
32    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
33        self.title = Some(title.into());
34        self
35    }
36
37    pub fn header(mut self, header: impl IntoElement) -> Self {
38        self.header = Some(header.into_any_element());
39        self
40    }
41
42    pub fn footer(mut self, footer: impl IntoElement) -> Self {
43        self.footer = Some(footer.into_any_element());
44        self
45    }
46
47    pub fn hoverable(mut self) -> Self {
48        self.hoverable = true;
49        self
50    }
51
52    pub fn no_shadow(mut self) -> Self {
53        self.shadow = false;
54        self
55    }
56
57    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
58        self.width = Some(width.into());
59        self
60    }
61
62    pub fn width_md(self) -> Self {
63        self.width(px(300.0))
64    }
65
66    pub fn width_lg(self) -> Self {
67        self.width(px(400.0))
68    }
69
70    pub fn no_shrink(mut self) -> Self {
71        self.shrink = false;
72        self
73    }
74}
75
76impl RenderOnce for Card {
77    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
78        let theme = cx.global::<Config>().theme.clone();
79        let id = stable_unique_id("card", "card", _window, cx);
80
81        let mut el = div()
82            .id(id)
83            .bg(theme.neutral.card)
84            .border_1()
85            .border_color(theme.neutral.border)
86            .rounded(px(theme.radius.md))
87            .overflow_hidden()
88            .when(!self.shrink, |s| s.flex_none())
89            .when_some(self.width, |s, width| s.w(width));
90
91        if self.shadow {
92            el = el.shadow_md();
93        }
94
95        if self.hoverable {
96            el = el.hover(|s| s.shadow_xl().border_color(theme.primary.base));
97        }
98
99        // We use on_click to ensure the ID-based hover and other interactions are registered correctly
100        el = el.on_click(|_, _, _| {});
101
102        // Header
103        if let Some(title) = self.title {
104            el = el.child(
105                div()
106                    .p_4()
107                    .border_b_1()
108                    .border_color(theme.neutral.border)
109                    .child(div().font_weight(gpui::FontWeight::BOLD).child(title)),
110            );
111        } else if let Some(header) = self.header {
112            el = el.child(
113                div()
114                    .p_4()
115                    .border_b_1()
116                    .border_color(theme.neutral.border)
117                    .child(header),
118            );
119        }
120
121        // Body
122        el = el.child(div().p_4().child(self.body));
123
124        // Footer
125        if let Some(footer) = self.footer {
126            el = el.child(
127                div()
128                    .p_4()
129                    .border_t_1()
130                    .border_color(theme.neutral.border)
131                    .bg(theme.neutral.hover)
132                    .child(footer),
133            );
134        }
135
136        el
137    }
138}
139
140impl IntoElement for Card {
141    type Element = Component<Self>;
142    fn into_element(self) -> Self::Element {
143        Component::new(self)
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn card_width_helpers_set_demo_widths() {
153        assert_eq!(Card::new("body").width_md().width, Some(px(300.0)));
154        assert_eq!(Card::new("body").width_lg().width, Some(px(400.0)));
155    }
156
157    #[test]
158    fn card_no_shrink_tracks_scroll_container_usage() {
159        assert!(!Card::new("body").no_shrink().shrink);
160    }
161}