1use std::{mem, path::PathBuf};
4
5use zng_txt::Txt;
6
7crate::declare_id! {
8 pub struct DialogId(_);
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14pub struct MsgDialog {
15 pub title: Txt,
17 pub message: Txt,
19 pub icon: MsgDialogIcon,
21 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
39pub enum MsgDialogIcon {
40 Info,
42 Warn,
44 Error,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
52pub enum MsgDialogButtons {
53 Ok,
57 OkCancel,
61 YesNo,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
67pub enum MsgDialogResponse {
68 Ok,
70 Yes,
72 No,
74 Cancel,
76 Error(Txt),
81}
82
83#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, serde::Serialize, serde::Deserialize)]
96#[serde(transparent)]
97pub struct FileDialogFilters(Txt);
98impl FileDialogFilters {
99 pub fn new() -> Self {
101 Self::default()
102 }
103
104 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
231pub struct FileDialog {
232 pub title: Txt,
234 pub starting_dir: PathBuf,
236 pub starting_name: Txt,
238 pub filters: Txt,
251
252 pub kind: FileDialogKind,
254}
255impl FileDialog {
256 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
283pub enum FileDialogKind {
284 OpenFile,
286 OpenFiles,
288 SelectFolder,
290 SelectFolders,
292 SaveFile,
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
298pub enum FileDialogResponse {
299 Selected(Vec<PathBuf>),
303 Cancel,
305 Error(Txt),
310}
311impl FileDialogResponse {
312 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 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}