Skip to main content

azul_layout/widgets/
stepper.rs

1//! Stepper / wizard widget — a horizontal multi-step progress indicator: a row
2//! of numbered step circles with labels, joined by connector lines. Completed
3//! and current steps are highlighted in the accent colour; upcoming steps (and
4//! the connectors that lead to them) are muted.
5//!
6//! This is a blend of [`crate::widgets::segmented::Segmented`] (a horizontal row
7//! of clickable items whose clicked index is derived from sibling position and
8//! whose active item is live-restyled via `set_css_property`) and the filled-track
9//! look of [`crate::widgets::progressbar::ProgressBar`] (the accent connector).
10//!
11//! Steps are CLICKABLE (free navigation, like a segmented control): clicking
12//! step `i` sets `current_step = i`, invokes the optional `on_step_change(state)`,
13//! and live-restyles every circle / connector / label to reflect the new
14//! position — no DOM rebuild. (A non-clickable, display-only stepper is also a
15//! valid design; this widget chooses clickable to exercise the segmented restyle
16//! pattern, and `set_current_step` still drives it from app code on rebuild.)
17//!
18//! A circle is "reached" (accent) iff its index `<= current_step`; the connector
19//! gap between circle `i` and `i+1` is accent iff `i < current_step`. Clicking the
20//! already-current step is a no-op (no callback).
21//!
22//! Key types: [`Stepper`], [`StepperState`], [`StepperOnStepChange`].
23
24use std::vec::Vec;
25
26use azul_core::{
27    callbacks::{CoreCallbackData, Update},
28    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
29    refany::RefAny,
30};
31use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
32use azul_css::{
33    props::{
34        basic::{color::ColorU, PixelValue, StyleFontSize},
35        layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutFlexGrow, LayoutFlexBasis, LayoutWidth, LayoutJustifyContent, LayoutHeight, LayoutMinWidth, LayoutPaddingTop},
36        property::{CssProperty, LayoutFlexBasisValue, LayoutWidthValue},
37        style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleCursor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextAlign, StyleUserSelect, StyleTextColor},
38    },
39    impl_option_inner, AzString, StringVec,
40};
41
42use crate::callbacks::CallbackInfo;
43
44static STEPPER_CLASS: &[IdOrClass] = &[Class(AzString::from_const_str("__azul-native-stepper"))];
45static STEPPER_STEP_CLASS: &[IdOrClass] =
46    &[Class(AzString::from_const_str("__azul-native-stepper-step"))];
47static STEPPER_ROW_CLASS: &[IdOrClass] =
48    &[Class(AzString::from_const_str("__azul-native-stepper-row"))];
49static STEPPER_CIRCLE_CLASS: &[IdOrClass] =
50    &[Class(AzString::from_const_str("__azul-native-stepper-circle"))];
51static STEPPER_CONNECTOR_CLASS: &[IdOrClass] =
52    &[Class(AzString::from_const_str("__azul-native-stepper-connector"))];
53static STEPPER_LABEL_CLASS: &[IdOrClass] =
54    &[Class(AzString::from_const_str("__azul-native-stepper-label"))];
55
56/// Callback function type invoked when the current step changes.
57pub type StepperOnStepChangeCallbackType =
58    extern "C" fn(RefAny, CallbackInfo, StepperState) -> Update;
59impl_widget_callback!(
60    StepperOnStepChange,
61    OptionStepperOnStepChange,
62    StepperOnStepChangeCallback,
63    StepperOnStepChangeCallbackType
64);
65
66azul_core::impl_managed_callback! {
67    wrapper:        StepperOnStepChangeCallback,
68    info_ty:        CallbackInfo,
69    return_ty:      Update,
70    default_ret:    Update::DoNothing,
71    invoker_static: STEPPER_ON_STEP_CHANGE_INVOKER,
72    invoker_ty:     AzStepperOnStepChangeCallbackInvoker,
73    thunk_fn:       az_stepper_on_step_change_callback_thunk,
74    setter_fn:      AzApp_setStepperOnStepChangeCallbackInvoker,
75    from_handle_fn: AzStepperOnStepChangeCallback_createFromHostHandle,
76    extra_args:     [ state: StepperState ],
77}
78
79/// A horizontal numbered-step progress indicator with a step-change callback.
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[repr(C)]
82pub struct Stepper {
83    pub stepper_state: StepperStateWrapper,
84    /// The label of each step, in order. The step count is `labels.len()`.
85    pub labels: StringVec,
86    /// Style for the row container.
87    pub container_style: CssPropertyWithConditionsVec,
88}
89
90#[derive(Debug, Default, Clone, PartialEq, Eq)]
91#[repr(C)]
92pub struct StepperStateWrapper {
93    /// The current step + total step count.
94    pub inner: StepperState,
95    /// Optional: function to call when the current step changes.
96    pub on_step_change: OptionStepperOnStepChange,
97}
98
99/// State of a [`Stepper`]: the zero-based current step and the total step count.
100#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
101#[repr(C)]
102pub struct StepperState {
103    /// Zero-based index of the current (active) step.
104    pub current_step: usize,
105    /// Total number of steps.
106    pub total_steps: usize,
107}
108
109// ---- colours ----
110/// Accent (reached/current) colour (#0d6efd).
111const ACCENT_COLOR: ColorU = ColorU { r: 13, g: 110, b: 253, a: 255 };
112/// Accent text colour (white) — the number inside a reached circle.
113const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
114/// Upcoming-circle background (#e9ecef, light grey).
115const MUTED_CIRCLE_COLOR: ColorU = ColorU { r: 233, g: 236, b: 239, a: 255 };
116/// Muted text colour (#868e96) — upcoming numbers/labels.
117const MUTED_TEXT_COLOR: ColorU = ColorU { r: 134, g: 142, b: 150, a: 255 };
118/// Reached-label text colour (#212529, dark).
119const DARK_TEXT_COLOR: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
120/// Upcoming-connector colour (#ced4da).
121const CONNECTOR_MUTED_COLOR: ColorU = ColorU { r: 206, g: 212, b: 218, a: 255 };
122/// Transparent — used for the (absent) connector at the row's two ends.
123const TRANSPARENT_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
124
125const ACCENT_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(ACCENT_COLOR)];
126const ACCENT_BG: StyleBackgroundContentVec =
127    StyleBackgroundContentVec::from_const_slice(ACCENT_BG_ITEMS);
128const MUTED_CIRCLE_BG_ITEMS: &[StyleBackgroundContent] =
129    &[StyleBackgroundContent::Color(MUTED_CIRCLE_COLOR)];
130const MUTED_CIRCLE_BG: StyleBackgroundContentVec =
131    StyleBackgroundContentVec::from_const_slice(MUTED_CIRCLE_BG_ITEMS);
132const CONNECTOR_MUTED_BG_ITEMS: &[StyleBackgroundContent] =
133    &[StyleBackgroundContent::Color(CONNECTOR_MUTED_COLOR)];
134const CONNECTOR_MUTED_BG: StyleBackgroundContentVec =
135    StyleBackgroundContentVec::from_const_slice(CONNECTOR_MUTED_BG_ITEMS);
136const TRANSPARENT_BG_ITEMS: &[StyleBackgroundContent] =
137    &[StyleBackgroundContent::Color(TRANSPARENT_COLOR)];
138const TRANSPARENT_BG: StyleBackgroundContentVec =
139    StyleBackgroundContentVec::from_const_slice(TRANSPARENT_BG_ITEMS);
140
141const CIRCLE_SIZE: isize = 28;
142const CIRCLE_RADIUS: isize = 14;
143const CONNECTOR_HEIGHT: isize = 2;
144
145/// Connector fill state for one half-segment.
146#[derive(Copy, Clone)]
147enum ConnFill {
148    /// Reached (accent).
149    Accent,
150    /// Not reached (muted grey).
151    Muted,
152    /// At a row end — drawn transparent so the line doesn't stick out.
153    Hidden,
154}
155
156impl ConnFill {
157    const fn bg(self) -> StyleBackgroundContentVec {
158        match self {
159            Self::Accent => ACCENT_BG,
160            Self::Muted => CONNECTOR_MUTED_BG,
161            Self::Hidden => TRANSPARENT_BG,
162        }
163    }
164}
165
166/// Row container: a horizontal flex row whose steps spread evenly.
167static STEPPER_CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
168    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
169    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
170    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Start)),
171    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
172];
173
174/// One step cell: a vertical flex column (indicator row over label) that grows to
175/// an equal share of the row (`flex-grow: 1; flex-basis: 0`).
176static STEPPER_STEP_STYLE: &[CssPropertyWithConditions] = &[
177    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
178    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
179    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
180    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
181    CssPropertyWithConditions::simple(CssProperty::FlexBasis(LayoutFlexBasisValue::Exact(
182        LayoutFlexBasis::Exact(PixelValue::const_px(0)),
183    ))),
184    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
185];
186
187/// Builds the indicator-row style: a full-width flex row that vertically centres
188/// the connectors (height `CONNECTOR_HEIGHT`) on the circle.
189fn row_style() -> CssPropertyWithConditionsVec {
190    CssPropertyWithConditionsVec::from_vec(vec![
191        CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
192        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
193            LayoutFlexDirection::Row,
194        )),
195        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
196        // Full cell width so the flex-grow connectors actually have space to fill.
197        CssPropertyWithConditions::simple(CssProperty::Width(LayoutWidthValue::Exact(
198            LayoutWidth::Px(PixelValue::percent(100.0)),
199        ))),
200    ])
201}
202
203/// Builds the style for one numbered circle. Background + number colour are the
204/// only reached-dependent properties.
205fn circle_style(reached: bool) -> CssPropertyWithConditionsVec {
206    let (bg, text) = if reached {
207        (ACCENT_BG, WHITE)
208    } else {
209        (MUTED_CIRCLE_BG, MUTED_TEXT_COLOR)
210    };
211    CssPropertyWithConditionsVec::from_vec(vec![
212        CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
213        CssPropertyWithConditions::simple(CssProperty::const_flex_direction(
214            LayoutFlexDirection::Row,
215        )),
216        CssPropertyWithConditions::simple(CssProperty::const_justify_content(
217            LayoutJustifyContent::Center,
218        )),
219        CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
220        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
221        CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
222            CIRCLE_SIZE,
223        ))),
224        CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
225            CIRCLE_SIZE,
226        ))),
227        CssPropertyWithConditions::simple(CssProperty::const_min_width(LayoutMinWidth::const_px(
228            CIRCLE_SIZE,
229        ))),
230        CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
231            StyleBorderTopLeftRadius::const_px(CIRCLE_RADIUS),
232        )),
233        CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
234            StyleBorderTopRightRadius::const_px(CIRCLE_RADIUS),
235        )),
236        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
237            StyleBorderBottomLeftRadius::const_px(CIRCLE_RADIUS),
238        )),
239        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
240            StyleBorderBottomRightRadius::const_px(CIRCLE_RADIUS),
241        )),
242        CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
243        CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
244        CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
245        CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
246        CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
247        CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
248            inner: text,
249        })),
250    ])
251}
252
253/// Builds the style for one connector half-line (left or right of a circle).
254fn connector_style(fill: ConnFill) -> CssPropertyWithConditionsVec {
255    CssPropertyWithConditionsVec::from_vec(vec![
256        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
257        CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
258            CONNECTOR_HEIGHT,
259        ))),
260        CssPropertyWithConditions::simple(CssProperty::const_background_content(fill.bg())),
261    ])
262}
263
264/// Builds the style for one step label.
265fn label_style(reached: bool) -> CssPropertyWithConditionsVec {
266    let text = if reached { DARK_TEXT_COLOR } else { MUTED_TEXT_COLOR };
267    CssPropertyWithConditionsVec::from_vec(vec![
268        CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(12))),
269        CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
270        CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
271        CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
272        CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
273            6,
274        ))),
275        CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
276            inner: text,
277        })),
278    ])
279}
280
281/// Connector fill for the left half-line of step `i` (the gap entering circle `i`).
282const fn conn_left_fill(i: usize, current: usize) -> ConnFill {
283    if i == 0 {
284        ConnFill::Hidden
285    } else if i <= current {
286        ConnFill::Accent
287    } else {
288        ConnFill::Muted
289    }
290}
291
292/// Connector fill for the right half-line of step `i` (the gap leaving circle `i`).
293const fn conn_right_fill(i: usize, last: usize, current: usize) -> ConnFill {
294    if i == last {
295        ConnFill::Hidden
296    } else if i < current {
297        ConnFill::Accent
298    } else {
299        ConnFill::Muted
300    }
301}
302
303impl Stepper {
304    /// Creates a stepper from the given step labels, with the first step current.
305    #[must_use] pub fn create(labels: StringVec) -> Self {
306        let total_steps = labels.as_ref().len();
307        Self {
308            stepper_state: StepperStateWrapper {
309                inner: StepperState {
310                    current_step: 0,
311                    total_steps,
312                },
313                ..Default::default()
314            },
315            labels,
316            container_style: CssPropertyWithConditionsVec::from_const_slice(
317                STEPPER_CONTAINER_STYLE,
318            ),
319        }
320    }
321
322    /// Sets the current (zero-based) step, clamped into `[0, total_steps - 1]`.
323    #[inline]
324    pub fn set_current_step(&mut self, current_step: usize) {
325        let total = self.stepper_state.inner.total_steps;
326        self.stepper_state.inner.current_step = if total == 0 {
327            0
328        } else {
329            current_step.min(total - 1)
330        };
331    }
332
333    /// Builder-style setter for the current step.
334    #[inline]
335    #[must_use] pub fn with_current_step(mut self, current_step: usize) -> Self {
336        self.set_current_step(current_step);
337        self
338    }
339
340    #[inline]
341    #[must_use] pub fn swap_with_default(&mut self) -> Self {
342        let mut s = Self::create(StringVec::from_const_slice(&[]));
343        core::mem::swap(&mut s, self);
344        s
345    }
346
347    #[inline]
348    pub fn set_on_step_change<C: Into<StepperOnStepChangeCallback>>(
349        &mut self,
350        data: RefAny,
351        on_step_change: C,
352    ) {
353        self.stepper_state.on_step_change = Some(StepperOnStepChange {
354            callback: on_step_change.into(),
355            refany: data,
356        })
357        .into();
358    }
359
360    #[inline]
361    #[must_use] pub fn with_on_step_change<C: Into<StepperOnStepChangeCallback>>(
362        mut self,
363        data: RefAny,
364        on_step_change: C,
365    ) -> Self {
366        self.set_on_step_change(data, on_step_change);
367        self
368    }
369
370    #[must_use] pub fn dom(self) -> Dom {
371        use azul_core::{
372            callbacks::CoreCallback,
373            dom::{EventFilter, HoverEventFilter},
374            refany::OptionRefAny,
375        };
376
377        let current = self.stepper_state.inner.current_step;
378        let count = self.labels.as_ref().len();
379        let last = count.saturating_sub(1);
380
381        // One shared RefAny across every step's callback (RefAny::clone shares the
382        // underlying state — same pattern as segmented/pagination/map).
383        let state = RefAny::new(self.stepper_state);
384
385        let mut children: Vec<Dom> = Vec::with_capacity(count);
386        for (i, label) in self.labels.as_ref().iter().enumerate() {
387            let reached = i <= current;
388
389            // Indicator row: [connector-left, circle, connector-right].
390            let row = Dom::create_div()
391                .with_ids_and_classes(IdOrClassVec::from_const_slice(STEPPER_ROW_CLASS))
392                .with_css_props(row_style())
393                .with_children(
394                    vec![
395                        Dom::create_div()
396                            .with_ids_and_classes(IdOrClassVec::from_const_slice(
397                                STEPPER_CONNECTOR_CLASS,
398                            ))
399                            .with_css_props(connector_style(conn_left_fill(i, current))),
400                        Dom::create_text(AzString::from(format!("{}", i + 1).as_str()))
401                            .with_ids_and_classes(IdOrClassVec::from_const_slice(
402                                STEPPER_CIRCLE_CLASS,
403                            ))
404                            .with_css_props(circle_style(reached)),
405                        Dom::create_div()
406                            .with_ids_and_classes(IdOrClassVec::from_const_slice(
407                                STEPPER_CONNECTOR_CLASS,
408                            ))
409                            .with_css_props(connector_style(conn_right_fill(i, last, current))),
410                    ]
411                    .into(),
412                );
413
414            let cell = Dom::create_div()
415                .with_ids_and_classes(IdOrClassVec::from_const_slice(STEPPER_STEP_CLASS))
416                .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
417                    STEPPER_STEP_STYLE,
418                ))
419                .with_callbacks(
420                    vec![CoreCallbackData {
421                        event: EventFilter::Hover(HoverEventFilter::MouseUp),
422                        callback: CoreCallback {
423                            cb: on_step_click as usize,
424                            ctx: OptionRefAny::None,
425                        },
426                        refany: state.clone(),
427                    }]
428                    .into(),
429                )
430                .with_tab_index(TabIndex::Auto)
431                .with_children(
432                    vec![
433                        row,
434                        Dom::create_text(label.clone())
435                            .with_ids_and_classes(IdOrClassVec::from_const_slice(
436                                STEPPER_LABEL_CLASS,
437                            ))
438                            .with_css_props(label_style(reached)),
439                    ]
440                    .into(),
441                );
442
443            children.push(cell);
444        }
445
446        Dom::create_div()
447            .with_ids_and_classes(IdOrClassVec::from_const_slice(STEPPER_CLASS))
448            .with_css_props(self.container_style)
449            .with_children(children.into())
450    }
451}
452
453impl Default for Stepper {
454    fn default() -> Self {
455        Self::create(StringVec::from_const_slice(&[]))
456    }
457}
458
459/// Click handler shared by all step cells. Resolves the clicked cell from its
460/// sibling position (= the zero-based step index), and — only if the step
461/// actually changed — updates the state, invokes the user callback, and
462/// live-restyles every circle / connector / label (the segmented pattern).
463extern "C" fn on_step_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
464    use azul_core::dom::DomNodeId;
465
466    let clicked = info.get_hit_node();
467    let Some(parent) = info.get_parent(clicked) else {
468        return Update::DoNothing;
469    };
470
471    // Collect the step cells in document order.
472    let mut cells: Vec<DomNodeId> = Vec::new();
473    let mut cur = info.get_first_child(parent);
474    while let Some(node) = cur {
475        cells.push(node);
476        cur = info.get_next_sibling(node);
477    }
478    let count = cells.len();
479    if count == 0 {
480        return Update::DoNothing;
481    }
482    let last = count - 1;
483
484    let Some(clicked_idx) = cells.iter().position(|n| *n == clicked) else {
485        return Update::DoNothing;
486    };
487
488    let current = {
489        let Some(st) = data.downcast_ref::<StepperStateWrapper>() else {
490            return Update::DoNothing;
491        };
492        st.inner.current_step
493    };
494    if clicked_idx == current {
495        // Clicked the already-current step — no change, no callback.
496        return Update::DoNothing;
497    }
498
499    let result = {
500        let Some(mut st) = data.downcast_mut::<StepperStateWrapper>() else {
501            return Update::DoNothing;
502        };
503        st.inner.current_step = clicked_idx;
504        let inner = st.inner;
505        let st = &mut *st;
506        match st.on_step_change.as_mut() {
507            Some(StepperOnStepChange { callback, refany }) => {
508                (callback.cb)(refany.clone(), info, inner)
509            }
510            None => Update::DoNothing,
511        }
512    };
513
514    // Live-restyle every cell: circle (reached → accent fill + white number),
515    // its two connector half-lines, and its label colour.
516    for (i, cell) in cells.iter().enumerate() {
517        let reached = i <= clicked_idx;
518
519        let Some(row) = info.get_first_child(*cell) else {
520            continue;
521        };
522        let conn_left = info.get_first_child(row);
523        let circle = conn_left.and_then(|cl| info.get_next_sibling(cl));
524        let conn_right = circle.and_then(|c| info.get_next_sibling(c));
525        let label = info.get_next_sibling(row);
526
527        if let Some(circle) = circle {
528            let (bg, text) = if reached {
529                (ACCENT_BG, WHITE)
530            } else {
531                (MUTED_CIRCLE_BG, MUTED_TEXT_COLOR)
532            };
533            info.set_css_property(circle, CssProperty::const_background_content(bg));
534            info.set_css_property(
535                circle,
536                CssProperty::const_text_color(StyleTextColor { inner: text }),
537            );
538        }
539        if let Some(cl) = conn_left {
540            info.set_css_property(
541                cl,
542                CssProperty::const_background_content(conn_left_fill(i, clicked_idx).bg()),
543            );
544        }
545        if let Some(cr) = conn_right {
546            info.set_css_property(
547                cr,
548                CssProperty::const_background_content(conn_right_fill(i, last, clicked_idx).bg()),
549            );
550        }
551        if let Some(label) = label {
552            let text = if reached { DARK_TEXT_COLOR } else { MUTED_TEXT_COLOR };
553            info.set_css_property(
554                label,
555                CssProperty::const_text_color(StyleTextColor { inner: text }),
556            );
557        }
558    }
559
560    result
561}
562
563impl From<Stepper> for Dom {
564    fn from(s: Stepper) -> Self {
565        s.dom()
566    }
567}