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 std::any::Any;
15
16use zng_app::event::CommandArgs;
17use zng_ext_input::focus::FOCUS;
18use zng_var::MergeVarBuilder;
19use zng_view_api::keyboard::Key;
20use zng_wgt::prelude::*;
21use zng_wgt_input::{gesture, keyboard};
22use zng_wgt_style::style_fn;
23use zng_wgt_text_input::label;
24
25mod view_fn;
26
27pub use view_fn::*;
28
29#[widget($crate::Wizard)]
31pub struct Wizard(WidgetBase);
32impl Wizard {
33 fn widget_intrinsic(&mut self) {
34 self.widget_builder().push_build_action(|wgt| {
35 let pages = wgt.capture_var_or_default(property_id!(pages));
36 wgt.set_child(node(pages));
37 wgt.push_intrinsic(NestGroup::CONTEXT, "state", |c| {
38 let c = with_context_var(c, SELECTED_PAGE_VAR, var(Page::nil()));
39
40 zng_wgt::node::with_context_local_init(c, &WIZARD_CTX, || WizardCtx { id: WIDGET.id() })
41 });
42 });
43
44 widget_set! {
45 self;
46
47 gesture::mnemonic_scope = true;
49 label::style_fn = style_fn!(|_| {
50 label::DefaultStyle! {
51 label::mnemonic_underline = true;
52 zng_wgt_text::underline = 1, LineStyle::Solid;
53 }
54 });
55 keyboard::on_key_up = hn!(|args| {
56 if let Key::Char(c) = args.key
57 && c.is_alphanumeric()
58 && !FOCUS.is_highlighting().get()
59 {
60 FOCUS.highlight();
62 args.propagation.stop();
63 }
64 });
65 }
66 }
67}
68
69context_var! {
70 static SELECTED_PAGE_VAR: Page = Page::nil();
71}
72context_local! {
73 static WIZARD_CTX: WizardCtx = WizardCtx::no_context();
74}
75struct WizardCtx {
76 id: WidgetId,
77}
78impl WizardCtx {
79 fn no_context() -> Self {
80 panic!("no `Wizard!` in context")
81 }
82}
83
84#[property(CHILD, widget_impl(Wizard))]
89pub fn pages(wgt: &mut WidgetBuilding, pages: impl IntoVar<Vec<Page>>) {
90 let _ = pages;
91 wgt.expect_property_capture();
92}
93
94#[property(CONTEXT, widget_impl(Wizard))]
96pub fn get_title(child: impl IntoUiNode, title: impl IntoVar<Txt>) -> UiNode {
97 bind_state(child, SELECTED_PAGE_VAR.flat_map(|p| p.title.0.clone()), title)
98}
99
100#[non_exhaustive]
110#[derive(Clone, Debug, PartialEq)]
111pub struct Page {
112 pub title: VarEq<Txt>,
116
117 pub info: VarEq<Txt>,
123
124 pub header: WidgetFn<PageArgs>,
135 pub side: WidgetFn<PageArgs>,
146 pub content: WidgetFn<PageArgs>,
150
151 pub content_fill: bool,
159
160 pub footer: WidgetFn<PageArgs>,
168
169 pub skip: VarEq<bool>,
173
174 pub can_back: VarEq<bool>,
178
179 pub can_next: VarEq<bool>,
181}
182
183impl Page {
184 pub fn new(title: impl IntoVar<Txt>, info: impl IntoVar<Txt>, content: WidgetFn<PageArgs>) -> Self {
186 Self {
187 title: VarEq(title.into_var()),
188 info: VarEq(info.into_var()),
189 header: WidgetFn::new(default_page_header),
190 side: WidgetFn::new(default_page_side),
191 content,
192 content_fill: false,
193 footer: WidgetFn::new(default_page_footer),
194 skip: VarEq(var(false)),
195 can_back: VarEq(var(true)),
196 can_next: VarEq(var(true)),
197 }
198 }
199
200 pub fn nil() -> Self {
202 Self {
203 title: VarEq(const_var(Txt::from_static(""))),
204 info: VarEq(const_var(Txt::from_static(""))),
205 header: WidgetFn::nil(),
206 side: WidgetFn::nil(),
207 content: WidgetFn::nil(),
208 content_fill: false,
209 footer: WidgetFn::nil(),
210 skip: VarEq(const_var(true)),
211 can_back: VarEq(const_var(false)),
212 can_next: VarEq(const_var(false)),
213 }
214 }
215
216 pub fn is_nil(&self) -> bool {
220 self.header.is_nil()
221 && self.side.is_nil()
222 && self.content.is_nil()
223 && self.footer.is_nil()
224 && !self.content_fill
225 && self.title.0.capabilities().is_const()
226 && self.info.0.capabilities().is_const()
227 && self.skip.0.capabilities().is_const()
228 && self.can_back.0.capabilities().is_const()
229 && self.can_next.0.capabilities().is_const()
230 && self.title.0.with(|t| t.as_static_str() == Some(""))
231 && self.info.0.with(|t| t.as_static_str() == Some(""))
232 && self.skip.0.get()
233 && !self.can_back.0.get()
234 && !self.can_next.0.get()
235 }
236}
237#[non_exhaustive]
239#[derive(Clone)]
240pub struct PageArgs {
241 pub index: usize,
245 pub pages_len: usize,
247 pub title: Var<Txt>,
249 pub info: Var<Txt>,
251 pub can_back: Var<bool>,
253 pub can_next: Var<bool>,
255}
256impl PageArgs {
257 pub fn is_first(&self) -> bool {
259 self.index == 0
260 }
261
262 pub fn is_last(&self) -> bool {
264 self.index == self.pages_len.saturating_sub(1)
265 }
266
267 pub fn is_custom(&self) -> bool {
269 self.index == usize::MAX
270 }
271
272 pub fn wizard_id(&self) -> WidgetId {
274 WIDGET.id()
275 }
276}
277
278command! {
279 pub static BACK_CMD {
281 l10n!: true,
282 name: "Back",
283 };
284
285 pub static NEXT_CMD {
287 l10n!: true,
288 name: "Next",
289 };
290
291 pub static CANCEL_CMD {
293 l10n!: true,
294 name: "Cancel",
295 };
296
297 pub static BEGIN_CMD {
306 l10n!: true,
307 name: "Begin",
308 };
309
310 pub static FINISH_CMD {
325 l10n!: true,
326 name: "Finish",
327 };
328}
329command_property! {
330 #[property(EVENT, widget_impl(Wizard))]
332 pub fn on_cancel<on_pre_cancel, can_cancel>(child: impl IntoUiNode, handler: Handler<CommandArgs>) -> UiNode {
333 CANCEL_CMD
334 }
335
336 #[property(EVENT, widget_impl(Wizard))]
338 pub fn on_begin<on_pre_begin, can_begin>(child: impl IntoUiNode, handler: Handler<CommandArgs>) -> UiNode {
339 BEGIN_CMD
340 }
341
342 #[property(EVENT, widget_impl(Wizard))]
344 pub fn on_finish<on_pre_finish, can_finish>(child: impl IntoUiNode, handler: Handler<CommandArgs>) -> UiNode {
345 FINISH_CMD
346 }
347}
348
349#[property(CONTEXT, widget_impl(Wizard))]
351pub fn begin_cmd_name(child: impl IntoUiNode, name: impl IntoVar<Txt>) -> UiNode {
352 let name = name.into_var();
353 match_node(child, move |_, op| {
354 if let UiNodeOp::Init = op {
355 let begin_name = BEGIN_CMD.scoped(WIDGET.id()).name();
356 let h = name.set_bind(&begin_name);
357 WIDGET.push_var_handle(h);
358 }
359 })
360}
361
362#[property(CONTEXT, widget_impl(Wizard))]
364pub fn finish_cmd_name(child: impl IntoUiNode, name: impl IntoVar<Txt>) -> UiNode {
365 let name = name.into_var();
366 match_node(child, move |_, op| {
367 if let UiNodeOp::Init = op {
368 let finish_name = FINISH_CMD.scoped(WIDGET.id()).name();
369 let h = name.set_bind(&finish_name);
370 WIDGET.push_var_handle(h);
371 }
372 })
373}
374
375fn node(pages: Var<Vec<Page>>) -> UiNode {
376 let mut cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
377 let mut sel_pg_i = 0usize;
378 const CUSTOM_PAGE_I: usize = usize::MAX;
379 match_node(UiNode::nil(), move |c, op| match op {
380 UiNodeOp::Init => {
381 WIDGET
382 .sub_var(&pages)
383 .sub_var(&PANEL_FN_VAR)
384 .sub_var(&HEADER_FN_VAR)
385 .sub_var(&HEADER_BACKGROUND_FN_VAR)
386 .sub_var(&SIDE_FN_VAR)
387 .sub_var(&SIDE_BACKGROUND_FN_VAR)
388 .sub_var(&SIDE_EXTRA_FN_VAR)
389 .sub_var(&CONTENT_FN_VAR)
390 .sub_var(&FOOTER_FN_VAR)
391 .sub_var(&FOOTER_EXTRA_FN_VAR)
392 .sub_var(&SELECTED_PAGE_VAR);
393 pages.with(|p| {
394 if !p.is_empty() {
395 sel_pg_i = 0;
396 cmds = subscribe(0, p);
397 *c.node() = build(0, p, None);
398 SELECTED_PAGE_VAR.set(p[0].clone());
399 }
400 });
401 }
402 UiNodeOp::Deinit => {
403 c.deinit();
404 *c.node() = UiNode::nil();
405 cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
406 SELECTED_PAGE_VAR.set(Page::nil());
407 }
408 UiNodeOp::Update { updates } => {
409 c.update(updates);
410
411 let mut rebuild = false;
412
413 if sel_pg_i != CUSTOM_PAGE_I {
414 let scope = WIDGET.id();
417
418 BACK_CMD.scoped(scope).each_update(true, false, |args| {
419 args.propagation.stop();
420
421 pages.with(|pages| {
423 for (i, pg) in pages[..sel_pg_i].iter().enumerate().rev() {
424 if !pg.skip.get() {
425 sel_pg_i = i;
426 rebuild = true;
427 break;
428 }
429 }
430 });
431 });
432
433 NEXT_CMD.scoped(scope).each_update(true, false, |args| {
434 args.propagation.stop();
435
436 pages.with(|pages| {
438 for (i, pg) in pages.iter().enumerate().skip(sel_pg_i + 1) {
439 if !pg.skip.get() {
440 sel_pg_i = i;
441 rebuild = true;
442 break;
443 }
444 }
445 });
446 });
447 }
448 if pages.is_new() {
449 sel_pg_i = 0;
450 rebuild = true;
451 } else if let Some(new) = SELECTED_PAGE_VAR.get_new() {
452 pages.with(|pages| {
453 if let Some(i) = pages.iter().position(|p| p == &new) {
454 if i != sel_pg_i {
455 sel_pg_i = i;
457 rebuild = true;
458 }
459 } else {
461 sel_pg_i = CUSTOM_PAGE_I;
463 rebuild = true;
464 }
465 })
466 }
467 if !rebuild && PANEL_FN_VAR.is_new()
468 || HEADER_FN_VAR.is_new()
469 || HEADER_BACKGROUND_FN_VAR.is_new()
470 || SIDE_FN_VAR.is_new()
471 || SIDE_BACKGROUND_FN_VAR.is_new()
472 || SIDE_EXTRA_FN_VAR.is_new()
473 || CONTENT_FN_VAR.is_new()
474 || FOOTER_FN_VAR.is_new()
475 || FOOTER_EXTRA_FN_VAR.is_new()
476 {
477 rebuild = true;
478 }
479
480 if rebuild {
481 c.deinit();
483 WIDGET.update_info().layout().render();
484
485 pages.with(|pages| {
486 if sel_pg_i < pages.len() {
487 SELECTED_PAGE_VAR.set(pages[sel_pg_i].clone());
490
491 cmds = subscribe(sel_pg_i, pages);
493 *c.node() = build(sel_pg_i, pages, None);
495 c.init();
496 } else {
497 cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
501
502 if sel_pg_i == CUSTOM_PAGE_I {
503 SELECTED_PAGE_VAR.with(|pg| {
505 *c.node() = build(CUSTOM_PAGE_I, pages, Some(pg));
506 });
507 c.init();
508 } else {
509 if !pages.is_empty() {
511 tracing::error!("invalid page selection, {} in {}", sel_pg_i, pages.len());
512 }
513 *c.node() = UiNode::nil();
514 SELECTED_PAGE_VAR.set(Page::nil());
515 }
516 }
517 });
518 }
519 }
520 _ => {}
521 })
522}
523
524fn subscribe(index: usize, pages: &[Page]) -> [CommandHandle; 2] {
525 let id = WIDGET.id();
526 let cmds = [BACK_CMD.scoped(id).subscribe(false), NEXT_CMD.scoped(id).subscribe(false)];
527
528 let mut flags = MergeVarBuilder::new();
529 for p in pages {
530 flags.push(p.skip.0.clone());
531 }
532 flags.push(pages[index].can_back.0.clone());
533 flags.push(pages[index].can_next.0.clone());
534 let skips_len = pages.len();
535 let can_dos = flags.build(move |flags| {
536 let mut can_back = false;
537 if flags.get(skips_len) {
538 for i in 0..index {
540 can_back = !flags.get(i);
541 if can_back {
542 break;
544 }
545 }
546 }
547 let mut can_next = false;
548 if flags.get(skips_len + 1) {
549 for i in index + 1..skips_len {
551 can_next = !flags.get(i);
552 if can_next {
553 break;
555 }
556 }
557 }
558 [can_back, can_next]
559 });
560
561 can_dos.set_bind_map(cmds[0].enabled(), |[b, _]| *b).perm();
562 can_dos.set_bind_map(cmds[1].enabled(), |[_, n]| *n).perm();
563 cmds[0].enabled().hold(can_dos).perm();
564
565 cmds
566}
567fn build(index: usize, pages: &[Page], custom_page: Option<&Page>) -> UiNode {
568 let page = custom_page.unwrap_or_else(|| &pages[index]);
569 let args = PageArgs {
570 index,
571 pages_len: pages.len(),
572 title: page.title.0.clone(),
573 info: page.info.0.clone(),
574 can_back: page.can_back.0.clone(),
575 can_next: page.can_next.0.clone(),
576 };
577 let header = (page.header)(args.clone());
578 let side = (page.side)(args.clone());
579 let content = (page.content)(args.clone());
580 let footer = (page.footer)(args.clone());
581
582 let header = if header.is_nil() {
583 header
584 } else {
585 let background = HEADER_BACKGROUND_FN_VAR.get()(());
586 HEADER_FN_VAR.get()(HeaderFnArgs {
587 header,
588 background,
589 index,
590 pages_len: pages.len(),
591 titles: pages.iter().map(|p| p.title.0.clone()).collect(),
592 skips: pages.iter().map(|p| p.skip.0.clone()).collect(),
593 })
594 };
595 let side = if side.is_nil() {
596 side
597 } else {
598 let background = SIDE_BACKGROUND_FN_VAR.get()(());
599 let side_extra = SIDE_EXTRA_FN_VAR.get()(args.clone());
600 SIDE_FN_VAR.get()(SideFnArgs {
601 side,
602 background,
603 side_extra,
604 index,
605 pages_len: pages.len(),
606 })
607 };
608 let content = CONTENT_FN_VAR.get()(ContentFnArgs {
609 content,
610 content_fill: page.content_fill,
611 index,
612 pages_len: pages.len(),
613 });
614 let footer_extra = FOOTER_EXTRA_FN_VAR.get()(args);
615 let footer = FOOTER_FN_VAR.get()(FooterFnArgs {
616 footer,
617 footer_extra,
618 index,
619 pages_len: pages.len(),
620 });
621
622 PANEL_FN_VAR.get()(PanelFnArgs {
623 header,
624 side,
625 content,
626 footer,
627 })
628}
629
630pub struct WIZARD;
632impl WIZARD {
633 pub fn try_id(&self) -> Option<WidgetId> {
635 if WIZARD_CTX.is_default() { None } else { Some(WIZARD_CTX.get().id) }
636 }
637 pub fn id(&self) -> WidgetId {
643 WIZARD_CTX.get().id
644 }
645
646 pub fn back(&self) {
648 BACK_CMD.scoped(self.id()).notify();
649 }
650
651 pub fn next(&self) {
653 NEXT_CMD.scoped(self.id()).notify();
654 }
655
656 pub fn begin(&self) {
658 BEGIN_CMD.scoped(self.id()).notify();
659 }
660
661 pub fn begin_param(&self, param: impl Any + Send + Sync) {
663 BEGIN_CMD.scoped(self.id()).notify_param(param);
664 }
665
666 pub fn finish(&self) {
668 FINISH_CMD.scoped(self.id()).notify();
669 }
670
671 pub fn finish_param(&self, param: impl Any + Send + Sync) {
673 FINISH_CMD.scoped(self.id()).notify_param(param);
674 }
675
676 pub fn selected_page(&self) -> Var<Page> {
678 SELECTED_PAGE_VAR.into_var()
679 }
680}