Skip to main content

rgpui_component/stepper/
item.rs

1use rgpui::{
2    AnyElement, App, Axis, ClickEvent, Half, InteractiveElement as _, IntoElement, ParentElement,
3    Pixels, RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _, px,
4    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            .relative()
127            .when(self.layout.is_horizontal(), |this| this.h_flex())
128            .when(self.layout.is_vertical(), |this| this.v_flex())
129            .when(!self.is_last, |this| this.flex_1())
130            .when(self.text_center, |this| this.flex_1().justify_center())
131            .items_start()
132            .refine_style(&self.style)
133            .child(
134                StepperTrigger::new()
135                    .icon(self.icon)
136                    .icon_size(icon_size)
137                    .step(self.step)
138                    .with_size(self.size)
139                    .checked_step(self.checked_step)
140                    .text_center(self.text_center)
141                    .layout(self.layout)
142                    .disabled(self.disabled)
143                    .children(self.children)
144                    .on_click({
145                        let on_click = self.on_click;
146                        move |e, window, cx| {
147                            on_click(e, window, cx);
148                        }
149                    }),
150            )
151            .when(!self.is_last, |this| {
152                this.child(
153                    StepperSeparator::new()
154                        .with_size(self.size)
155                        .layout(self.layout)
156                        .text_center(self.text_center)
157                        .icon_size(icon_size)
158                        .checked(is_passed),
159                )
160            })
161    }
162}
163
164/// A separator between stepper items.
165///
166/// Default is `absolute` positioned.
167#[derive(IntoElement)]
168struct StepperSeparator {
169    size: Size,
170    checked: bool,
171    icon_size: Pixels,
172    layout: Axis,
173    style: StyleRefinement,
174    text_center: bool,
175}
176
177impl StepperSeparator {
178    fn new() -> Self {
179        Self {
180            size: Size::default(),
181            checked: false,
182            icon_size: px(24.),
183            layout: Axis::Horizontal,
184            style: StyleRefinement::default(),
185            text_center: false,
186        }
187    }
188
189    fn with_size(mut self, size: Size) -> Self {
190        self.size = size;
191        self
192    }
193
194    fn text_center(mut self, center: bool) -> Self {
195        self.text_center = center;
196        self
197    }
198
199    fn layout(mut self, layout: Axis) -> Self {
200        self.layout = layout;
201        self
202    }
203
204    fn icon_size(mut self, size: Pixels) -> Self {
205        self.icon_size = size;
206        self
207    }
208
209    fn checked(mut self, checked: bool) -> Self {
210        self.checked = checked;
211        self
212    }
213}
214
215impl Styled for StepperSeparator {
216    fn style(&mut self) -> &mut StyleRefinement {
217        &mut self.style
218    }
219}
220
221impl RenderOnce for StepperSeparator {
222    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
223        let icon_size = self.icon_size;
224        let text_center = self.text_center;
225        let separator_wide = match self.size {
226            Size::XSmall => px(1.5),
227            Size::Large => px(3.),
228            _ => px(2.),
229        };
230
231        let gap = px(4.);
232
233        div()
234            .absolute()
235            .flex_1()
236            .when(self.layout.is_horizontal(), |this| {
237                this.h(separator_wide).mt(icon_size.half()).map(|this| {
238                    if !text_center {
239                        this.ml(icon_size + gap).mr(gap).left_0().right_0()
240                    } else {
241                        this.mx(icon_size.half() + gap)
242                            .left(relative(0.5))
243                            .right(relative(-0.5))
244                    }
245                })
246            })
247            .when(self.layout.is_vertical(), |this| {
248                this.w(separator_wide).ml(icon_size.half()).map(|this| {
249                    if !text_center {
250                        this.mt(icon_size + gap).mb(gap).top_0().bottom_0()
251                    } else {
252                        this.mx(icon_size.half() + gap)
253                            .top(relative(0.5))
254                            .bottom(relative(-0.5))
255                    }
256                })
257            })
258            .refine_style(&self.style)
259            .bg(cx.theme().border)
260            .when(self.checked, |this| this.bg(cx.theme().primary))
261    }
262}