zng_view_api/
dialog.rs

1//! Native dialog types.
2
3use std::{mem, path::PathBuf};
4
5use zng_txt::Txt;
6
7crate::declare_id! {
8    /// Identifies an ongoing async native dialog with the user.
9    pub struct DialogId(_);
10}
11
12/// Defines a native message dialog.
13#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14#[non_exhaustive]
15pub struct MsgDialog {
16    /// Message dialog window title.
17    pub title: Txt,
18    /// Message text.
19    pub message: Txt,
20    /// Kind of message.
21    pub icon: MsgDialogIcon,
22    /// Message buttons.
23    pub buttons: MsgDialogButtons,
24}
25impl MsgDialog {
26    /// New message dialog.
27    pub fn new(title: impl Into<Txt>, message: impl Into<Txt>, icon: MsgDialogIcon, buttons: MsgDialogButtons) -> Self {
28        Self {
29            title: title.into(),
30            message: message.into(),
31            icon,
32            buttons,
33        }
34    }
35}
36impl Default for MsgDialog {
37    fn default() -> Self {
38        Self {
39            title: Txt::from_str(""),
40            message: Txt::from_str(""),
41            icon: MsgDialogIcon::Info,
42            buttons: MsgDialogButtons::Ok,
43        }
44    }
45}
46
47/// Icon of a message dialog.
48///
49/// Defines the overall *level* style of the dialog.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
51#[non_exhaustive]
52pub enum MsgDialogIcon {
53    /// Informational.
54    Info,
55    /// Warning.
56    Warn,
57    /// Error.
58    Error,
59}
60
61/// Buttons of a message dialog.
62///
63/// Defines what kind of question the user is answering.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
65#[non_exhaustive]
66pub enum MsgDialogButtons {
67    /// Ok.
68    ///
69    /// Just a confirmation of message received.
70    Ok,
71    /// Ok or Cancel.
72    ///
73    /// Approve selected choice or cancel.
74    OkCancel,
75    /// Yes or No.
76    YesNo,
77}
78
79/// Response to a message dialog.
80#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
81#[non_exhaustive]
82pub enum MsgDialogResponse {
83    /// Message received or approved.
84    Ok,
85    /// Question approved.
86    Yes,
87    /// Question denied.
88    No,
89    /// Message denied.
90    Cancel,
91    /// Failed to show the message.
92    ///
93    /// The associated string may contain debug information, caller should assume that native file dialogs
94    /// are not available for the given window ID at the current view-process instance.
95    Error(Txt),
96}
97
98/// File dialog filters builder.
99///
100/// # Syntax
101///
102/// ```txt
103/// Display Name|ext1;ext2|All Files|*
104/// ```
105///
106/// You can use the [`push_filter`] method to create filters. Note that the extensions are
107/// not glob patterns, they must be an extension (without the dot prefix) or `*` for all files.
108///
109/// [`push_filter`]: FileDialogFilters::push_filter
110#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, serde::Serialize, serde::Deserialize)]
111#[serde(transparent)]
112pub struct FileDialogFilters(Txt);
113impl FileDialogFilters {
114    /// New default (empty).
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Push a filter entry.
120    pub fn push_filter<S: AsRef<str>>(&mut self, display_name: &str, extensions: &[S]) -> &mut Self {
121        if !self.0.is_empty() && !self.0.ends_with('|') {
122            self.0.push('|');
123        }
124
125        let mut extensions: Vec<_> = extensions
126            .iter()
127            .map(|s| s.as_ref())
128            .filter(|&s| !s.contains('|') && !s.contains(';'))
129            .collect();
130        if extensions.is_empty() {
131            extensions = vec!["*"];
132        }
133
134        let display_name = display_name.replace('|', " ");
135        let display_name = display_name.trim();
136        if !display_name.is_empty() {
137            self.0.push_str(display_name);
138            self.0.push_str(" (");
139        }
140        let mut prefix = "";
141        for pat in &extensions {
142            self.0.push_str(prefix);
143            prefix = ", ";
144            self.0.push_str("*.");
145            self.0.push_str(pat);
146        }
147        if !display_name.is_empty() {
148            self.0.push(')');
149        }
150
151        self.0.push('|');
152
153        prefix = "";
154        for pat in extensions {
155            self.0.push_str(prefix);
156            prefix = ";";
157            self.0.push_str(pat);
158        }
159
160        self
161    }
162
163    /// Iterate over filter entries and patterns.
164    pub fn iter_filters(&self) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
165        Self::iter_filters_str(self.0.as_str())
166    }
167    fn iter_filters_str(filters: &str) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
168        struct Iter<'a> {
169            filters: &'a str,
170        }
171        struct PatternIter<'a> {
172            patterns: &'a str,
173        }
174        impl<'a> Iterator for Iter<'a> {
175            type Item = (&'a str, PatternIter<'a>);
176
177            fn next(&mut self) -> Option<Self::Item> {
178                if let Some(i) = self.filters.find('|') {
179                    let display_name = &self.filters[..i];
180                    self.filters = &self.filters[i + 1..];
181
182                    let patterns = if let Some(i) = self.filters.find('|') {
183                        let pat = &self.filters[..i];
184                        self.filters = &self.filters[i + 1..];
185                        pat
186                    } else {
187                        let pat = self.filters;
188                        self.filters = "";
189                        pat
190                    };
191
192                    if !patterns.is_empty() {
193                        Some((display_name.trim(), PatternIter { patterns }))
194                    } else {
195                        self.filters = "";
196                        None
197                    }
198                } else {
199                    self.filters = "";
200                    None
201                }
202            }
203        }
204        impl<'a> Iterator for PatternIter<'a> {
205            type Item = &'a str;
206
207            fn next(&mut self) -> Option<Self::Item> {
208                if let Some(i) = self.patterns.find(';') {
209                    let pattern = &self.patterns[..i];
210                    self.patterns = &self.patterns[i + 1..];
211                    Some(pattern.trim())
212                } else if !self.patterns.is_empty() {
213                    let pat = self.patterns;
214                    self.patterns = "";
215                    Some(pat)
216                } else {
217                    self.patterns = "";
218                    None
219                }
220            }
221        }
222        Iter {
223            filters: filters.trim_start().trim_start_matches('|'),
224        }
225    }
226
227    /// Gets the filter text.
228    pub fn build(mut self) -> Txt {
229        self.0.end_mut();
230        self.0
231    }
232}
233#[cfg(feature = "var")]
234zng_var::impl_from_and_into_var! {
235    fn from(filter: Txt) -> FileDialogFilters {
236        FileDialogFilters(filter)
237    }
238
239    fn from(filter: &'static str) -> FileDialogFilters {
240        FileDialogFilters(filter.into())
241    }
242}
243
244/// Defines a native file dialog.
245#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
246#[non_exhaustive]
247pub struct FileDialog {
248    /// Dialog window title.
249    pub title: Txt,
250    /// Selected directory when the dialog opens.
251    pub starting_dir: PathBuf,
252    /// Starting file name.
253    pub starting_name: Txt,
254    /// File extension filters.
255    ///
256    /// Syntax:
257    ///
258    /// ```txt
259    /// Display Name|ext1;ext2|All Files|*
260    /// ```
261    ///
262    /// You can use the [`push_filter`] method to create filters. Note that the extensions are
263    /// not glob patterns, they must be an extension (without the dot prefix) or `*` for all files.
264    ///
265    /// [`push_filter`]: Self::push_filter
266    pub filters: Txt,
267
268    /// Defines the file dialog looks and what kind of result is expected.
269    pub kind: FileDialogKind,
270}
271impl FileDialog {
272    /// New file dialog.
273    pub fn new(
274        title: impl Into<Txt>,
275        starting_dir: PathBuf,
276        starting_name: impl Into<Txt>,
277        filters: impl Into<Txt>,
278        kind: FileDialogKind,
279    ) -> Self {
280        Self {
281            title: title.into(),
282            starting_dir,
283            starting_name: starting_name.into(),
284            filters: filters.into(),
285            kind,
286        }
287    }
288
289    /// Push a filter entry.
290    pub fn push_filter<S: AsRef<str>>(&mut self, display_name: &str, extensions: &[S]) -> &mut Self {
291        let mut f = FileDialogFilters(mem::take(&mut self.filters));
292        f.push_filter(display_name, extensions);
293        self.filters = f.build();
294        self
295    }
296
297    /// Iterate over filter entries and patterns.
298    pub fn iter_filters(&self) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
299        FileDialogFilters::iter_filters_str(&self.filters)
300    }
301}
302impl Default for FileDialog {
303    fn default() -> Self {
304        FileDialog {
305            title: Txt::from_str(""),
306            starting_dir: PathBuf::new(),
307            starting_name: Txt::from_str(""),
308            filters: Txt::from_str(""),
309            kind: FileDialogKind::OpenFile,
310        }
311    }
312}
313
314/// Kind of file dialogs.
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
316#[non_exhaustive]
317pub enum FileDialogKind {
318    /// Pick one file for reading.
319    OpenFile,
320    /// Pick one or many files for reading.
321    OpenFiles,
322    /// Pick one directory for reading.
323    SelectFolder,
324    /// Pick one or many directories for reading.
325    SelectFolders,
326    /// Pick one file for writing.
327    SaveFile,
328}
329
330/// Response to a message dialog.
331#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
332#[non_exhaustive]
333pub enum FileDialogResponse {
334    /// Selected paths.
335    ///
336    /// Is never empty.
337    Selected(Vec<PathBuf>),
338    /// User did not select any path.
339    Cancel,
340    /// Failed to show the dialog.
341    ///
342    /// The associated string may contain debug information, caller should assume that native file dialogs
343    /// are not available for the given window ID at the current view-process instance.
344    Error(Txt),
345}
346impl FileDialogResponse {
347    /// Gets the selected paths, or empty for cancel.
348    pub fn into_paths(self) -> Result<Vec<PathBuf>, Txt> {
349        match self {
350            FileDialogResponse::Selected(s) => Ok(s),
351            FileDialogResponse::Cancel => Ok(vec![]),
352            FileDialogResponse::Error(e) => Err(e),
353        }
354    }
355
356    /// Gets the last selected path, or `None` for cancel.
357    pub fn into_path(self) -> Result<Option<PathBuf>, Txt> {
358        self.into_paths().map(|mut p| p.pop())
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn file_filters() {
368        let mut dlg = FileDialog {
369            title: "".into(),
370            starting_dir: "".into(),
371            starting_name: "".into(),
372            filters: "".into(),
373            kind: FileDialogKind::OpenFile,
374        };
375
376        let expected = "Display Name (*.abc, *.bca)|abc;bca|All Files (*.*)|*";
377
378        dlg.push_filter("Display Name", &["abc", "bca"]).push_filter("All Files", &["*"]);
379        assert_eq!(expected, dlg.filters);
380
381        let expected = vec![("Display Name (*.abc, *.bca)", vec!["abc", "bca"]), ("All Files (*.*)", vec!["*"])];
382        let parsed: Vec<(&str, Vec<&str>)> = dlg.iter_filters().map(|(n, p)| (n, p.collect())).collect();
383        assert_eq!(expected, parsed);
384    }
385}