Skip to main content

gpui_component/stepper/
item.rs

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/// A step item within a [`Stepper`].
14#[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    /// Set the icon of the stepper item.
47    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
48        self.icon = Some(icon.into());
49        self
50    }
51
52    /// Set disabled state of the stepper item.
53    ///
54    /// Will override the stepper's disabled state if set to true.
55    ///
56    /// Default is false.
57    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/// A separator between stepper items.
169///
170/// Default is `absolute` positioned.
171#[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}