Skip to main content

ui/components/
stats_card.rs

1use gpui::{AnyElement, FontWeight};
2
3use crate::prelude::*;
4
5// Note: Tailwind's "Grid List" category (card-based grid) is covered by
6// composing existing `Card`s in a caller-owned `h_flex()`/wrap layout — no
7// dedicated component is needed for it.
8
9/// Direction of a [`StatsCard`] trend indicator.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum StatsTrend {
12    Up,
13    Down,
14}
15
16/// A `Card`-based metric tile: big number, label, and optional trend indicator.
17/// Callers compose multiple `StatsCard`s into a grid.
18#[derive(IntoElement, RegisterComponent)]
19pub struct StatsCard {
20    label: SharedString,
21    value: SharedString,
22    trend: Option<(StatsTrend, SharedString)>,
23}
24
25impl StatsCard {
26    pub fn new(label: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
27        Self {
28            label: label.into(),
29            value: value.into(),
30            trend: None,
31        }
32    }
33
34    pub fn trend(mut self, trend: StatsTrend, delta: impl Into<SharedString>) -> Self {
35        self.trend = Some((trend, delta.into()));
36        self
37    }
38}
39
40impl RenderOnce for StatsCard {
41    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
42        Card::new().child(
43            v_flex()
44                .gap_2()
45                .child(
46                    Label::new(self.label)
47                        .size(LabelSize::Small)
48                        .color(Color::Muted),
49                )
50                .child(
51                    h_flex()
52                        .items_end()
53                        .justify_between()
54                        .child(
55                            div()
56                                .text_size(rems(1.75))
57                                .font_weight(FontWeight::BOLD)
58                                .text_color(semantic::text(cx))
59                                .child(self.value),
60                        )
61                        .when_some(self.trend, |this, (trend, delta)| {
62                            let (icon, color) = match trend {
63                                StatsTrend::Up => (IconName::ArrowUp, palette::success(600)),
64                                StatsTrend::Down => (IconName::ArrowDown, palette::danger(600)),
65                            };
66                            this.child(
67                                h_flex()
68                                    .gap_1()
69                                    .items_center()
70                                    .child(
71                                        Icon::new(icon)
72                                            .size(IconSize::XSmall)
73                                            .color(Color::Custom(color)),
74                                    )
75                                    .child(
76                                        Label::new(delta)
77                                            .size(LabelSize::Small)
78                                            .color(Color::Custom(color)),
79                                    ),
80                            )
81                        }),
82                ),
83        )
84    }
85}
86
87impl Component for StatsCard {
88    fn scope() -> ComponentScope {
89        ComponentScope::DataDisplay
90    }
91
92    fn description() -> Option<&'static str> {
93        Some("A metric tile built on `Card`, with a large number, label, and optional trend.")
94    }
95
96    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
97        Some(
98            h_flex()
99                .gap_4()
100                .child(StatsCard::new("Total Subscribers", "71,897"))
101                .child(StatsCard::new("Avg. Open Rate", "58.16%").trend(StatsTrend::Up, "4.05%"))
102                .child(StatsCard::new("Avg. Click Rate", "24.57%").trend(StatsTrend::Down, "1.39%"))
103                .into_any_element(),
104        )
105    }
106}