Skip to main content

zng_wgt_setup/page/
install_dir.rs

1use std::path::PathBuf;
2
3use zng_ext_l10n::l10n;
4use zng_unit::ByteLength;
5use zng_wgt::{align, prelude::*, visibility};
6use zng_wgt_button::Button;
7use zng_wgt_container::Container;
8use zng_wgt_dialog::DIALOG;
9use zng_wgt_menu::context::ContextMenu;
10use zng_wgt_text_input::{TextInput, label::Label, selectable::SelectableText};
11use zng_wgt_toggle::{self as toggle, Toggle};
12use zng_wgt_wizard::Page;
13
14use crate::{APP_NAME_VAR, APP_ORG_VAR, SETUP_OP_VAR, SetupOp};
15
16/// Page for reviewing and changing the install directory.
17#[non_exhaustive]
18pub struct InstallDirPage {
19    /// Default install directory.
20    pub default_dir: Var<PathBuf>,
21    /// User selected install directory.
22    pub install_dir: Var<PathBuf>,
23    /// Minimal required space on the selected disk.
24    ///
25    /// If this is `0.bytes()` the required space is not shown.
26    pub min_required_space: Var<ByteLength>,
27}
28impl Default for InstallDirPage {
29    fn default() -> Self {
30        let default_dir = expr_var! {
31            let org = #{APP_ORG_VAR};
32            let app = #{APP_NAME_VAR};
33
34            if let Ok(pf) = std::env::var("ProgramFiles") {
35                let mut dir = PathBuf::from(pf);
36                if !org.is_empty() {
37                    dir.push(org);
38                }
39                if !app.is_empty() {
40                    dir.push(app);
41                }
42                dir
43            } else {
44                PathBuf::new()
45            }
46        };
47        Self {
48            install_dir: default_dir.cow(),
49            default_dir,
50            min_required_space: const_var(0.bytes()),
51        }
52    }
53}
54impl InstallDirPage {
55    /// New default.
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Build the page.
61    pub fn build(self) -> Page {
62        let title = SETUP_OP_VAR.flat_map(|op| match op {
63            SetupOp::Install => l10n!("install_dir/title.install", "Select Install Location"),
64            SetupOp::Update | SetupOp::Repair => l10n!("destination/title.update-repair", "Change Install Location"),
65            SetupOp::Uninstall => l10n!("install_dir/title.uninstall", "Install Location"),
66        });
67        let info = SETUP_OP_VAR.flat_map(|op| match op {
68            SetupOp::Install => l10n!("install_dir/info.install", "Where should {$app} be installed?", app = APP_NAME_VAR),
69            SetupOp::Update | SetupOp::Repair | SetupOp::Uninstall => {
70                l10n!(
71                    "install_dir/info.update-repair-uninstall",
72                    "Where {$app} is installed.",
73                    app = APP_NAME_VAR
74                )
75            }
76        });
77        let min_space = self.min_required_space.flat_map(|&s| {
78            if s == 0.bytes() {
79                const_var(Txt::default())
80            } else {
81                // l10n-# $bytes is already formatted e.g.: 900kB or 50MB.
82                l10n!(
83                    "install-dir/min-required-space",
84                    "At least {$bytes} of free disk space is required.",
85                    bytes = s
86                )
87            }
88        });
89
90        let mut pg = Page::new(
91            title.clone(),
92            info,
93            wgt_fn!(|_| { install_dir_ui(self.default_dir.clone(), self.install_dir.clone(), title.clone(), min_space.clone()) }),
94        );
95        pg.side = WidgetFn::nil();
96        pg
97    }
98}
99fn install_dir_ui(
100    default_dir: Var<PathBuf>,
101    install_dir: Var<PathBuf>,
102    select_dlg_title: Var<Txt>,
103    min_required_space: Var<Txt>,
104) -> UiNode {
105    let can_modify = if install_dir.capabilities().is_always_read_only() {
106        const_var(false)
107    } else {
108        SETUP_OP_VAR.map(|op| !matches!(op, SetupOp::Uninstall))
109    };
110    let can_reset = expr_var! {
111        *#{can_modify.clone()} && #{default_dir.clone()} != #{install_dir.clone()}
112    };
113    Container! {
114        child_spacing = 5;
115        child = TextInput! {
116            txt = install_dir.map(|p| {
117                let p = p.display().to_txt();
118                if cfg!(windows) {
119                    p.replace('/', "\\")
120                } else {
121                    p.replace('\\', "/")
122                }
123                .to_txt()
124            });
125            txt_editable = false;
126            align = Align::FILL_TOP;
127        };
128
129        when #{can_modify} {
130            child_end = select_dir_btn(install_dir.clone(), select_dlg_title.clone());
131            child_bottom = SelectableText! {
132                align = Align::BOTTOM_START;
133                visibility = min_required_space.map(|t| (!t.is_empty()).into());
134                txt = min_required_space;
135            };
136        }
137        when #{can_reset} {
138            child_end = Toggle! {
139                style_fn = toggle::ComboStyle!();
140                child = select_dir_btn(install_dir.clone(), select_dlg_title);
141                align = Align::FILL_TOP;
142                checked_popup = wgt_fn!(|_| ContextMenu! {
143                    children = ui_vec![
144                        Button! {
145                            child = Label! {
146                                txt = l10n!("install_dir/reset-label", "Default Location");
147                            };
148                            on_click = hn!(default_dir, install_dir, |args| {
149                                args.propagation.stop();
150                                install_dir.set(default_dir.get());
151                            });
152                        }
153                    ]
154                });
155            };
156        }
157    }
158}
159fn select_dir_btn(install_dir: Var<PathBuf>, select_dlg_title: Var<Txt>) -> UiNode {
160    Button! {
161        child = Label! {
162            txt = l10n!("install_dir/select-label", "Select Location");
163        };
164        on_click = async_hn!(install_dir, select_dlg_title, |args| {
165            args.propagation.stop();
166            select_dir_dlg(install_dir, select_dlg_title).await;
167        });
168        align = Align::FILL_TOP;
169    }
170}
171async fn select_dir_dlg(install_dir: Var<PathBuf>, title: Var<Txt>) {
172    let current = install_dir.get();
173    let current_parent = current.parent().map(PathBuf::from).unwrap_or_default();
174    let current_name = current.file_name().unwrap_or_default().to_str().unwrap_or_default().to_txt();
175    let r = DIALOG.select_folder(title, current_parent, current_name);
176
177    match r.wait_rsp().await {
178        zng_wgt_dialog::FileDialogResponse::Selected(mut p) => install_dir.set(p.remove(0)),
179        zng_wgt_dialog::FileDialogResponse::Cancel => {}
180        zng_wgt_dialog::FileDialogResponse::Error(e) => {
181            tracing::error!("cannot select install dir, {e}");
182        }
183        _ => unreachable!(),
184    }
185}