1use gpui::{
2 AnyElement, App, Axis, ClickEvent, Half, InteractiveElement as _, IntoElement, ParentElement,
3 Pixels, RenderOnce, Role, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
4 div, prelude::FluentBuilder as _, px, relative,
5};
6
7use crate::{
8 ActiveTheme as _, AxisExt, Icon, Sizable, Size, StyledExt as _,
9 stepper::trigger::StepperTrigger,
10};
11
12#[derive(IntoElement)]
14pub struct StepperItem {
15 step: usize,
16 checked_step: usize,
17 style: StyleRefinement,
18 icon: Option<Icon>,
19 children: Vec<AnyElement>,
20 layout: Axis,
21 disabled: bool,
22 size: Size,
23 is_last: bool,
24 text_center: bool,
25 on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
26}
27
28impl StepperItem {
29 pub fn new() -> Self {
30 Self {
31 step: 0,
32 checked_step: 0,
33 style: StyleRefinement::default(),
34 icon: None,
35 layout: Axis::Horizontal,
36 disabled: false,
37 size: Size::default(),
38 is_last: false,
39 text_center: false,
40 children: Vec::new(),
41 on_click: Box::new(|_, _, _| {}),
42 }
43 }
44
45 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
47 self.icon = Some(icon.into());
48 self
49 }
50
51 pub fn disabled(mut self, disabled: bool) -> Self {
57 self.disabled = disabled;
58 self
59 }
60
61 pub(super) fn text_center(mut self, center: bool) -> Self {
62 self.text_center = center;
63 self
64 }
65
66 pub(super) fn step(mut self, ix: usize) -> Self {
67 self.step = ix;
68 self
69 }
70
71 pub(super) fn checked_step(mut self, checked_step: usize) -> Self {
72 self.checked_step = checked_step;
73 self
74 }
75
76 pub(super) fn layout(mut self, layout: Axis) -> Self {
77 self.layout = layout;
78 self
79 }
80
81 pub(super) fn is_last(mut self, is_last: bool) -> Self {
82 self.is_last = is_last;
83 self
84 }
85
86 pub(super) fn on_click<F>(mut self, f: F) -> Self
87 where
88 F: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
89 {
90 self.on_click = Box::new(f);
91 self
92 }
93}
94
95impl ParentElement for StepperItem {
96 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
97 self.children.extend(elements);
98 }
99}
100
101impl Sizable for StepperItem {
102 fn with_size(mut self, size: impl Into<Size>) -> Self {
103 self.size = size.into();
104 self
105 }
106}
107
108impl Styled for StepperItem {
109 fn style(&mut self) -> &mut StyleRefinement {
110 &mut self.style
111 }
112}
113
114impl RenderOnce for StepperItem {
115 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
116 let is_passed = self.step < self.checked_step;
117 let icon_size = match self.size {
118 Size::XSmall => px(8.),
119 Size::Small => px(18.),
120 Size::Large => px(32.),
121 _ => px(24.),
122 };
123
124 div()
125 .id(("stepper-item", self.step))
126 .role(Role::ListItem)
127 .aria_position_in_set(self.step + 1)
128 .relative()
129 .when(self.layout.is_horizontal(), |this| this.h_flex())
130 .when(self.layout.is_vertical(), |this| this.v_flex())
131 .when(!self.is_last, |this| this.flex_1())
132 .when(self.text_center, |this| this.flex_1().justify_center())
133 .items_start()
134 .refine_style(&self.style)
135 .child(
136 StepperTrigger::new()
137 .icon(self.icon)
138 .icon_size(icon_size)
139 .step(self.step)
140 .with_size(self.size)
141 .checked_step(self.checked_step)
142 .text_center(self.text_center)
143 .layout(self.layout)
144 .disabled(self.disabled)
145 .children(self.children)
146 .on_click({
147 let on_click = self.on_click;
148 move |e, window, cx| {
149 on_click(e, window, cx);
150 }
151 }),
152 )
153 .when(!self.is_last, |this| {
154 this.child(
155 StepperSeparator::new()
156 .with_size(self.size)
157 .layout(self.layout)
158 .text_center(self.text_center)
159 .icon_size(icon_size)
160 .checked(is_passed),
161 )
162 })
163 }
164}
165
166#[derive(IntoElement)]
170struct StepperSeparator {
171 size: Size,
172 checked: bool,
173 icon_size: Pixels,
174 layout: Axis,
175 style: StyleRefinement,
176 text_center: bool,
177}
178
179impl StepperSeparator {
180 fn new() -> Self {
181 Self {
182 size: Size::default(),
183 checked: false,
184 icon_size: px(24.),
185 layout: Axis::Horizontal,
186 style: StyleRefinement::default(),
187 text_center: false,
188 }
189 }
190
191 fn with_size(mut self, size: Size) -> Self {
192 self.size = size;
193 self
194 }
195
196 fn text_center(mut self, center: bool) -> Self {
197 self.text_center = center;
198 self
199 }
200
201 fn layout(mut self, layout: Axis) -> Self {
202 self.layout = layout;
203 self
204 }
205
206 fn icon_size(mut self, size: Pixels) -> Self {
207 self.icon_size = size;
208 self
209 }
210
211 fn checked(mut self, checked: bool) -> Self {
212 self.checked = checked;
213 self
214 }
215}
216
217impl Styled for StepperSeparator {
218 fn style(&mut self) -> &mut StyleRefinement {
219 &mut self.style
220 }
221}
222
223impl RenderOnce for StepperSeparator {
224 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
225 let icon_size = self.icon_size;
226 let text_center = self.text_center;
227 let separator_wide = match self.size {
228 Size::XSmall => px(1.5),
229 Size::Large => px(3.),
230 _ => px(2.),
231 };
232
233 let gap = px(4.);
234
235 div()
236 .absolute()
237 .flex_1()
238 .when(self.layout.is_horizontal(), |this| {
239 this.h(separator_wide).mt(icon_size.half()).map(|this| {
240 if !text_center {
241 this.ml(icon_size + gap).mr(gap).left_0().right_0()
242 } else {
243 this.mx(icon_size.half() + gap)
244 .left(relative(0.5))
245 .right(relative(-0.5))
246 }
247 })
248 })
249 .when(self.layout.is_vertical(), |this| {
250 this.w(separator_wide).ml(icon_size.half()).map(|this| {
251 if !text_center {
252 this.mt(icon_size + gap).mb(gap).top_0().bottom_0()
253 } else {
254 this.mx(icon_size.half() + gap)
255 .top(relative(0.5))
256 .bottom(relative(-0.5))
257 }
258 })
259 })
260 .refine_style(&self.style)
261 .bg(cx.theme().border)
262 .when(self.checked, |this| this.bg(cx.theme().tokens.primary))
263 }
264}