1use 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}