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