Skip to main content

zng_wgt_setup/page/
status.rs

1use std::fmt::Write as _;
2
3use zng_ext_l10n::l10n;
4use zng_ext_setup::{SETUP, SetupOpStatus, SetupStatus, task::SetupTaskError};
5use zng_wgt::{ICONS, Wgt, align, prelude::*};
6use zng_wgt_container::{Container, child_out_bottom};
7use zng_wgt_fill::background;
8use zng_wgt_filter::opacity;
9use zng_wgt_progress::{Progress, ProgressView};
10use zng_wgt_size_offset::{min_height, size, y};
11use zng_wgt_stack::{Stack, StackDirection};
12use zng_wgt_text::{Text, icon::ico_color};
13use zng_wgt_text_input::selectable::SelectableText;
14use zng_wgt_wizard::{Page, PageArgs};
15
16use crate::{APP_NAME_VAR, SETUP_OP_VAR, SetupOp, page::TaskInfo};
17
18/// Setup operation status page.
19#[non_exhaustive]
20pub struct StatusPage {
21    /// The status displayed by the page.
22    ///
23    /// Is [`SETUP::status`] by default.
24    pub status: Var<SetupStatus>,
25
26    /// Display info about setup task components of the operation that will run.
27    ///
28    /// This task list is displayed on the same order as defined, as a *check list* with
29    /// individual progress indicator.
30    ///
31    /// If this list is empty or a task is not present only the overall operation progress
32    /// indicator is shown.
33    pub task_infos: Vec<TaskInfo>,
34
35    /// Widget that generates the status item for each item in `task_infos`.
36    pub task_info_fn: WidgetFn<StatusTaskInfoFnArgs>,
37}
38impl Default for StatusPage {
39    fn default() -> Self {
40        Self {
41            status: SETUP.status(),
42            task_infos: vec![],
43            task_info_fn: WidgetFn::new(default_task_info_fn),
44        }
45    }
46}
47impl StatusPage {
48    /// New default page.
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Insert display info about a setup task component.
54    pub fn push_info(&mut self, info: TaskInfo) {
55        if let Some(i) = self.task_infos.iter().position(|t| t.id == info.id) {
56            tracing::debug!("task info for {:?} replaced", info.id);
57            self.task_infos.remove(i);
58        }
59        self.task_infos.push(info);
60    }
61
62    /// Insert display info about a setup task component.
63    pub fn with_info(mut self, info: TaskInfo) -> Self {
64        self.push_info(info);
65        self
66    }
67
68    /// Build page.
69    pub fn build(self) -> Page {
70        let title = SETUP_OP_VAR.flat_map(|op| match op {
71            SetupOp::Install => l10n!("status/title.install", "Installing"),
72            SetupOp::Update => l10n!("status/title.update", "Updating"),
73            SetupOp::Repair => l10n!("status/title.repair", "Repairing"),
74            SetupOp::Uninstall => l10n!("status/title.uninstall", "Uninstalling"),
75        });
76        let info = SETUP_OP_VAR.flat_map(|op| match op {
77            SetupOp::Install => l10n!(
78                "status/info.install",
79                "Please wait while {$app} is installed on your computer.",
80                app = APP_NAME_VAR
81            ),
82            SetupOp::Update => l10n!(
83                "status/info.update",
84                "Please wait while {$app} is updated on your computer.",
85                app = APP_NAME_VAR
86            ),
87            SetupOp::Repair => l10n!(
88                "status/info.repair",
89                "Please wait while {$app} is repaired on your computer.",
90                app = APP_NAME_VAR
91            ),
92            SetupOp::Uninstall => l10n!(
93                "status/info.uninstall",
94                "Please wait while {$app} is uninstalled from your computer.",
95                app = APP_NAME_VAR
96            ),
97        });
98        let mut pg = Page::new(
99            title,
100            info,
101            wgt_fn!(|_| build(self.status.clone(), self.task_infos.clone(), self.task_info_fn.clone())),
102        );
103        pg.side = WidgetFn::nil();
104        pg.footer = wgt_fn!(|a: PageArgs| {
105            let id = a.wizard_id();
106            zng_wgt_wizard::default_page_footer_cancel(id)
107        });
108        pg
109    }
110}
111
112fn build(status: Var<SetupStatus>, tasks: Vec<TaskInfo>, item_fn: WidgetFn<StatusTaskInfoFnArgs>) -> UiNode {
113    // get (is_prepare, SetupOpStatus), but only if it matches SETUP_OP_VAR
114    let op_status = expr_var! {
115        match #{SETUP_OP_VAR} {
116            SetupOp::Install | SetupOp::Update | SetupOp::Repair => match #{status} {
117                SetupStatus::PrepareInstall(op) => Some((true, op.clone())),
118                SetupStatus::CommitInstall(op) => Some((false, op.clone())),
119                _ => None,
120            },
121            SetupOp::Uninstall => match #{status} {
122                SetupStatus::ValidateUninstall(op) => Some((true, op.clone())),
123                SetupStatus::Uninstall(op) => Some((false, op.clone())),
124                _ => None,
125            },
126        }
127    };
128
129    if tasks.len() <= 1 {
130        Stack! {
131            direction = StackDirection::top_to_bottom();
132            children = ui_vec![
133                Text! {
134                    txt = op_status.map(|s| match s {
135                        Some((_, s)) if s.progress.1 > 1 => formatx!("{} / {}", s.progress.0, s.progress.1),
136                        _ => "".into(),
137                    });
138                    txt_align = Align::CENTER;
139                    font_size = 1.2.em();
140                },
141                ProgressView! {
142                    align = Align::FILL_TOP;
143                    progress = op_status.flat_map(|s| match s {
144                        Some((_, s)) => s.task_progress.0.clone(),
145                        None => const_var(Progress::indeterminate()),
146                    });
147                }
148            ];
149            spacing = 5;
150        }
151    } else {
152        Stack! {
153            direction = StackDirection::top_to_bottom();
154            spacing = 5;
155            children = list_presenter_from_iter(
156                tasks,
157                wgt_fn!(|a| item_fn(StatusTaskInfoFnArgs::new(a, &op_status))),
158            );
159        }
160    }
161}
162
163/// Arguments for generating a task status UI.
164#[derive(Clone)]
165pub struct StatusTaskInfoFnArgs {
166    /// Task info.
167    pub info: TaskInfo,
168
169    status: Var<Option<(bool, SetupOpStatus)>>,
170}
171impl StatusTaskInfoFnArgs {
172    fn new(info: TaskInfo, status: &Var<Option<(bool, SetupOpStatus)>>) -> Self {
173        Self {
174            info,
175            status: status.clone(),
176        }
177    }
178
179    /// Task progress.
180    pub fn progress(&self) -> Var<Progress> {
181        let mut progress = const_var(Progress::from_n_of(0, 1));
182        let id = self.info.id.clone();
183        self.status.flat_map(move |s| {
184            if let Some((_, s)) = s
185                && s.task == id
186            {
187                progress = s.task_progress.0.clone();
188            }
189            progress.clone()
190        })
191    }
192
193    /// When `true` progress is for [`SetupStatus::PrepareInstall`] or [`SetupStatus::ValidateUninstall`].
194    ///
195    /// When `false` progress is for [`SetupStatus::CommitInstall`] or [`SetupStatus::Uninstall`].
196    pub fn is_preparing(&self) -> Var<bool> {
197        self.status.map(|s| if let Some((is_prep, _)) = s { *is_prep } else { true })
198    }
199
200    /// Progress is for cancel operation.
201    pub fn is_cancelling(&self) -> Var<bool> {
202        self.status.map(|s| if let Some((_, s)) = s { s.cancel } else { false })
203    }
204
205    /// Task errors.
206    pub fn errors(&self) -> Var<Vec<SetupTaskError>> {
207        let id = self.info.id.clone();
208        self.status.map(move |s| {
209            if let Some((_, s)) = s {
210                s.errors
211                    .iter()
212                    .filter_map(|((_, ty, name), e)| if *ty == id.0 && *name == id.1 { Some(e.clone()) } else { None })
213                    .collect()
214            } else {
215                vec![]
216            }
217        })
218    }
219
220    /// If task has completed successfully.
221    pub fn is_success(&self) -> Var<bool> {
222        let mut progress = const_var(false);
223        let id = self.info.id.clone();
224        self.status.flat_map(move |s| {
225            if let Some((is_prep, s)) = s
226                && !is_prep
227            {
228                if s.errors.iter().any(|((_, ty, name), _)| *ty == id.0 && *name == id.1) {
229                    progress = const_var(false);
230                } else if s.task == id {
231                    progress = s.task_progress.map(|p| p.is_complete());
232                }
233            }
234            progress.clone()
235        })
236    }
237}
238
239pub fn default_task_info_fn(args: StatusTaskInfoFnArgs) -> UiNode {
240    let errors = args.errors().map(|e| {
241        let mut r = String::new();
242        let mut sep = "";
243        for e in e {
244            write!(&mut r, "{sep}{e}").unwrap();
245            sep = "\n";
246        }
247        Txt::from(r)
248    });
249    Container! {
250        child_start = Wgt! {
251            background = ICONS.req("circle");
252            y = 5;
253            size = 20;
254
255            #[easing(300.ms())]
256            opacity = 0.1.fct();
257
258            when #{args.is_success()} {
259                opacity = 1.fct();
260                background = ICONS.req("check-circle");
261            }
262            when !#{errors.clone()}.is_empty() {
263                opacity = 1.fct();
264                background = ICONS.req("error");
265                ico_color = light_dark(colors::RED.darken(50.pct()), colors::RED.lighten(50.pct()));
266            }
267        };
268        child_bottom = ProgressView! {
269            progress = args.progress();
270            // reserve space for progress message
271            min_height = 24;
272
273            when !#{errors.clone()}.is_empty() {
274                // replace progress message
275                child_out_bottom = SelectableText! {
276                    txt = errors;
277                    font_color = light_dark(colors::RED.darken(70.pct()), colors::RED.lighten(70.pct()));
278                };
279            }
280        };
281        child = Text! {
282            txt = args.info.info.0;
283            font_size = 1.1.em();
284        };
285        child_spacing = 5;
286    }
287}