Skip to main content

liora_components/
statistic.rs

1use gpui::{
2    AnyElement, App, Hsla, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px,
3};
4use liora_core::Config;
5use liora_icons::{Icon, IntoIconPath};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum StatisticLayout {
9    Vertical,
10    HorizontalCompact,
11    HorizontalBetween,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum StatisticIconPosition {
16    Left,
17    Right,
18}
19
20pub struct Statistic {
21    title: SharedString,
22    value: SharedString,
23    prefix: Option<AnyElement>,
24    suffix: Option<AnyElement>,
25    value_color: Option<Hsla>,
26    icon: Option<String>,
27    icon_position: StatisticIconPosition,
28    icon_color: Option<Hsla>,
29    layout: StatisticLayout,
30}
31
32impl Statistic {
33    pub fn new(title: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
34        Self {
35            title: title.into(),
36            value: value.into(),
37            prefix: None,
38            suffix: None,
39            value_color: None,
40            icon: None,
41            icon_position: StatisticIconPosition::Right,
42            icon_color: None,
43            layout: StatisticLayout::Vertical,
44        }
45    }
46
47    pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
48        self.prefix = Some(prefix.into_any_element());
49        self
50    }
51
52    pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
53        self.suffix = Some(suffix.into_any_element());
54        self
55    }
56
57    pub fn value_color(mut self, color: Hsla) -> Self {
58        self.value_color = Some(color);
59        self
60    }
61
62    pub fn icon(mut self, icon: impl IntoIconPath) -> Self {
63        self.icon = Some(icon.icon_path().into_owned());
64        self
65    }
66
67    pub fn icon_position(mut self, position: StatisticIconPosition) -> Self {
68        self.icon_position = position;
69        self
70    }
71
72    pub fn icon_left(self) -> Self {
73        self.icon_position(StatisticIconPosition::Left)
74    }
75
76    pub fn icon_right(self) -> Self {
77        self.icon_position(StatisticIconPosition::Right)
78    }
79
80    pub fn icon_color(mut self, color: Hsla) -> Self {
81        self.icon_color = Some(color);
82        self
83    }
84
85    pub fn layout(mut self, layout: StatisticLayout) -> Self {
86        self.layout = layout;
87        self
88    }
89
90    pub fn vertical(self) -> Self {
91        self.layout(StatisticLayout::Vertical)
92    }
93
94    pub fn horizontal(self) -> Self {
95        self.horizontal_compact()
96    }
97
98    pub fn horizontal_compact(self) -> Self {
99        self.layout(StatisticLayout::HorizontalCompact)
100    }
101
102    pub fn horizontal_between(self) -> Self {
103        self.layout(StatisticLayout::HorizontalBetween)
104    }
105
106    fn resolved_icon_color(&self, value_color: Hsla) -> Hsla {
107        self.icon_color.unwrap_or(value_color)
108    }
109}
110
111impl RenderOnce for Statistic {
112    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
113        let theme = cx.global::<Config>().theme.clone();
114        let value_line_height = px(32.0);
115        let value_color = self.value_color.unwrap_or(theme.neutral.text_1);
116        let icon_color = self.resolved_icon_color(value_color);
117        let icon_position = self.icon_position;
118
119        let title = div()
120            .text_sm()
121            .text_color(theme.neutral.text_3)
122            .child(self.title);
123
124        let icon = self.icon.map(|path| {
125            Icon::new(path)
126                .size(px(18.0))
127                .color(icon_color)
128                .into_any_element()
129        });
130        let (leading_icon, trailing_icon) = match icon_position {
131            StatisticIconPosition::Left => (icon, None),
132            StatisticIconPosition::Right => (None, icon),
133        };
134
135        let value = div()
136            .text_2xl()
137            .line_height(value_line_height)
138            .font_weight(gpui::FontWeight::BOLD)
139            .text_color(value_color)
140            .child(self.value);
141
142        let value_line = div()
143            .flex()
144            .flex_row()
145            .items_center()
146            .gap_2()
147            .when_some(self.prefix, |s, p| {
148                s.child(
149                    div()
150                        .flex()
151                        .items_center()
152                        .justify_center()
153                        .h(value_line_height)
154                        .child(p),
155                )
156            })
157            .when_some(leading_icon, |s, icon| {
158                s.child(
159                    div()
160                        .flex()
161                        .items_center()
162                        .justify_center()
163                        .h(value_line_height)
164                        .child(icon),
165                )
166            })
167            .child(value)
168            .when_some(trailing_icon, |s, icon| {
169                s.child(
170                    div()
171                        .flex()
172                        .items_center()
173                        .justify_center()
174                        .h(value_line_height)
175                        .child(icon),
176                )
177            })
178            .when_some(self.suffix, |s, p| {
179                s.child(
180                    div()
181                        .flex()
182                        .items_center()
183                        .justify_center()
184                        .h(value_line_height)
185                        .child(p),
186                )
187            });
188
189        match self.layout {
190            StatisticLayout::Vertical => div()
191                .flex()
192                .flex_col()
193                .gap_1()
194                .child(title)
195                .child(value_line),
196            StatisticLayout::HorizontalCompact => div()
197                .flex()
198                .flex_row()
199                .items_center()
200                .gap_4()
201                .child(title)
202                .child(value_line),
203            StatisticLayout::HorizontalBetween => div()
204                .flex()
205                .flex_row()
206                .items_center()
207                .justify_between()
208                .gap_4()
209                .w_full()
210                .child(title)
211                .child(value_line),
212        }
213    }
214}
215
216impl IntoElement for Statistic {
217    type Element = gpui::Component<Self>;
218    fn into_element(self) -> Self::Element {
219        gpui::Component::new(self)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use liora_icons_lucide::IconName;
227
228    #[test]
229    fn statistic_horizontal_helpers_set_layout() {
230        assert_eq!(
231            Statistic::new("Visitors", "1,024")
232                .horizontal_compact()
233                .layout,
234            StatisticLayout::HorizontalCompact
235        );
236        assert_eq!(
237            Statistic::new("Visitors", "1,024")
238                .horizontal_between()
239                .layout,
240            StatisticLayout::HorizontalBetween
241        );
242    }
243
244    #[test]
245    fn statistic_icon_helpers_set_position_and_color() {
246        let icon_color = gpui::red();
247        let statistic = Statistic::new("Growth", "12.5")
248            .icon(IconName::TrendingUp)
249            .icon_left()
250            .icon_color(icon_color);
251
252        assert_eq!(statistic.icon_position, StatisticIconPosition::Left);
253        assert!(statistic.icon.is_some());
254        assert_eq!(statistic.icon_color, Some(icon_color));
255    }
256
257    #[test]
258    fn statistic_icon_color_defaults_to_value_color() {
259        let value_color = gpui::green();
260        assert_eq!(
261            Statistic::new("Growth", "12.5")
262                .icon(IconName::TrendingUp)
263                .resolved_icon_color(value_color),
264            value_color
265        );
266    }
267}