liora_components/
statistic.rs1use 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}