1use std::{mem, path::PathBuf, time::Duration};
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<'a>(&mut self, display_name: &str, extensions: impl IntoIterator<Item = &'a str>) -> &mut Self {
121 if !self.0.is_empty() && !self.0.ends_with('|') {
122 self.0.push('|');
123 }
124
125 let extensions: Vec<_> = extensions.into_iter().filter(|s| !s.contains('|') && !s.contains(';')).collect();
126 self.push_filter_impl(display_name, extensions)
127 }
128
129 fn push_filter_impl(&mut self, display_name: &str, mut extensions: Vec<&str>) -> &mut FileDialogFilters {
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<'a>(&mut self, display_name: &str, extensions: impl IntoIterator<Item = &'a str>) -> &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#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
364#[non_exhaustive]
365pub struct Notification {
366 pub title: Txt,
368 pub message: Txt,
370 pub actions: Vec<NotificationAction>,
372 pub timeout: Option<Duration>,
374}
375impl Notification {
376 pub fn new(title: impl Into<Txt>, body: impl Into<Txt>) -> Self {
378 Self {
379 title: title.into(),
380 message: body.into(),
381 actions: vec![],
382 timeout: None,
383 }
384 }
385
386 pub const fn close() -> Self {
390 Self {
391 title: Txt::from_static(""),
392 message: Txt::from_static(""),
393 actions: vec![],
394 timeout: Some(Duration::ZERO),
395 }
396 }
397
398 pub fn push_action(&mut self, id: impl Into<Txt>, label: impl Into<Txt>) {
400 self.actions.push(NotificationAction::new(id, label))
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
406#[non_exhaustive]
407pub struct NotificationAction {
408 pub id: Txt,
410 pub label: Txt,
412}
413impl NotificationAction {
414 pub fn new(id: impl Into<Txt>, label: impl Into<Txt>) -> Self {
416 Self {
417 id: id.into(),
418 label: label.into(),
419 }
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
425#[non_exhaustive]
426pub enum NotificationResponse {
427 Action(Txt),
434 Dismissed,
436 Removed,
438 Error(Txt),
442}
443
444bitflags::bitflags! {
445 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
447 pub struct DialogCapability: u32 {
448 const MESSAGE = 1 << 0;
450 const OPEN_FILE = 1 << 1;
452 const OPEN_FILES = 1 << 2;
454 const SAVE_FILE = 1 << 3;
456 const SELECT_FOLDER = 1 << 4;
458 const SELECT_FOLDERS = 1 << 5;
460 const NOTIFICATION = 1 << 6;
462 const NOTIFICATION_ACTIONS = (1 << 7) | Self::NOTIFICATION.bits();
464 const CLOSE_NOTIFICATION = (1 << 8) | Self::NOTIFICATION.bits();
466 const UPDATE_NOTIFICATION = (1 << 9) | Self::NOTIFICATION.bits();
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn file_filters() {
477 let mut dlg = FileDialog {
478 title: "".into(),
479 starting_dir: "".into(),
480 starting_name: "".into(),
481 filters: "".into(),
482 kind: FileDialogKind::OpenFile,
483 };
484
485 let expected = "Display Name (*.abc, *.bca)|abc;bca|All Files (*.*)|*";
486
487 dlg.push_filter("Display Name", ["abc", "bca"]).push_filter("All Files", ["*"]);
488 assert_eq!(expected, dlg.filters);
489
490 let expected = vec![("Display Name (*.abc, *.bca)", vec!["abc", "bca"]), ("All Files (*.*)", vec!["*"])];
491 let parsed: Vec<(&str, Vec<&str>)> = dlg.iter_filters().map(|(n, p)| (n, p.collect())).collect();
492 assert_eq!(expected, parsed);
493 }
494}