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