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 el = el.on_click(|_, _, _| {});
101
102 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 el = el.child(div().p_4().child(self.body));
123
124 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}