Skip to main content

36_reducer/
36_reducer.rs

1//! Example 36: Reducer — Multi-Step Wizard
2//!
3//! A state machine driven by reducer! macro. Each step of the wizard
4//! dispatches actions to advance, go back, or reset.
5//!
6//! Run with: `cargo run -p telex-tui --example 36_reducer`
7
8use crossterm::event::KeyCode;
9use crossterm::style::Color;
10use telex::prelude::*;
11
12telex::require_api!(0, 2);
13
14fn main() {
15    telex::run(App).unwrap();
16}
17
18#[derive(Clone, PartialEq)]
19enum WizardState {
20    Welcome,
21    Name(String),
22    Color(String, String),
23    Done(String, String),
24}
25
26#[derive(Clone)]
27enum WizardAction {
28    Next,
29    Back,
30    SetName(String),
31    SetColor(String),
32    Reset,
33}
34
35struct App;
36
37impl Component for App {
38    fn render(&self, cx: Scope) -> View {
39        let show_help = state!(cx, || false);
40
41        cx.use_command(
42            KeyBinding::key(KeyCode::F(1)),
43            with!(show_help => move || show_help.update(|v| *v = !*v)),
44        );
45
46        let (wizard, dispatch) = reducer!(cx, WizardState::Welcome, |state: WizardState, action: WizardAction| {
47            match (state, action) {
48                (_, WizardAction::Reset) => WizardState::Welcome,
49                (WizardState::Welcome, WizardAction::Next) => WizardState::Name(String::new()),
50                (WizardState::Name(_), WizardAction::SetName(n)) => WizardState::Name(n),
51                (WizardState::Name(name), WizardAction::Next) => {
52                    let name = if name.is_empty() { "Anonymous".to_string() } else { name };
53                    WizardState::Color(name, String::new())
54                }
55                (WizardState::Name(_), WizardAction::Back) => WizardState::Welcome,
56                (WizardState::Color(name, _), WizardAction::SetColor(c)) => WizardState::Color(name, c),
57                (WizardState::Color(name, color), WizardAction::Next) => {
58                    let color = if color.is_empty() { "Blue".to_string() } else { color };
59                    WizardState::Done(name, color)
60                }
61                (WizardState::Color(_, _), WizardAction::Back) => WizardState::Name(String::new()),
62                (WizardState::Done(_, _), WizardAction::Back) => WizardState::Welcome,
63                (s, _) => s,
64            }
65        });
66
67        let step = match &wizard.get() {
68            WizardState::Welcome => 1,
69            WizardState::Name(_) => 2,
70            WizardState::Color(_, _) => 3,
71            WizardState::Done(_, _) => 4,
72        };
73
74        let progress = format!("Step {} of 4", step);
75        let dots: String = (1..=4)
76            .map(|i| if i <= step { "●" } else { "○" })
77            .collect::<Vec<_>>()
78            .join(" ");
79
80        let content = match &wizard.get() {
81            WizardState::Welcome => {
82                let d = dispatch.clone();
83                View::vstack()
84                    .spacing(1)
85                    .child(View::styled_text("Welcome to the Wizard!").color(Color::Cyan).bold().build())
86                    .child(View::text("This example shows centralized state"))
87                    .child(View::text("management with reducer!"))
88                    .child(
89                        View::button()
90                            .label("[ Start -> ]")
91                            .on_press(move || d(WizardAction::Next))
92                            .build(),
93                    )
94                    .build()
95            }
96            WizardState::Name(name) => {
97                let d1 = dispatch.clone();
98                let d2 = dispatch.clone();
99                let d3 = dispatch.clone();
100                View::vstack()
101                    .spacing(1)
102                    .child(View::styled_text("What's your name?").color(Color::Cyan).bold().build())
103                    .child(
104                        View::text_input()
105                            .value(name.clone())
106                            .placeholder("Enter your name...")
107                            .on_change(move |s: String| d1(WizardAction::SetName(s)))
108                            .build(),
109                    )
110                    .child(
111                        View::hstack()
112                            .spacing(1)
113                            .child(
114                                View::button()
115                                    .label("[ <- Back ]")
116                                    .on_press(move || d2(WizardAction::Back))
117                                    .build(),
118                            )
119                            .child(
120                                View::button()
121                                    .label("[ Next -> ]")
122                                    .on_press(move || d3(WizardAction::Next))
123                                    .build(),
124                            )
125                            .build(),
126                    )
127                    .build()
128            }
129            WizardState::Color(name, color) => {
130                let d1 = dispatch.clone();
131                let d2 = dispatch.clone();
132                let d3 = dispatch.clone();
133                View::vstack()
134                    .spacing(1)
135                    .child(View::styled_text(format!("Hi, {}! Pick a color:", name)).color(Color::Cyan).bold().build())
136                    .child(
137                        View::text_input()
138                            .value(color.clone())
139                            .placeholder("Enter a color (e.g. Blue)...")
140                            .on_change(move |s: String| d1(WizardAction::SetColor(s)))
141                            .build(),
142                    )
143                    .child(
144                        View::hstack()
145                            .spacing(1)
146                            .child(
147                                View::button()
148                                    .label("[ <- Back ]")
149                                    .on_press(move || d2(WizardAction::Back))
150                                    .build(),
151                            )
152                            .child(
153                                View::button()
154                                    .label("[ Finish -> ]")
155                                    .on_press(move || d3(WizardAction::Next))
156                                    .build(),
157                            )
158                            .build(),
159                    )
160                    .build()
161            }
162            WizardState::Done(name, color) => {
163                let d = dispatch.clone();
164                View::vstack()
165                    .spacing(1)
166                    .child(View::styled_text("All done!").color(Color::Green).bold().build())
167                    .child(View::text(format!("Name:  {}", name)))
168                    .child(View::text(format!("Color: {}", color)))
169                    .child(
170                        View::button()
171                            .label("[ Start Over ]")
172                            .on_press(move || d(WizardAction::Reset))
173                            .build(),
174                    )
175                    .build()
176            }
177        };
178
179        View::vstack()
180            .spacing(1)
181            .child(View::styled_text("Reducer Wizard").bold().build())
182            .child(
183                View::hstack()
184                    .spacing(1)
185                    .child(View::styled_text(&progress).dim().build())
186                    .child(View::styled_text(&dots).color(Color::Cyan).build())
187                    .build(),
188            )
189            .child(View::styled_text("────────────────────────").dim().build())
190            .child(content)
191            .child(View::styled_text("F1 help • Ctrl+Q quit").dim().build())
192            .child(
193                View::modal()
194                    .visible(show_help.get())
195                    .title("Example 36: Reducer")
196                    .on_dismiss(with!(show_help => move || show_help.set(false)))
197                    .child(
198                        View::vstack()
199                            .child(View::styled_text("What you're seeing").bold().build())
200                            .child(View::text("• Multi-step wizard state machine"))
201                            .child(View::text("• All transitions in one reducer fn"))
202                            .child(View::text("• No scattered booleans"))
203                            .child(View::gap(1))
204                            .child(View::styled_text("Key concepts").bold().build())
205                            .child(View::text("• reducer!(cx, init, |state, action| ...)"))
206                            .child(View::text("• Returns (State<S>, Rc<dyn Fn(A)>)"))
207                            .child(View::text("• dispatch(action) to transition"))
208                            .child(View::text("• Pattern match (state, action) pairs"))
209                            .child(View::gap(1))
210                            .child(View::styled_text("Try this").bold().build())
211                            .child(View::text("• Walk through all 4 steps"))
212                            .child(View::text("• Go back and change answers"))
213                            .child(View::text("• Watch the progress dots"))
214                            .child(View::gap(1))
215                            .child(View::styled_text("Next up").bold().build())
216                            .child(View::text("-> 37_error_boundary: crash protection"))
217                            .child(View::gap(1))
218                            .child(View::styled_text("Press Escape to close").dim().build())
219                            .build(),
220                    )
221                    .build(),
222            )
223            .build()
224    }
225}