1use gpui::{AnyElement, ClickEvent, white};
2use smallvec::SmallVec;
3
4use crate::prelude::*;
5
6pub struct StepperStep {
8 label: SharedString,
9 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
10}
11
12impl StepperStep {
13 pub fn new(label: impl Into<SharedString>) -> Self {
14 Self {
15 label: label.into(),
16 on_click: None,
17 }
18 }
19
20 pub fn on_click(
21 mut self,
22 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
23 ) -> Self {
24 self.on_click = Some(Box::new(handler));
25 self
26 }
27}
28
29#[derive(Clone, Copy, PartialEq, Eq)]
30enum StepState {
31 Completed,
32 Current,
33 Upcoming,
34}
35
36#[derive(IntoElement, RegisterComponent)]
42pub struct Stepper {
43 current_step: usize,
44 steps: SmallVec<[StepperStep; 4]>,
45}
46
47impl Stepper {
48 pub fn new(current_step: usize) -> Self {
50 Self {
51 current_step,
52 steps: SmallVec::new(),
53 }
54 }
55
56 pub fn step(mut self, step: StepperStep) -> Self {
57 self.steps.push(step);
58 self
59 }
60
61 pub fn steps(mut self, steps: impl IntoIterator<Item = StepperStep>) -> Self {
62 self.steps.extend(steps);
63 self
64 }
65}
66
67impl RenderOnce for Stepper {
68 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
69 let total = self.steps.len();
70 let current_step = self.current_step;
71
72 h_flex()
73 .items_start()
74 .children(self.steps.into_iter().enumerate().map(|(index, step)| {
75 let state = if index < current_step {
76 StepState::Completed
77 } else if index == current_step {
78 StepState::Current
79 } else {
80 StepState::Upcoming
81 };
82
83 let (circle_bg, circle_border, accent_color) = match state {
84 StepState::Completed => (palette::primary(600), palette::primary(600), white()),
85 StepState::Current => (
86 gpui::transparent_black(),
87 palette::primary(600),
88 palette::primary(600),
89 ),
90 StepState::Upcoming => (
91 gpui::transparent_black(),
92 semantic::border_muted(cx),
93 semantic::text_muted(cx),
94 ),
95 };
96
97 let circle_content = if state == StepState::Completed {
98 Icon::new(IconName::Check)
99 .size(IconSize::Small)
100 .color(Color::Custom(white()))
101 .into_any_element()
102 } else {
103 Label::new((index + 1).to_string())
104 .size(LabelSize::Small)
105 .color(Color::Custom(accent_color))
106 .into_any_element()
107 };
108
109 let circle = h_flex()
110 .id(("stepper-step", index))
111 .size_8()
112 .justify_center()
113 .rounded_full()
114 .border_2()
115 .border_color(circle_border)
116 .bg(circle_bg)
117 .when(step.on_click.is_some(), |this| this.cursor_pointer())
118 .child(circle_content)
119 .when_some(step.on_click, |this, handler| this.on_click(handler));
120
121 h_flex()
122 .items_start()
123 .child(
124 v_flex().items_center().gap_1().child(circle).child(
125 Label::new(step.label)
126 .size(LabelSize::XSmall)
127 .color(Color::Custom(accent_color)),
128 ),
129 )
130 .when(index + 1 < total, |this| {
131 let line_color = if state == StepState::Completed {
132 palette::primary(600)
133 } else {
134 semantic::border_muted(cx)
135 };
136 this.child(div().w_8().h_px().mt(px(16.)).bg(line_color))
137 })
138 }))
139 }
140}
141
142impl Component for Stepper {
143 fn scope() -> ComponentScope {
144 ComponentScope::Navigation
145 }
146
147 fn description() -> Option<&'static str> {
148 Some("A horizontal row of step circles connected by a line, for multi-step flows.")
149 }
150
151 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
152 let steps = || {
153 [
154 StepperStep::new("Account"),
155 StepperStep::new("Profile"),
156 StepperStep::new("Confirm"),
157 ]
158 };
159
160 Some(
161 v_flex()
162 .gap_6()
163 .child(example_group_with_title(
164 "Progress States",
165 vec![
166 single_example(
167 "Not Started",
168 Stepper::new(0).steps(steps()).into_any_element(),
169 ),
170 single_example(
171 "Second Step In Progress",
172 Stepper::new(1).steps(steps()).into_any_element(),
173 ),
174 single_example(
175 "All Complete",
176 Stepper::new(3).steps(steps()).into_any_element(),
177 ),
178 ],
179 ))
180 .into_any_element(),
181 )
182 }
183}