1use super::{Button, Steps};
4use crate::event::Event;
5use crate::geometry::{Rect, Size};
6use crate::keymap::Key;
7use crate::widget::{Axis, EventCx, Flex, Length, MeasureCx, Node, NodeMut, PaintCx, View, Widget};
8
9const SECTION_GAP: u16 = 1;
11
12pub struct Wizard<Msg> {
30 labels: Vec<String>,
31 current: usize,
32 on_back: Option<Msg>,
33 on_next: Option<Msg>,
34 on_finish: Option<Msg>,
35 on_cancel: Option<Msg>,
36 on_step: Option<Box<dyn Fn(usize) -> Msg>>,
37 busy: bool,
38 page_height: Option<u16>,
39}
40
41impl<Msg: Clone + 'static> Wizard<Msg> {
42 #[must_use]
44 pub fn new(labels: impl IntoIterator<Item = impl Into<String>>) -> Self {
45 Self {
46 labels: labels.into_iter().map(Into::into).collect(),
47 current: 0,
48 on_back: None,
49 on_next: None,
50 on_finish: None,
51 on_cancel: None,
52 on_step: None,
53 busy: false,
54 page_height: None,
55 }
56 }
57
58 #[must_use]
60 pub fn current(mut self, index: usize) -> Self {
61 self.current = index;
62 self
63 }
64
65 #[must_use]
67 pub fn on_back(mut self, message: Msg) -> Self {
68 self.on_back = Some(message);
69 self
70 }
71
72 #[must_use]
74 pub fn on_next(mut self, message: Msg) -> Self {
75 self.on_next = Some(message);
76 self
77 }
78
79 #[must_use]
81 pub fn on_finish(mut self, message: Msg) -> Self {
82 self.on_finish = Some(message);
83 self
84 }
85
86 #[must_use]
88 pub fn on_cancel(mut self, message: Msg) -> Self {
89 self.on_cancel = Some(message);
90 self
91 }
92
93 #[must_use]
95 pub fn on_step(mut self, message: impl Fn(usize) -> Msg + 'static) -> Self {
96 self.on_step = Some(Box::new(message));
97 self
98 }
99
100 #[must_use]
102 pub fn busy(mut self, busy: bool) -> Self {
103 self.busy = busy;
104 self
105 }
106
107 #[must_use]
109 pub fn page_height(mut self, rows: u16) -> Self {
110 self.page_height = Some(rows);
111 self
112 }
113
114 pub fn show<'v>(self, ui: &'v mut View<'_, Msg>, page: impl FnOnce(&mut View<'_, Msg>)) -> NodeMut<'v, Msg> {
116 let mut children = Vec::new();
117 {
118 let ui = &mut ui.nested(&mut children);
119 let mut steps = Steps::new(self.labels.clone()).current(self.current).running(self.busy);
120 if let Some(message) = self.on_step {
121 steps = steps.on_select(message);
122 }
123 ui.add(steps).id("wizard-steps");
124 let body = ui.column(page).fill_width().id("wizard-page");
125 if let Some(rows) = self.page_height {
126 body.height(Length::Cells(rows));
127 }
128 let last = self.current + 1 >= self.labels.len();
129 let (next_label, next) = if last {
130 (crate::t!("quvyta.wizard.finish"), self.on_finish.clone())
131 } else {
132 (crate::t!("quvyta.wizard.next"), self.on_next.clone())
133 };
134 let busy = self.busy;
135 let (on_back, on_cancel, current) = (self.on_back.clone(), self.on_cancel.clone(), self.current);
136 ui.row(|ui| {
137 if let Some(cancel) = on_cancel {
138 ui.add(Button::new(crate::t!("quvyta.wizard.cancel")).disabled(busy).on_press(cancel))
139 .id("wizard-cancel");
140 }
141 ui.spacer();
142 if current > 0
143 && let Some(back) = on_back
144 {
145 ui.add(Button::new(crate::t!("quvyta.wizard.back")).disabled(busy).on_press(back))
146 .id("wizard-back");
147 }
148 let mut button = Button::new(next_label).variant("primary").loading(busy);
149 if let Some(message) = next {
150 button = button.on_press(message);
151 }
152 ui.add(button).id("wizard-next");
153 })
154 .gap(2)
155 .fill_width();
156 }
157 let mut column = Node::new(Flex::new(Axis::Column, children), 0);
158 column.layout.gap = SECTION_GAP;
159 column.layout.width = Length::Fill(1);
160 ui.add(WizardFrame { body: vec![column], on_cancel: self.on_cancel }).fill_width()
161 }
162}
163
164struct WizardFrame<Msg> {
166 body: Vec<Node<Msg>>,
167 on_cancel: Option<Msg>,
168}
169
170impl<Msg: Clone + 'static> Widget<Msg> for WizardFrame<Msg> {
171 fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
172 self.body.first().map_or(Size::default(), |body| cx.measure_child(body, available))
173 }
174
175 fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
176 if let Some(body) = self.body.first() {
177 cx.paint_child(body, area);
178 }
179 }
180
181 fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
182 match (event, &self.on_cancel) {
183 (Event::Key(key), Some(message)) if key.is_plain(Key::Esc) => {
184 cx.emit(message.clone());
185 true
186 }
187 _ => false,
188 }
189 }
190
191 fn children(&self) -> &[Node<Msg>] {
192 &self.body
193 }
194
195 fn children_mut(&mut self) -> &mut [Node<Msg>] {
196 &mut self.body
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::runtime::{App, Command, Harness};
204 use crate::widgets::{FormErrors, TextInput};
205
206 #[derive(Default)]
207 struct Setup {
208 step: usize,
209 name: String,
210 errors: FormErrors,
211 finished: bool,
212 cancelled: bool,
213 cancellable: bool,
214 }
215
216 #[derive(Clone)]
217 enum Msg {
218 Name(String),
219 Back,
220 Next,
221 Finish,
222 Cancel,
223 }
224
225 impl App for Setup {
226 type Msg = Msg;
227 fn update(&mut self, msg: Msg) -> Command<Msg> {
228 match msg {
229 Msg::Name(name) => self.name = name,
230 Msg::Back => self.step -= 1,
231 Msg::Next => {
232 self.errors.check("name", !self.name.is_empty(), "Name the project");
233 if !self.errors.is_empty() {
234 return self.errors.focus_first();
235 }
236 self.step += 1;
237 }
238 Msg::Finish => self.finished = true,
239 Msg::Cancel => self.cancelled = true,
240 }
241 Command::none()
242 }
243 fn view(&self, ui: &mut View<'_, Msg>) {
244 let mut wizard = Wizard::new(["Project", "Summary"])
245 .current(self.step)
246 .on_back(Msg::Back)
247 .on_next(Msg::Next)
248 .on_finish(Msg::Finish);
249 if self.cancellable {
250 wizard = wizard.on_cancel(Msg::Cancel);
251 }
252 wizard.page_height(2).show(ui, |ui| {
253 if self.step == 0 {
254 ui.add(TextInput::new(&self.name).on_change(Msg::Name)).width(Length::Cells(20)).id("name");
255 } else {
256 ui.add(crate::widgets::Text::new(format!("Create {}", self.name)));
257 }
258 });
259 }
260 }
261
262 #[test]
263 fn next_is_blocked_by_validation_then_advances_and_finishes() {
264 let mut h = Harness::new(Setup::default(), 40, 8);
265 assert_eq!(h.screen(), "● Project ○ Summary\n\n ❯\n\n\n Next\n\n\n");
266 h.click_text("Next");
267 assert_eq!(h.app().step, 0);
268 assert!(h.is_focused("name"), "focus goes to the problem");
269 h.type_text("web");
270 h.click_text("Next");
271 assert_eq!(h.app().step, 1);
272 let screen = h.screen();
273 assert!(screen.contains("✓ Project ● Summary"), "{screen}");
274 assert!(screen.contains("Back ▌ Finish"), "{screen}");
276 h.click_text("Finish");
277 assert!(h.app().finished);
278 h.click_text("Back");
279 assert_eq!(h.app().step, 0);
280 }
281
282 #[test]
283 fn cancel_is_a_button_and_esc_only_when_asked_for() {
284 let mut h = Harness::new(Setup::default(), 40, 8);
285 h.press("tab").press("esc");
286 assert!(!h.app().cancelled);
287 assert!(!h.screen().contains("Cancel"));
288 let mut h = Harness::new(Setup { cancellable: true, ..Setup::default() }, 40, 8);
289 h.press("tab").press("esc");
290 assert!(h.app().cancelled);
291 assert!(h.screen().contains("Cancel"));
292 }
293}