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 pub footer: WidgetFn<PageArgs>,
143
144 pub skip: VarEq<bool>,
148
149 pub can_back: VarEq<bool>,
153
154 pub can_next: VarEq<bool>,
156}
157
158impl Page {
159 pub fn new(title: impl IntoVar<Txt>, info: impl IntoVar<Txt>, content: WidgetFn<PageArgs>) -> Self {
161 Self {
162 title: VarEq(title.into_var()),
163 info: VarEq(info.into_var()),
164 header: WidgetFn::new(default_page_header),
165 side: WidgetFn::new(default_page_side),
166 content,
167 footer: WidgetFn::new(default_page_footer),
168 skip: VarEq(var(false)),
169 can_back: VarEq(var(true)),
170 can_next: VarEq(var(true)),
171 }
172 }
173}
174#[non_exhaustive]
176#[derive(Clone)]
177pub struct PageArgs {
178 pub index: usize,
180 pub pages_len: usize,
182 pub title: Var<Txt>,
184 pub info: Var<Txt>,
186 pub can_back: Var<bool>,
188 pub can_next: Var<bool>,
190}
191impl PageArgs {
192 pub fn is_first(&self) -> bool {
194 self.index == 0
195 }
196
197 pub fn is_last(&self) -> bool {
199 self.index == self.pages_len.saturating_sub(1)
200 }
201
202 pub fn wizard_id(&self) -> WidgetId {
204 WIDGET.id()
205 }
206}
207
208command! {
209 pub static BACK_CMD {
211 l10n!: true,
212 name: "Back",
213 };
214
215 pub static NEXT_CMD {
217 l10n!: true,
218 name: "Next",
219 };
220
221 pub static CANCEL_CMD {
223 l10n!: true,
224 name: "Cancel",
225 };
226
227 pub static FINISH_CMD {
235 l10n!: true,
236 name: "Finish",
237 };
238}
239command_property! {
240 #[property(EVENT, widget_impl(Wizard))]
242 pub fn on_cancel<on_pre_cancel, can_cancel>(child: impl IntoUiNode, handler: Handler<CommandArgs>) -> UiNode {
243 CANCEL_CMD
244 }
245
246 #[property(EVENT, widget_impl(Wizard))]
248 pub fn on_finish<on_pre_finish, can_finish>(child: impl IntoUiNode, handler: Handler<CommandArgs>) -> UiNode {
249 FINISH_CMD
250 }
251}
252
253#[property(CONTEXT, widget_impl(Wizard))]
255pub fn finish_cmd_name(child: impl IntoUiNode, name: impl IntoVar<Txt>) -> UiNode {
256 let name = name.into_var();
257 match_node(child, move |_, op| {
258 if let UiNodeOp::Init = op {
259 let finish_name = FINISH_CMD.scoped(WIDGET.id()).name();
260 let h = name.set_bind(&finish_name);
261 WIDGET.push_var_handle(h);
262 }
263 })
264}
265
266fn node(pages: Var<Vec<Page>>) -> UiNode {
267 let mut cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
268 let mut selected_page = 0usize;
269 let mut get_title = VarHandle::dummy();
270 match_node(UiNode::nil(), move |c, op| match op {
271 UiNodeOp::Init => {
272 WIDGET
273 .sub_var(&pages)
274 .sub_var(&PANEL_FN_VAR)
275 .sub_var(&HEADER_FN_VAR)
276 .sub_var(&HEADER_BACKGROUND_FN_VAR)
277 .sub_var(&SIDE_FN_VAR)
278 .sub_var(&SIDE_BACKGROUND_FN_VAR)
279 .sub_var(&SIDE_EXTRA_FN_VAR)
280 .sub_var(&CONTENT_FN_VAR)
281 .sub_var(&FOOTER_FN_VAR)
282 .sub_var(&FOOTER_EXTRA_FN_VAR);
283 pages.with(|p| {
284 if !p.is_empty() {
285 cmds = subscribe(0, p);
286 *c.node() = build(0, p);
287 get_title = p[0].title.set_bind(&GET_TITLE_VAR);
288 }
289 });
290 }
291 UiNodeOp::Deinit => {
292 c.deinit();
293 *c.node() = UiNode::nil();
294 cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
295 selected_page = 0;
296 get_title = VarHandle::dummy();
297 }
298 UiNodeOp::Update { updates } => {
299 c.update(updates);
300
301 let mut rebuild = false;
302 let id = WIDGET.id();
303 BACK_CMD.scoped(id).each_update(true, false, |args| {
304 while selected_page > 0 {
305 selected_page -= 1;
306 if !pages.with(|p| p[selected_page].skip.get()) {
307 rebuild = true;
308 break;
309 }
310 }
311 args.propagation.stop();
312 });
313 NEXT_CMD.scoped(id).each_update(true, false, |args| {
314 let last = pages.with(|p| p.len()).saturating_sub(1);
315 while selected_page < last {
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
325 if pages.is_new() {
326 selected_page = 0;
327 rebuild = true;
328 } else if PANEL_FN_VAR.is_new()
329 || HEADER_FN_VAR.is_new()
330 || HEADER_BACKGROUND_FN_VAR.is_new()
331 || SIDE_FN_VAR.is_new()
332 || SIDE_BACKGROUND_FN_VAR.is_new()
333 || SIDE_EXTRA_FN_VAR.is_new()
334 || CONTENT_FN_VAR.is_new()
335 || FOOTER_FN_VAR.is_new()
336 || FOOTER_EXTRA_FN_VAR.is_new()
337 {
338 rebuild = true;
339 }
340
341 if rebuild {
342 c.deinit();
343 pages.with(|p| {
344 if !p.is_empty() {
345 cmds = subscribe(selected_page, p);
346 *c.node() = build(selected_page, p);
347 get_title = p[selected_page].title.set_bind(&GET_TITLE_VAR);
348 c.init();
349 } else {
350 cmds = [CommandHandle::dummy(), CommandHandle::dummy()];
351 *c.node() = UiNode::nil();
352 get_title = VarHandle::dummy();
353 }
354 });
355 WIDGET.update_info().layout().render();
356 }
357 }
358 _ => {}
359 })
360}
361
362fn subscribe(index: usize, pages: &[Page]) -> [CommandHandle; 2] {
363 let id = WIDGET.id();
364 let cmds = [BACK_CMD.scoped(id).subscribe(false), NEXT_CMD.scoped(id).subscribe(false)];
365
366 let mut skips = MergeVarBuilder::new();
367 for p in pages {
368 skips.push(p.skip.0.clone());
369 }
370 let can_dos = skips.build(move |skips| {
371 let mut can_back = false;
372 for i in 0..index {
373 can_back = !skips.get(i);
374 if can_back {
375 break;
376 }
377 }
378 let mut can_next = false;
379 for i in index + 1..skips.len() {
380 can_next = !skips.get(i);
381 if can_next {
382 break;
383 }
384 }
385 [can_back, can_next]
386 });
387
388 can_dos.set_bind_map(cmds[0].enabled(), |[b, _]| *b).perm();
389 can_dos.set_bind_map(cmds[1].enabled(), |[_, n]| *n).perm();
390 cmds[0].enabled().hold(can_dos).perm();
391
392 cmds
393}
394fn build(index: usize, pages: &[Page]) -> UiNode {
395 let page = &pages[index];
396 let args = PageArgs {
397 index,
398 pages_len: pages.len(),
399 title: page.title.0.clone(),
400 info: page.info.0.clone(),
401 can_back: page.can_back.0.clone(),
402 can_next: page.can_next.0.clone(),
403 };
404 let header = (page.header)(args.clone());
405 let side = (page.side)(args.clone());
406 let content = (page.content)(args.clone());
407 let footer = (page.footer)(args.clone());
408
409 let header = if header.is_nil() {
410 header
411 } else {
412 let background = HEADER_BACKGROUND_FN_VAR.get()(());
413 HEADER_FN_VAR.get()(HeaderFnArgs {
414 header,
415 background,
416 index,
417 pages_len: pages.len(),
418 titles: pages.iter().map(|p| p.title.0.clone()).collect(),
419 skips: pages.iter().map(|p| p.skip.0.clone()).collect(),
420 })
421 };
422 let side = if side.is_nil() {
423 side
424 } else {
425 let background = SIDE_BACKGROUND_FN_VAR.get()(());
426 let side_extra = SIDE_EXTRA_FN_VAR.get()(args.clone());
427 SIDE_FN_VAR.get()(SideFnArgs {
428 side,
429 background,
430 side_extra,
431 index,
432 pages_len: pages.len(),
433 })
434 };
435 let content = CONTENT_FN_VAR.get()(ContentFnArgs {
436 content,
437 index,
438 pages_len: pages.len(),
439 });
440 let footer_extra = FOOTER_EXTRA_FN_VAR.get()(args);
441 let footer = FOOTER_FN_VAR.get()(FooterFnArgs {
442 footer,
443 footer_extra,
444 index,
445 pages_len: pages.len(),
446 });
447
448 PANEL_FN_VAR.get()(PanelFnArgs {
449 header,
450 side,
451 content,
452 footer,
453 })
454}