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};
6
7use crate::{
8    ActiveTheme as _, AxisExt, Icon, Sizable, Size, StyledExt as _,
9    stepper::trigger::StepperTrigger,
10};
11
12/// A step item within a [`Stepper`].
13#[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    /// Set the icon of the stepper item.
46    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
47        self.icon = Some(icon.into());
48        self
49    }
50
51    /// Set disabled state of the stepper item.
52    ///
53    /// Will override the stepper's disabled state if set to true.
54    ///
55    /// Default is false.
56    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/// A separator between stepper items.
167///
168/// Default is `absolute` positioned.
169#[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}