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)]
14#[non_exhaustive]
15pub struct MsgDialog {
16 pub title: Txt,
18 pub message: Txt,
20 pub icon: MsgDialogIcon,
22 pub buttons: MsgDialogButtons,
24}
25impl MsgDialog {
26 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
51#[non_exhaustive]
52pub enum MsgDialogIcon {
53 Info,
55 Warn,
57 Error,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
65#[non_exhaustive]
66pub enum MsgDialogButtons {
67 Ok,
71 OkCancel,
75 YesNo,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
81#[non_exhaustive]
82pub enum MsgDialogResponse {
83 Ok,
85 Yes,
87 No,
89 Cancel,
91 Error(Txt),
96}
97
98#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, serde::Serialize, serde::Deserialize)]
111#[serde(transparent)]
112pub struct FileDialogFilters(Txt);
113impl FileDialogFilters {
114 pub fn new() -> Self {
116 Self::default()
117 }
118
119 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 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 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#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
246#[non_exhaustive]
247pub struct FileDialog {
248 pub title: Txt,
250 pub starting_dir: PathBuf,
252 pub starting_name: Txt,
254 pub filters: Txt,
267
268 pub kind: FileDialogKind,
270}
271impl FileDialog {
272 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
316#[non_exhaustive]
317pub enum FileDialogKind {
318 OpenFile,
320 OpenFiles,
322 SelectFolder,
324 SelectFolders,
326 SaveFile,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
332#[non_exhaustive]
333pub enum FileDialogResponse {
334 Selected(Vec<PathBuf>),
338 Cancel,
340 Error(Txt),
345}
346impl FileDialogResponse {
347 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 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}