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