Skip to main content

zng_wgt_setup/page/
finish.rs

1use std::mem;
2
3use zng_ext_l10n::l10n;
4use zng_wgt::{
5    enabled, margin,
6    prelude::{task::parking_lot::Mutex, *},
7};
8use zng_wgt_container::Container;
9use zng_wgt_markdown::Markdown;
10use zng_wgt_scroll::{Scroll, ScrollMode};
11use zng_wgt_stack::{Stack, StackDirection};
12use zng_wgt_text::Text;
13use zng_wgt_toggle::{CheckStyle, Toggle};
14use zng_wgt_wizard::{CAN_FINISH_VAR, FINISH_CMD, Page, PageArgs, default_page_footer_finish};
15
16use crate::{APP_NAME_VAR, APP_VERSION_VAR, INSTALLED_VERSION_VAR, SETUP_OP_VAR, SetupOp};
17
18/// Setup operation finish without errors page.
19pub struct FinishPage {
20    /// Title.
21    ///
22    /// Displayed inside the content, unlike other pages.
23    pub title: Var<Txt>,
24
25    /// Default message markdown.
26    pub msg: Var<Txt>,
27
28    /// Custom message markdown, appended after the `msg` paragraph.
29    pub msg_extra: Var<Txt>,
30
31    /// Actions to run once the wizard finishes.
32    pub actions: Vec<FinishAction>,
33}
34impl Default for FinishPage {
35    fn default() -> Self {
36        Self {
37            title: SETUP_OP_VAR.flat_map(|op| match op {
38                SetupOp::Install => l10n!("finish/title.install", "{$app} Installed", app = APP_NAME_VAR),
39                SetupOp::Update => l10n!("finish/title.update", "{$app} Updated", app = APP_NAME_VAR),
40                SetupOp::Repair => l10n!("finish/title.repair", "{$app} Repaired", app = APP_NAME_VAR),
41                SetupOp::Uninstall => l10n!("finish/title.uninstall", "{$app} Uninstalled", app = APP_NAME_VAR),
42            }),
43            msg: SETUP_OP_VAR.flat_map(|op| match op {
44                SetupOp::Install => l10n!(
45                    "finish/message.install",
46                    "Wizard has installed {$app} {$version} on your computer.",
47                    app = APP_NAME_VAR,
48                    version = APP_VERSION_VAR,
49                ),
50                SetupOp::Update => l10n!(
51                    "finish/message.update",
52                    "Wizard has updated {$app} from {$current_version} to {$new_version} on your computer.",
53                    app = APP_NAME_VAR,
54                    current_version = INSTALLED_VERSION_VAR,
55                    new_version = APP_VERSION_VAR,
56                ),
57                SetupOp::Repair => l10n!(
58                    "finish/message.repair",
59                    "Wizard has repaired {$app} {$version} installation on your computer.",
60                    app = APP_NAME_VAR,
61                    version = APP_VERSION_VAR,
62                ),
63                SetupOp::Uninstall => l10n!(
64                    "finish/message.uninstall",
65                    "Wizard has uninstalled {$app} {$version} from your your computer.",
66                    app = APP_NAME_VAR,
67                    version = APP_VERSION_VAR,
68                ),
69            }),
70            msg_extra: const_var("".into()),
71            actions: vec![],
72        }
73    }
74}
75impl FinishPage {
76    /// New default, without finish actions.
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Build the page.
82    pub fn build(self) -> Page {
83        let Self {
84            title,
85            msg,
86            msg_extra,
87            actions,
88        } = self;
89
90        let msg = expr_var! {
91            formatx!("## {}\n\n{}\n\n{}", #{title}, #{msg}, #{msg_extra})
92        };
93
94        let (action_infos, actions): (Vec<_>, Vec<_>) = actions.into_iter().map(|a| ((a.selected, a.info), a.action)).unzip();
95        let actions = Mutex::new(actions);
96
97        let content = wgt_fn!(|args: PageArgs| {
98            if !args.is_last() && !args.is_custom() {
99                tracing::warn!("finish page is not last");
100            }
101
102            // subscribe to run actions on finish
103            let actions = mem::take(&mut *actions.lock());
104            if !actions.is_empty() {
105                let selected: Vec<_> = action_infos.iter().map(|(s, _)| s.clone()).collect();
106                let handle = FINISH_CMD.scoped(args.wizard_id()).on_pre_event(
107                    false,
108                    true,
109                    false,
110                    hn_once!(|_| {
111                        for (action, selected) in actions.into_iter().zip(selected) {
112                            if selected.get()
113                                && let Err(e) = action()
114                            {
115                                tracing::error!("finish action failed, {e}");
116                            }
117                        }
118                    }),
119                );
120                // let SetupWizard! define if can finish
121                CAN_FINISH_VAR.set_bind(handle.enabled()).perm();
122                WIDGET.push_any_handle(handle);
123            }
124
125            Container! {
126                child = Scroll! {
127                    child_align = Align::FILL_TOP;
128                    padding = 20;
129                    child = Markdown! {
130                        txt = msg.clone();
131                    };
132                    mode = ScrollMode::VERTICAL;
133                };
134                child_bottom = Stack! {
135                    margin = (0, 20, 20, 20);
136                    direction = StackDirection::top_to_bottom();
137                    spacing = 5;
138                    children =
139                        action_infos
140                            .iter()
141                            .map(|(checked, txt)| {
142                                Toggle! {
143                                    checked = checked.clone();
144                                    child = Text!(txt.clone());
145                                    style_fn = CheckStyle!();
146                                    enabled = checked.capabilities().can_modify();
147                                }
148                            })
149                            .collect::<UiVec>(),
150                    ;
151                };
152            }
153        });
154
155        let mut pg = Page::new("", "", content);
156        pg.content_fill = true;
157        pg.header = WidgetFn::nil();
158        pg.footer = wgt_fn!(|a: PageArgs| default_page_footer_finish(a.wizard_id()));
159        pg
160    }
161}
162
163/// Represents an action that [`FinishPage`] runs once the wizard finishes.
164#[non_exhaustive]
165pub struct FinishAction {
166    /// Short display info about the action.
167    pub info: Var<Txt>,
168    /// If the action should run.
169    ///
170    /// If this var is modifiable the user will see an enabled checkbox for the action.
171    pub selected: Var<bool>,
172    /// Action to run.
173    ///
174    /// The action will run blocking on the app context, on a [`FINISH_CMD`] preview handler. Note
175    /// that the setup app will likely exit just after this call, starting async tasks is not recommended.
176    pub action: Box<dyn FnOnce() -> std::io::Result<()> + Send + 'static>,
177}
178impl FinishAction {
179    /// New action the user can enable/disable.
180    pub fn new(info: impl IntoVar<Txt>, enabled_default: bool, action: impl FnOnce() -> std::io::Result<()> + Send + 'static) -> Self {
181        Self {
182            info: info.into_var(),
183            selected: var(enabled_default),
184            action: Box::new(action),
185        }
186    }
187
188    /// Force `enabled`, the user will not be allowed to disable.
189    pub fn required(mut self) -> Self {
190        self.selected = const_var(true);
191        self
192    }
193
194    /// Action that runs the installed app.
195    pub fn run_app(mut run_app_cmd: std::process::Command) -> Self {
196        Self::new(l10n!("finish/action-run.info", "Run {$app}", app = APP_NAME_VAR), true, move || {
197            run_app_cmd.spawn()?;
198            Ok(())
199        })
200    }
201}