1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use zng_app::event::CommandArgs;
15use zng_ext_input::focus::FOCUS;
16use zng_var::MergeVarBuilder;
17use zng_view_api::keyboard::Key;
18use zng_wgt::prelude::*;
19use zng_wgt_input::{gesture, keyboard};
20use zng_wgt_style::style_fn;
21use zng_wgt_text_input::label;
22
23mod view_fn;
24
25pub use view_fn::*;
26
27#[widget($crate::Wizard)]
29pub struct Wizard(WidgetBase);
30impl Wizard {
31 fn widget_intrinsic(&mut self) {
32 self.widget_builder().push_build_action(|wgt| {
33 let pages = wgt.capture_var_or_default(property_id!(pages));
34 wgt.set_child(node(pages));
35 wgt.push_intrinsic(NestGroup::CONTEXT, "state", |c| {
36 with_context_var(c, GET_TITLE_VAR, var(Txt::from_static("")))
37 });
38 });
39
40 widget_set! {
41 self;
42
43 gesture::mnemonic_scope = true;
45 label::style_fn = style_fn!(|_| {
46 label::DefaultStyle! {
47 label::mnemonic_underline = true;
48 zng_wgt_text::underline = 1, LineStyle::Solid;
49 }
50 });
51 keyboard::on_key_up = hn!(|args| {
52 if let Key::Char(c) = args.key
53 && c.is_alphanumeric()
54 && !FOCUS.is_highlighting().get()
55 {
56 FOCUS.highlight();
58 args.propagation.stop();
59 }
60 });
61 }
62 }
63}
64
65context_var! {
66 static GET_TITLE_VAR: Txt = Txt::from_static("");
67}
68
69#[property(CHILD, widget_impl(Wizard))]
74pub fn pages(wgt: &mut WidgetBuilding, pages: impl IntoVar<Vec<Page>>) {
75 let _ = pages;
76 wgt.expect_property_capture();
77}
78
79#[property(CONTEXT, widget_impl(Wizard))]
81pub fn get_title(child: impl IntoUiNode, title: impl IntoVar<Txt>) -> UiNode {
82 bind_state(child, GET_TITLE_VAR, title)
83}
84
85#[non_exhaustive]
95#[derive(Clone, Debug, PartialEq)]
96pub struct Page {
97 pub title: VarEq<Txt>,
101
102 pub info: VarEq<Txt>,
108
109 pub header: WidgetFn<PageArgs>,
120 pub side: WidgetFn<PageArgs>,
131 pub content: WidgetFn<PageArgs>,
135
136 pub content_fill: bool,
144
145 pub footer: WidgetFn<PageArgs>,
153
154 pub skip: VarEq<bool>,
158
159 pub can_back: VarEq<bool>,
163
164 pub can_next: VarEq<bool>,
166}
167
168impl Page {
169 pub fn new(title: impl IntoVar<Txt>, info: impl IntoVar<Txt>, content: WidgetFn<PageArgs>) -> Self {
171 Self {
172 title: VarEq(title.into_var()),
173 info: VarEq(info.into_var()),
174 header: WidgetFn::new(default_page_header),
175 side: WidgetFn::new(default_page_side),
176 content,
177 content_fill: false,
178 footer: WidgetFn::new(default_page_footer),
179 skip: VarEq(var(false)),
180 can_back: VarEq(var(true)),
181 can_next: VarEq(var(true)),
182 }
183 }
184}
185#[non_exhaustive]
187#[derive(Clone)]
188pub struct PageArgs {
189 pub index: usize,
191 pub pages_len: usize,
193 pub title: Var<Txt>,
195 pub info: Var<Txt>,
197 pub can_back: Var<bool>,
199 pub can_next: Var<bool>,
201}
202impl PageArgs {
203 pub fn is_first(&self) -> bool {
205 self.index == 0
206 }
207
208 pub fn is_last(&self) -> bool {
210 self.index == self.pages_len.saturating_sub(1)
211 }
212
213 pub fn wizard_id(&self) -> WidgetId {
215 WIDGET.id()
216 }
217}
218
219command! {
220 pub static BACK_CMD {
222 l10n!: true,
223 name: "Back",
224 };
225
226 pub static NEXT_CMD {
228 l10n!: true,
229 name: "Next",
230 };
231
232 pub static CANCEL_CMD {
234 l10n!: true,
235 name: "Cancel",
236 };
237
238 pub static FINISH_CMD {
246 l10n!: true,
247 name: "Finish",
248 };
249}
250command_property! {
251 #[property(EVENT, widget_impl(Wizard))]
253 pub fn on_cancel<on_pre_cancel, can_cancel>(child: impl IntoUiNode, handler: Handler<CommandArgs>) -> UiNode {
254 CANCEL_CMD
255 }
256
257 #[property(EVENT, widget_impl(Wizard))]
259 pub fn on_finish<on_pre_finish, can_finish>(child: impl IntoUiNode, handler: Handler<CommandArgs>) -> UiNode {
260 FINISH_CMD
261 }
262}
263
264#[property(CONTEXT, widget_impl(Wizard))]
266pub fn finish_cmd_name(child: impl IntoUiNode, name: impl IntoVar<Txt>) -> UiNode {
267 let name = name.into_var();
268 match_node(child, move |_, op| {
269 if let UiNodeOp::Init = op {
270 let finish_name = FINISH_CMD.scoped(WIDGET.id()).name();
271 let h = name.set_bind(&finish_name);
272 WIDGET.push_var_handle(h);
273 }
274 })
275}
276
277fn node(pages: Var<Vec<Page>>) -> UiNode {
278 let mut cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
279 let mut selected_page = 0usize;
280 let mut get_title = VarHandle::dummy();
281 match_node(UiNode::nil(), move |c, op| match op {
282 UiNodeOp::Init => {
283 WIDGET
284 .sub_var(&pages)
285 .sub_var(&PANEL_FN_VAR)
286 .sub_var(&HEADER_FN_VAR)
287 .sub_var(&HEADER_BACKGROUND_FN_VAR)
288 .sub_var(&SIDE_FN_VAR)
289 .sub_var(&SIDE_BACKGROUND_FN_VAR)
290 .sub_var(&SIDE_EXTRA_FN_VAR)
291 .sub_var(&CONTENT_FN_VAR)
292 .sub_var(&FOOTER_FN_VAR)
293 .sub_var(&FOOTER_EXTRA_FN_VAR);
294 pages.with(|p| {
295 if !p.is_empty() {
296 cmds = subscribe(0, p);
297 *c.node() = build(0, p);
298 get_title = p[0].title.set_bind(&GET_TITLE_VAR);
299 }
300 });
301 }
302 UiNodeOp::Deinit => {
303 c.deinit();
304 *c.node() = UiNode::nil();
305 cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
306 selected_page = 0;
307 get_title = VarHandle::dummy();
308 }
309 UiNodeOp::Update { updates } => {
310 c.update(updates);
311
312 let mut rebuild = false;
313 let id = WIDGET.id();
314 BACK_CMD.scoped(id).each_update(true, false, |args| {
315 while selected_page > 0 {
316 selected_page -= 1;
317 if !pages.with(|p| p[selected_page].skip.get()) {
318 rebuild = true;
319 break;
320 }
321 }
322 args.propagation.stop();
323 });
324 NEXT_CMD.scoped(id).each_update(true, false, |args| {
325 let last = pages.with(|p| p.len()).saturating_sub(1);
326 while selected_page < last {
327 selected_page += 1;
328 if !pages.with(|p| p[selected_page].skip.get()) {
329 rebuild = true;
330 break;
331 }
332 }
333 args.propagation.stop();
334 });
335
336 if pages.is_new() {
337 selected_page = 0;
338 rebuild = true;
339 } else if PANEL_FN_VAR.is_new()
340 || HEADER_FN_VAR.is_new()
341 || HEADER_BACKGROUND_FN_VAR.is_new()
342 || SIDE_FN_VAR.is_new()
343 || SIDE_BACKGROUND_FN_VAR.is_new()
344 || SIDE_EXTRA_FN_VAR.is_new()
345 || CONTENT_FN_VAR.is_new()
346 || FOOTER_FN_VAR.is_new()
347 || FOOTER_EXTRA_FN_VAR.is_new()
348 {
349 rebuild = true;
350 }
351
352 if rebuild {
353 c.deinit();
354 pages.with(|p| {
355 if !p.is_empty() {
356 cmds = subscribe(selected_page, p);
357 *c.node() = build(selected_page, p);
358 get_title = p[selected_page].title.set_bind(&GET_TITLE_VAR);
359 c.init();
360 } else {
361 cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
362 *c.node() = UiNode::nil();
363 get_title = VarHandle::dummy();
364 }
365 });
366 WIDGET.update_info().layout().render();
367 }
368 }
369 _ => {}
370 })
371}
372
373fn subscribe(index: usize, pages: &[Page]) -> [CommandHandle; 2] {
374 let id = WIDGET.id();
375 let cmds = [BACK_CMD.scoped(id).subscribe(false), NEXT_CMD.scoped(id).subscribe(false)];
376
377 let mut flags = MergeVarBuilder::new();
378 for p in pages {
379 flags.push(p.skip.0.clone());
380 }
381 flags.push(pages[index].can_back.0.clone());
382 flags.push(pages[index].can_next.0.clone());
383 let skips_len = pages.len();
384 let can_dos = flags.build(move |flags| {
385 let mut can_back = false;
386 if flags.get(skips_len) {
387 for i in 0..index {
389 can_back = !flags.get(i);
390 if can_back {
391 break;
393 }
394 }
395 }
396 let mut can_next = false;
397 if flags.get(skips_len + 1) {
398 for i in index + 1..skips_len {
400 can_next = !flags.get(i);
401 if can_next {
402 break;
404 }
405 }
406 }
407 [can_back, can_next]
408 });
409
410 can_dos.set_bind_map(cmds[0].enabled(), |[b, _]| *b).perm();
411 can_dos.set_bind_map(cmds[1].enabled(), |[_, n]| *n).perm();
412 cmds[0].enabled().hold(can_dos).perm();
413
414 cmds
415}
416fn build(index: usize, pages: &[Page]) -> UiNode {
417 let page = &pages[index];
418 let args = PageArgs {
419 index,
420 pages_len: pages.len(),
421 title: page.title.0.clone(),
422 info: page.info.0.clone(),
423 can_back: page.can_back.0.clone(),
424 can_next: page.can_next.0.clone(),
425 };
426 let header = (page.header)(args.clone());
427 let side = (page.side)(args.clone());
428 let content = (page.content)(args.clone());
429 let footer = (page.footer)(args.clone());
430
431 let header = if header.is_nil() {
432 header
433 } else {
434 let background = HEADER_BACKGROUND_FN_VAR.get()(());
435 HEADER_FN_VAR.get()(HeaderFnArgs {
436 header,
437 background,
438 index,
439 pages_len: pages.len(),
440 titles: pages.iter().map(|p| p.title.0.clone()).collect(),
441 skips: pages.iter().map(|p| p.skip.0.clone()).collect(),
442 })
443 };
444 let side = if side.is_nil() {
445 side
446 } else {
447 let background = SIDE_BACKGROUND_FN_VAR.get()(());
448 let side_extra = SIDE_EXTRA_FN_VAR.get()(args.clone());
449 SIDE_FN_VAR.get()(SideFnArgs {
450 side,
451 background,
452 side_extra,
453 index,
454 pages_len: pages.len(),
455 })
456 };
457 let content = CONTENT_FN_VAR.get()(ContentFnArgs {
458 content,
459 content_fill: page.content_fill,
460 index,
461 pages_len: pages.len(),
462 });
463 let footer_extra = FOOTER_EXTRA_FN_VAR.get()(args);
464 let footer = FOOTER_FN_VAR.get()(FooterFnArgs {
465 footer,
466 footer_extra,
467 index,
468 pages_len: pages.len(),
469 });
470
471 PANEL_FN_VAR.get()(PanelFnArgs {
472 header,
473 side,
474 content,
475 footer,
476 })
477}