Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Stepper

Stepper preview

Stepper — a modern, embeddable step-flow widget (Material/Ant/Flutter "stepper"), and Wizard, a thin modal launcher built on it.

A stepper shows a visible step-indicator strip above (or beside) a content area driven by a Switcher, with a footer of Back / Skip / Help / Next / Finish controls. It supports linear and non-linear (clickable) navigation, optional + skippable steps, per step validation gating, a generic chrome slot, and a StepperController handle for programmatic reset / jump / introspection.

Data flow

The application owns its form state as Signals. A step's content factory captures clones of those signals (write side); Step::complete_when derives the Next gate from the same signals; and Stepper::on_finish reads them back — plus the StepperController for per-step introspection (visited / skipped) — to branch on the choices made. There is no QVariant field registry: plain shared signals are the cross-step channel.

#[derive(Clone)]
struct Form { name: Signal<String>, plan: Signal<Plan> }
let form = Form { name: Signal::new(String::new()), plan: Signal::new(Plan::Free) };

Stepper::new()
    .step(Step::new(lit!("Account"))
        .content({ let f = form.clone(); move || TextInput::new().text(f.name.clone()) })
        .complete_when(form.name.map(|n| !n.is_empty())))
    .step(Step::new(lit!("Plan"))
        .content({ let f = form.clone(); move || plan_picker(f.plan.clone()) }))
    .on_finish({ let f = form.clone(); move |_ctx, ctrl| {
        match f.plan.get() { Plan::Free => {/* … */} Plan::Pro => {/* … */} }
        let _ = ctrl.skipped(1);
    }});

Builder methods at a glance

step, steps, controller, orientation, vertical, non_linear, circle_size, chrome, chrome_position, back_label, next_label, finish_label, skip_label, help, cancel, on_finish, enter_advances, tooltip, rich_tooltip, rich_tooltip_content, composite_tooltip

API reference

📖 Full rustdoc API for this module

pub enum StepperOrientation

Indicator-strip orientation for a Stepper.

#![allow(unused)]
fn main() {
pub enum StepperOrientation { /* variants */ }
}

Variants

  • Horizontal — Markers in a row, content below (default).
  • Vertical — Markers in a column on the leading side, content beside.

pub enum ChromePosition

Where the optional chrome slot (banner / sidebar) sits relative to the stepper body.

#![allow(unused)]
fn main() {
pub enum ChromePosition { /* variants */ }
}

Variants

  • Leading — Leading column (left in LTR). Forced to Top in vertical orientation.
  • Top — Banner above the stepper body.

pub struct Stepper

An embeddable multi-step flow widget. See the module docs for the data-flow pattern and a usage example.

#![allow(unused)]
fn main() {
pub struct Stepper { /* fields */ }
}

Methods

pub fn new() -> Self

Create an empty Stepper. Append steps with step or steps and provide a finish callback with on_finish.

pub fn step(mut self, step: Step) -> Self

Append a single Step definition.

pub fn steps(mut self, steps: impl IntoIterator<Item = Step>) -> Self

Append multiple Step definitions from an iterator.

pub fn controller(mut self, controller: StepperController) -> Self

Drive the stepper with an externally-held controller (for programmatic reset / jump / introspection). If omitted, the stepper creates its own.

pub fn orientation(mut self, orientation: StepperOrientation) -> Self

Set the indicator-strip orientation (horizontal or vertical).

pub fn vertical(mut self) -> Self

Shorthand for .orientation(StepperOrientation::Vertical).

pub fn non_linear(mut self, non_linear: bool) -> Self

Allow jumping between steps by clicking their indicators (the markers become Role::Tab). Linear (default) markers are Role::ListItem.

pub fn circle_size(mut self, size: f32) -> Self

Override the marker circle diameter (logical px).

pub fn chrome(mut self, chrome: impl Widget + 'static) -> Self

A generic chrome widget (banner / sidebar) — the modern replacement for QWizard's watermark pixmap.

It lands in the leading column by default (ChromePosition::Leading, QWizard's watermark slot), i.e. a full height sidebar. For a title banner pair it with .chrome_position(ChromePosition::Top), or the chrome renders as a wide sidebar holding a few words.

pub fn chrome_position(mut self, position: ChromePosition) -> Self

Choose where the optional chrome widget sits relative to the stepper body. Forced to ChromePosition::Top when orientation is Vertical.

pub fn back_label(mut self, label: impl Into<LocalizedString>) -> Self

Override the "Back" button label. Default: "Back".

pub fn next_label(mut self, label: impl Into<LocalizedString>) -> Self

Override the "Next" button label. Default: "Next".

pub fn finish_label(mut self, label: impl Into<LocalizedString>) -> Self

Override the "Finish" button label. Default: "Finish".

pub fn skip_label(mut self, label: impl Into<LocalizedString>) -> Self

Override the "Skip" button label. Default: "Skip".

pub fn help( mut self, label: impl Into<LocalizedString>, action: impl Fn(&mut EventContext, &StepperController) + 'static, ) -> Self

Add a Help button + callback to the footer.

pub fn cancel( mut self, label: impl Into<LocalizedString>, action: impl Fn(&mut EventContext, &StepperController) + 'static, ) -> Self

Add a Cancel button + callback to the footer.

pub fn on_finish<R: IntoFinishOutcome>( mut self, action: impl Fn(&mut EventContext, &StepperController) -> R + 'static, ) -> Self

Called when Finish is activated on the last step. Receives the event context and the controller (for skipped / visited introspection); read collected values from the form signals your steps wrote.

The callback may refuse. Its return value goes through the IntoFinishOutcome bridge — () always succeeds, while false, Err(_), or FinishOutcome::Rejected keep the stepper on the last step and mark it StepStatus::Error (a Wizard modal stays open). This is the Finish counterpart of Step::validate_on_next — for the case where the commit itself can fail (disk full, name taken, server refused):

.on_finish(move |ctx, _ctrl| match create_project(&name.get()) {
    Ok(()) => true,
    Err(e) => { status.set(e.to_string()); false }
})

pub fn enter_advances(mut self, enter_advances: bool) -> Self

Whether pressing Enter activates the footer's primary button (Next, or Finish on the last step). Default: true.

The key is handled on the bubble pass at the stepper root, so a focused control that wants Enter for itself — a Button, a multi-line editor, a list row — consumes it first and the stepper never sees it. A single-line form field lets it through, which is where the "Enter means Next" contract is expected. Gates apply exactly as they do to a click: a blocked complete_when / validate_on_next refuses the same way.

Turn it off for a step whose body treats Enter as content in a way the framework cannot see.

pub fn tooltip(mut self, text: impl Into<LocalizedString>) -> Self

Attach a plain single-line tooltip to this stepper. Clears any previously set rich or composite tooltip.

pub fn rich_tooltip(mut self, key: impl Into<String>) -> Self

Attach a rich tooltip identified by a registry key. Clears any previously set plain or composite tooltip.

pub fn rich_tooltip_content(mut self, content: crate::tooltip::TooltipContent) -> Self

Attach a rich tooltip with inline content. Clears any previously set plain or composite tooltip.

pub fn composite_tooltip(mut self, content: impl Widget + 'static) -> Self

Attach a composite tooltip (arbitrary widget body). Clears any previously set plain or rich tooltip.

pub struct StepperController

Shared handle controlling a Stepper.

#![allow(unused)]
fn main() {
pub struct StepperController { /* fields */ }
}

Methods

pub fn new(step_count: usize) -> Self

A controller for a stepper with step_count steps, starting at step 0.

pub fn next(&self)

Advance to the next reachable step, recording the current one on the back-stack. Invisible (Step::visible_when) and StepStatus::Disabled steps are stepped over; a no-op when none remains.

pub fn skip(&self)

Mark the current (optional) step skipped, then advance like next.

pub fn back(&self)

Return to the most recently visited reachable step (the back-stack top). Entries that became unreachable meanwhile are popped and skipped. No-op on an empty stack.

pub fn go_to(&self, idx: usize)

Jump to step idx (non-linear), recording the current step on the back-stack so back returns here. A no-op when idx is out of range or not reachable.

pub fn reset(&self)

Reset to the first reachable step: clears the back-stack, restores the statuses the stepper was declared with (so a Disabled / Optional step keeps its character), and clears visited/skipped flags. Per-step visibility is app-owned and left untouched.

pub fn set_status(&self, idx: usize, status: StepStatus)

Override a step's StepStatus (e.g. mark it Error after async validation). Setting StepStatus::Disabled takes the step out of the flow — next / go_to skip it — but does not move off it if it is the active step.

pub fn set_visible(&self, idx: usize, visible: bool)

Show or hide step idx. A hidden step is skipped by next / back / go_to and drops out of the indicator strip — the branching-wizard shape ("this step only if you chose X") without maintaining two step lists.

Usually driven declaratively by Step::visible_when; this is the imperative twin. Hiding the active step does not navigate away from it — hide steps the user has not reached yet.

pub fn current(&self) -> usize

pub fn status(&self, idx: usize) -> StepStatus

pub fn visited(&self, idx: usize) -> bool

true if step idx has ever been the active step.

pub fn skipped(&self, idx: usize) -> bool

true if step idx was skipped via skip.

pub fn is_visible(&self, idx: usize) -> bool

true if step idx is visible (see set_visible).

pub fn is_reachable(&self, idx: usize) -> bool

true if step idx participates in the flow — visible and not StepStatus::Disabled.

pub fn next_reachable(&self, from: usize) -> Option<usize>

The next reachable step after from, if any.

pub fn has_next(&self) -> bool

true if next would move — i.e. the active step is not the last reachable one. The footer shows Next when this holds and Finish when it does not.

pub fn step_count(&self) -> usize

pub fn can_back(&self) -> bool

true if there is a previously-visited, still-reachable step to return to.

pub fn current_step_signal(&self) -> Signal<usize>

The active-step signal — the stepper's Switcher and indicators bind to it.

pub fn version_signal(&self) -> Signal<u64>

Bumped on every structural mutation; bind at BindingLevel::Rebuild.

pub enum StepStatus

Lifecycle state of a single step, surfaced in the indicator strip and (for the active step) as aria-current="step".

Mirrors the modern stepper status model (Ant wait/process/finish/error, Flutter StepState): Upcoming = not yet reached, Active = currently shown, Complete = validated, Error = failed validation, Disabled = unreachable, Optional = reachable but skippable, Skipped = an optional step the user bypassed.

#![allow(unused)]
fn main() {
pub enum StepStatus { /* variants */ }
}

Variants

  • Upcoming
  • Active
  • Complete
  • Error
  • Disabled
  • Optional
  • Skipped

Methods

pub fn is_optional(self) -> bool

true for Optional — the only status that surfaces a Skip button.

pub struct Step

One page in a Stepper.

A step carries a localized title, optional supporting_text, a content factory (the body shown when the step is active), and an optional completion gate. The recommended data-flow pattern: the application owns its form state as Signals, the content factory binds widgets to those signals (write side), and complete_when derives the Next gate from the same signals.

#![allow(unused)]
fn main() {
pub struct Step { /* fields */ }
}

Methods

pub fn new(title: impl Into<LocalizedString>) -> Self

pub fn content<W, F>(mut self, factory: F) -> Self where W: Widget + 'static, F: Fn() -> W + 'static,

The body shown while this step is active. The factory may capture clones of the application's form Signals to read/write step input.

pub fn content_boxed(mut self, factory: impl Fn() -> Box<dyn Widget> + 'static) -> Self

The body shown while this step is active, as a boxed widget — the escape hatch for a body whose concrete type varies at runtime.

content is generic over one W: Widget, and Box<dyn Widget> does not itself implement Widget, so a step whose body branches on app state cannot be expressed as a single content factory. Box each branch instead of duplicating the surrounding builder:

Step::new(lit!("Details")).content_boxed({
    let purpose = purpose.clone();
    move || -> Box<dyn Widget> {
        match purpose.get() {
            Purpose::Novel => Box::new(novel_form()),
            Purpose::Import => Box::new(import_form()),
        }
    }
})

pub fn supporting_text(mut self, text: impl Into<LocalizedString>) -> Self

Secondary line under the title in the header / indicator.

pub fn status(mut self, status: StepStatus) -> Self

Set the step's initial StepStatus.

pub fn optional(mut self, optional: bool) -> Self

Mark the step optional (reachable but skippable — surfaces a Skip button while active). Equivalent to .status(StepStatus::Optional).

pub fn complete_when(mut self, signal: impl Into<teksilo_core::signal::Prop<bool>>) -> Self

Reactive Next gate: while this step is active, Next is enabled iff signal is true. Derive it from the same form signals the step's content writes — e.g. name.map(|n| !n.is_empty()).

pub fn validate_on_next(mut self, f: impl Fn() -> bool + 'static) -> Self

Imperative validation fallback: checked on the Next click. Returning false blocks navigation. Prefer complete_when where a reactive signal is available.

pub fn visible_when(mut self, visible: impl Into<teksilo_core::signal::Prop<bool>>) -> Self

Reactive visibility: while visible is false this step drops out of the flow — Next / Back / indicator clicks skip it, and its marker is hidden from the indicator strip (and from AT).

This is how a branching wizard is expressed: declare every step once and gate the conditional ones on the choice that selects them, instead of maintaining one step list per branch.

let purpose = Signal::new(Purpose::Novel);
Stepper::new()
    .step(Step::new(lit!("Purpose")).content(|| purpose_picker()))
    .step(Step::new(lit!("Import source"))
        .visible_when(purpose.map(|p| *p == Purpose::Import))
        .content(|| import_form()))

Hiding the step the user is currently on does not navigate away from it — gate steps ahead of the choice, not the one making it.