Skip to main content

liora_components/
steps.rs

1use gpui::{App, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px};
2use liora_core::Config;
3use liora_icons::Icon;
4use liora_icons_lucide::IconName;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum StepsDirection {
8    #[default]
9    Horizontal,
10    Vertical,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum StepStatus {
15    Wait,
16    Process,
17    Finish,
18    Error,
19}
20
21pub struct StepItem {
22    pub title: SharedString,
23    pub description: Option<SharedString>,
24    pub icon: Option<IconName>,
25    pub status: Option<StepStatus>,
26}
27
28pub struct Steps {
29    active: usize,
30    direction: StepsDirection,
31    items: Vec<StepItem>,
32}
33
34impl StepItem {
35    pub fn new(title: impl Into<SharedString>) -> Self {
36        Self {
37            title: title.into(),
38            description: None,
39            icon: None,
40            status: None,
41        }
42    }
43
44    pub fn description(mut self, d: impl Into<SharedString>) -> Self {
45        self.description = Some(d.into());
46        self
47    }
48
49    pub fn icon(mut self, icon: IconName) -> Self {
50        self.icon = Some(icon);
51        self
52    }
53
54    pub fn status(mut self, s: StepStatus) -> Self {
55        self.status = Some(s);
56        self
57    }
58}
59
60impl Steps {
61    pub fn new() -> Self {
62        Self {
63            active: 0,
64            direction: StepsDirection::Horizontal,
65            items: vec![],
66        }
67    }
68
69    pub fn active(mut self, active: usize) -> Self {
70        self.active = active;
71        self
72    }
73
74    pub fn direction(mut self, d: StepsDirection) -> Self {
75        self.direction = d;
76        self
77    }
78
79    pub fn step(mut self, item: StepItem) -> Self {
80        self.items.push(item);
81        self
82    }
83}
84
85impl RenderOnce for Steps {
86    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
87        let theme = cx.global::<Config>().theme.clone();
88        let items_count = self.items.len();
89        let active = self.active;
90        let direction = self.direction;
91        let is_vertical = direction == StepsDirection::Vertical;
92
93        div()
94            .flex()
95            .when(!is_vertical, |s| s.flex_row().w_full())
96            .when(is_vertical, |s| s.flex_col().h_full())
97            .children(self.items.into_iter().enumerate().map(|(i, item)| {
98                let is_last = i == items_count - 1;
99                let status = item.status.unwrap_or_else(|| {
100                    if i < active {
101                        StepStatus::Finish
102                    } else if i == active {
103                        StepStatus::Process
104                    } else {
105                        StepStatus::Wait
106                    }
107                });
108
109                let color = match status {
110                    StepStatus::Finish => theme.primary.base,
111                    StepStatus::Process => theme.neutral.text_1,
112                    StepStatus::Wait => theme.neutral.text_3,
113                    StepStatus::Error => theme.danger.base,
114                };
115
116                let icon_bg = match status {
117                    StepStatus::Finish => gpui::transparent_black(),
118                    StepStatus::Process => theme.primary.base,
119                    StepStatus::Wait => gpui::transparent_black(),
120                    StepStatus::Error => theme.danger.base,
121                };
122
123                let icon_border = match status {
124                    StepStatus::Finish => theme.primary.base,
125                    StepStatus::Process => theme.primary.base,
126                    StepStatus::Wait => theme.neutral.border,
127                    StepStatus::Error => theme.danger.base,
128                };
129
130                let icon_color = match status {
131                    StepStatus::Finish => theme.primary.base,
132                    StepStatus::Process => theme.neutral.card,
133                    StepStatus::Wait => theme.neutral.text_3,
134                    StepStatus::Error => theme.neutral.card,
135                };
136
137                div()
138                    .flex()
139                    .when(!is_vertical, |s| s.flex_1().flex_row().items_center())
140                    .when(is_vertical, |s| s.flex_col())
141                    .child(
142                        div()
143                            .flex()
144                            .when(!is_vertical, |s| s.flex_row().items_center().gap_2())
145                            .when(is_vertical, |s| s.flex_col().items_start().gap_2())
146                            .child(
147                                // Icon/Number container
148                                div()
149                                    .flex()
150                                    .items_center()
151                                    .justify_center()
152                                    .w(px(24.0))
153                                    .h(px(24.0))
154                                    .rounded_full()
155                                    .border_1()
156                                    .border_color(icon_border)
157                                    .bg(icon_bg)
158                                    .child(match item.icon {
159                                        Some(icon) => Icon::new(icon)
160                                            .size(px(14.0))
161                                            .color(icon_color)
162                                            .into_any_element(),
163                                        None => {
164                                            if status == StepStatus::Finish {
165                                                Icon::new(IconName::Check)
166                                                    .size(px(14.0))
167                                                    .color(icon_color)
168                                                    .into_any_element()
169                                            } else {
170                                                div()
171                                                    .text_xs()
172                                                    .text_color(icon_color)
173                                                    .child((i + 1).to_string())
174                                                    .into_any_element()
175                                            }
176                                        }
177                                    }),
178                            )
179                            .child(
180                                div()
181                                    .flex()
182                                    .flex_col()
183                                    .child(
184                                        div()
185                                            .text_sm()
186                                            .font_weight(gpui::FontWeight::BOLD)
187                                            .text_color(color)
188                                            .child(item.title),
189                                    )
190                                    .when_some(item.description, |s, d| {
191                                        s.child(
192                                            div()
193                                                .text_xs()
194                                                .text_color(theme.neutral.text_3)
195                                                .child(d),
196                                        )
197                                    }),
198                            ),
199                    )
200                    .when(!is_last, |s| {
201                        s.child(
202                            // Line
203                            div()
204                                .flex_1()
205                                .when(!is_vertical, |s| {
206                                    s.mx_4().h(px(1.0)).bg(if i < active {
207                                        theme.primary.base
208                                    } else {
209                                        theme.neutral.border
210                                    })
211                                })
212                                .when(is_vertical, |s| {
213                                    s.ml(px(12.0)).my_2().w(px(1.0)).min_h(px(40.0)).bg(
214                                        if i < active {
215                                            theme.primary.base
216                                        } else {
217                                            theme.neutral.border
218                                        },
219                                    )
220                                }),
221                        )
222                    })
223            }))
224    }
225}
226
227impl IntoElement for Steps {
228    type Element = gpui::Component<Self>;
229    fn into_element(self) -> Self::Element {
230        gpui::Component::new(self)
231    }
232}