1use azul_css::{
11 corety::OptionString,
12 impl_option, impl_option_inner,
13 props::basic::color::{ColorU, OptionColorU},
14 AzString, OptionStringVec, StringVec,
15};
16
17#[cfg(not(any(target_os = "android", target_os = "ios")))]
18use tfd::{DefaultColorValue, MessageBoxIcon};
19
20#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
22#[repr(C)]
23#[allow(clippy::pub_underscore_fields)] pub struct MsgBox {
25 pub _reserved: u8,
26}
27
28#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
30#[repr(C)]
31#[allow(clippy::pub_underscore_fields)] pub struct FileDialog {
33 pub _reserved: u8,
34}
35
36#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
38#[repr(C)]
39#[allow(clippy::pub_underscore_fields)] pub struct ColorPickerDialog {
41 pub _reserved: u8,
42}
43
44#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
45#[repr(C)]
46pub enum OkCancel {
47 Ok,
48 Cancel,
49}
50
51#[cfg(not(any(target_os = "android", target_os = "ios")))]
52impl From<tfd::OkCancel> for OkCancel {
53 #[inline]
54 fn from(e: tfd::OkCancel) -> Self {
55 match e {
56 tfd::OkCancel::Ok => Self::Ok,
57 tfd::OkCancel::Cancel => Self::Cancel,
58 }
59 }
60}
61
62#[cfg(not(any(target_os = "android", target_os = "ios")))]
63impl From<OkCancel> for tfd::OkCancel {
64 #[inline]
65 fn from(e: OkCancel) -> Self {
66 match e {
67 OkCancel::Ok => Self::Ok,
68 OkCancel::Cancel => Self::Cancel,
69 }
70 }
71}
72
73#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
74#[repr(C)]
75pub enum YesNo {
76 Yes,
77 No,
78}
79
80#[cfg(not(any(target_os = "android", target_os = "ios")))]
81impl From<YesNo> for tfd::YesNo {
82 #[inline]
83 fn from(e: YesNo) -> Self {
84 match e {
85 YesNo::Yes => Self::Yes,
86 YesNo::No => Self::No,
87 }
88 }
89}
90
91#[cfg(not(any(target_os = "android", target_os = "ios")))]
92impl From<tfd::YesNo> for YesNo {
93 #[inline]
94 fn from(e: tfd::YesNo) -> Self {
95 match e {
96 tfd::YesNo::Yes => Self::Yes,
97 tfd::YesNo::No => Self::No,
98 }
99 }
100}
101
102#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
103#[repr(C)]
104pub enum MsgBoxIcon {
105 Info,
106 Warning,
107 Error,
108 Question,
109}
110
111#[cfg(not(any(target_os = "android", target_os = "ios")))]
112impl From<MsgBoxIcon> for MessageBoxIcon {
113 #[inline]
114 fn from(e: MsgBoxIcon) -> Self {
115 match e {
116 MsgBoxIcon::Info => Self::Info,
117 MsgBoxIcon::Warning => Self::Warning,
118 MsgBoxIcon::Error => Self::Error,
119 MsgBoxIcon::Question => Self::Question,
120 }
121 }
122}
123
124impl Default for MsgBox {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130impl MsgBox {
131 #[must_use] pub const fn new() -> Self {
135 Self { _reserved: 0 }
136 }
137
138 #[allow(clippy::needless_pass_by_value)]
144 pub fn ok(title: AzString, message: AzString, icon: MsgBoxIcon) {
145 #[cfg(not(any(target_os = "android", target_os = "ios")))]
146 {
147 let mut msg = message.as_str().to_string();
148 msg = msg.replace('\"', "");
149 msg = msg.replace('\'', "");
150 tfd::MessageBox::new(title.as_str(), &msg)
151 .with_icon(icon.into())
152 .run_modal();
153 }
154 #[cfg(any(target_os = "android", target_os = "ios"))]
155 {
156 let _ = (title, message, icon);
157 }
158 }
159
160 #[allow(clippy::needless_pass_by_value)]
163 #[must_use] pub fn ok_cancel(
164 title: AzString,
165 message: AzString,
166 icon: MsgBoxIcon,
167 default: OkCancel,
168 ) -> OkCancel {
169 #[cfg(not(any(target_os = "android", target_os = "ios")))]
170 {
171 tfd::MessageBox::new(title.as_str(), message.as_str())
172 .with_icon(icon.into())
173 .run_modal_ok_cancel(default.into())
174 .into()
175 }
176 #[cfg(any(target_os = "android", target_os = "ios"))]
177 {
178 let _ = (title, message, icon);
179 default
180 }
181 }
182
183 #[allow(clippy::needless_pass_by_value)]
186 #[must_use] pub fn yes_no(
187 title: AzString,
188 message: AzString,
189 icon: MsgBoxIcon,
190 default: YesNo,
191 ) -> YesNo {
192 #[cfg(not(any(target_os = "android", target_os = "ios")))]
193 {
194 tfd::MessageBox::new(title.as_str(), message.as_str())
195 .with_icon(icon.into())
196 .run_modal_yes_no(default.into())
197 .into()
198 }
199 #[cfg(any(target_os = "android", target_os = "ios"))]
200 {
201 let _ = (title, message, icon);
202 default
203 }
204 }
205
206 pub fn info(content: AzString) {
208 Self::ok(AzString::from("Info"), content, MsgBoxIcon::Info);
209 }
210}
211
212impl Default for ColorPickerDialog {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218impl ColorPickerDialog {
219 #[must_use] pub const fn new() -> Self {
222 Self { _reserved: 0 }
223 }
224
225 #[allow(clippy::needless_pass_by_value)]
228 #[must_use] pub fn open(title: AzString, default_value: OptionColorU) -> OptionColorU {
229 #[cfg(not(any(target_os = "android", target_os = "ios")))]
230 {
231 let rgb = default_value
232 .into_option()
233 .map_or([0, 0, 0], |c| [c.r, c.g, c.b]);
234 let default_color = DefaultColorValue::RGB(rgb);
235 let result = tfd::ColorChooser::new(title.as_str())
236 .with_default_color(default_color)
237 .run_modal();
238 match result {
239 Some(r) => OptionColorU::Some(ColorU {
240 r: r.1[0],
241 g: r.1[1],
242 b: r.1[2],
243 a: ColorU::ALPHA_OPAQUE,
244 }),
245 None => OptionColorU::None,
246 }
247 }
248 #[cfg(any(target_os = "android", target_os = "ios"))]
249 {
250 let _ = title;
251 default_value
252 }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
257#[repr(C)]
258pub struct FileTypeList {
259 pub document_types: StringVec,
260 pub document_descriptor: AzString,
261}
262
263impl_option!(
264 FileTypeList,
265 OptionFileTypeList,
266 copy = false,
267 [Debug, Clone, PartialEq, Eq, PartialOrd]
268);
269
270#[cfg(not(any(target_os = "android", target_os = "ios")))]
272#[allow(clippy::needless_pass_by_value)]
274fn apply_filter(mut dialog: tfd::FileDialog, filter: FileTypeList) -> tfd::FileDialog {
275 let v = filter.document_types.clone().into_library_owned_vec();
276 let patterns: Vec<&str> = v.iter().map(AzString::as_str).collect();
277 dialog = dialog.with_filter(&patterns, filter.document_descriptor.as_str());
278 dialog
279}
280
281impl Default for FileDialog {
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287impl FileDialog {
288 #[must_use] pub const fn new() -> Self {
291 Self { _reserved: 0 }
292 }
293
294 #[allow(clippy::needless_pass_by_value)]
297 pub fn open_file(
298 title: AzString,
299 default_path: OptionString,
300 filter_list: OptionFileTypeList,
301 ) -> OptionString {
302 #[cfg(not(any(target_os = "android", target_os = "ios")))]
303 {
304 let mut dialog = tfd::FileDialog::new(title.as_str());
305 if let Some(path) = default_path.as_option() {
306 dialog = dialog.with_path(path.as_str());
307 }
308 if let Some(filter) = filter_list.into_option() {
309 dialog = apply_filter(dialog, filter);
310 }
311 dialog.open_file().map(AzString::from).into()
312 }
313 #[cfg(any(target_os = "android", target_os = "ios"))]
314 {
315 let _ = (title, default_path, filter_list);
316 OptionString::None
317 }
318 }
319
320 #[allow(clippy::needless_pass_by_value)]
323 pub fn open_directory(title: AzString, default_path: OptionString) -> OptionString {
324 #[cfg(not(any(target_os = "android", target_os = "ios")))]
325 {
326 let mut dialog = tfd::FileDialog::new(title.as_str());
327 if let Some(path) = default_path.as_option() {
328 dialog = dialog.with_path(path.as_str());
329 }
330 dialog.select_folder().map(AzString::from).into()
331 }
332 #[cfg(any(target_os = "android", target_os = "ios"))]
333 {
334 let _ = (title, default_path);
335 OptionString::None
336 }
337 }
338
339 #[allow(clippy::needless_pass_by_value)]
342 pub fn open_multiple_files(
343 title: AzString,
344 default_path: OptionString,
345 filter_list: OptionFileTypeList,
346 ) -> OptionStringVec {
347 #[cfg(not(any(target_os = "android", target_os = "ios")))]
348 {
349 let mut dialog =
350 tfd::FileDialog::new(title.as_str()).with_multiple_selection(true);
351 if let Some(path) = default_path.as_option() {
352 dialog = dialog.with_path(path.as_str());
353 }
354 if let Some(filter) = filter_list.into_option() {
355 dialog = apply_filter(dialog, filter);
356 }
357 dialog.open_files().map(StringVec::from).into()
358 }
359 #[cfg(any(target_os = "android", target_os = "ios"))]
360 {
361 let _ = (title, default_path, filter_list);
362 OptionStringVec::None
363 }
364 }
365
366 #[allow(clippy::needless_pass_by_value)]
369 pub fn save_file(title: AzString, default_path: OptionString) -> OptionString {
370 #[cfg(not(any(target_os = "android", target_os = "ios")))]
371 {
372 let mut dialog = tfd::FileDialog::new(title.as_str());
373 if let Some(path) = default_path.as_option() {
374 dialog = dialog.with_path(path.as_str());
375 }
376 dialog.save_file().map(AzString::from).into()
377 }
378 #[cfg(any(target_os = "android", target_os = "ios"))]
379 {
380 let _ = (title, default_path);
381 OptionString::None
382 }
383 }
384}
385
386pub fn msg_box(content: &str) {
388 MsgBox::info(AzString::from(content));
389}
390
391#[cfg(test)]
392mod autotest_generated {
393 use super::*;
394
395 fn s(value: &str) -> AzString {
409 AzString::from(value.to_string())
410 }
411
412 fn file_type_list(patterns: &[&str], descriptor: &str) -> FileTypeList {
413 FileTypeList {
414 document_types: StringVec::from_vec(patterns.iter().map(|p| s(p)).collect()),
415 document_descriptor: s(descriptor),
416 }
417 }
418
419 #[test]
424 fn namespace_handles_are_zero_initialised() {
425 assert_eq!(MsgBox::new()._reserved, 0);
426 assert_eq!(FileDialog::new()._reserved, 0);
427 assert_eq!(ColorPickerDialog::new()._reserved, 0);
428 }
429
430 #[test]
431 fn namespace_handles_are_const_evaluable() {
432 const MSG_BOX: MsgBox = MsgBox::new();
436 const FILE_DIALOG: FileDialog = FileDialog::new();
437 const COLOR_PICKER: ColorPickerDialog = ColorPickerDialog::new();
438
439 assert_eq!(MSG_BOX._reserved, 0);
440 assert_eq!(FILE_DIALOG._reserved, 0);
441 assert_eq!(COLOR_PICKER._reserved, 0);
442 }
443
444 #[test]
445 fn namespace_handles_default_matches_new() {
446 assert_eq!(MsgBox::default(), MsgBox::new());
447 assert_eq!(FileDialog::default(), FileDialog::new());
448 assert_eq!(ColorPickerDialog::default(), ColorPickerDialog::new());
449 }
450
451 #[test]
452 fn namespace_handles_are_stateless_single_byte_shims() {
453 assert_eq!(core::mem::size_of::<MsgBox>(), 1);
456 assert_eq!(core::mem::size_of::<FileDialog>(), 1);
457 assert_eq!(core::mem::size_of::<ColorPickerDialog>(), 1);
458 assert_eq!(core::mem::align_of::<MsgBox>(), 1);
459 assert_eq!(core::mem::align_of::<FileDialog>(), 1);
460 assert_eq!(core::mem::align_of::<ColorPickerDialog>(), 1);
461 }
462
463 #[test]
464 fn namespace_handles_are_copy_and_hash_consistently() {
465 use std::{
466 collections::hash_map::DefaultHasher,
467 hash::{Hash, Hasher},
468 };
469
470 fn hash_of<T: Hash>(value: &T) -> u64 {
471 let mut hasher = DefaultHasher::new();
472 value.hash(&mut hasher);
473 hasher.finish()
474 }
475
476 let original = MsgBox::new();
477 let copied = original; assert_eq!(original, copied);
479 assert_eq!(hash_of(&original), hash_of(&copied));
480 assert_eq!(hash_of(&MsgBox::new()), hash_of(&MsgBox::new()));
481 assert_eq!(hash_of(&FileDialog::new()), hash_of(&FileDialog::new()));
482 assert_eq!(
483 hash_of(&ColorPickerDialog::new()),
484 hash_of(&ColorPickerDialog::new())
485 );
486 }
487
488 #[cfg(not(any(target_os = "android", target_os = "ios")))]
493 #[test]
494 fn ok_cancel_round_trips_through_tfd() {
495 for variant in [OkCancel::Ok, OkCancel::Cancel] {
496 let encoded: tfd::OkCancel = variant.into();
497 let decoded: OkCancel = encoded.into();
498 assert_eq!(decoded, variant, "round-trip lost {variant:?}");
499 }
500
501 assert_eq!(OkCancel::from(tfd::OkCancel::Ok), OkCancel::Ok);
503 assert_eq!(OkCancel::from(tfd::OkCancel::Cancel), OkCancel::Cancel);
504 }
505
506 #[cfg(not(any(target_os = "android", target_os = "ios")))]
507 #[test]
508 fn yes_no_round_trips_through_tfd() {
509 for variant in [YesNo::Yes, YesNo::No] {
510 let encoded: tfd::YesNo = variant.into();
511 let decoded: YesNo = encoded.into();
512 assert_eq!(decoded, variant, "round-trip lost {variant:?}");
513 }
514
515 assert_eq!(YesNo::from(tfd::YesNo::Yes), YesNo::Yes);
516 assert_eq!(YesNo::from(tfd::YesNo::No), YesNo::No);
517 }
518
519 #[cfg(not(any(target_os = "android", target_os = "ios")))]
520 #[test]
521 fn answer_enums_must_be_converted_by_variant_never_by_discriminant() {
522 assert_eq!(OkCancel::Ok as u8, 0);
529 assert_eq!(OkCancel::Cancel as u8, 1);
530 assert_eq!(tfd::OkCancel::Ok as u8, 1);
531 assert_eq!(tfd::OkCancel::Cancel as u8, 0);
532
533 assert_eq!(YesNo::Yes as u8, 0);
534 assert_eq!(YesNo::No as u8, 1);
535 assert_eq!(tfd::YesNo::Yes as u8, 1);
536 assert_eq!(tfd::YesNo::No as u8, 0);
537
538 assert_eq!(tfd::OkCancel::from(OkCancel::Ok), tfd::OkCancel::Ok);
540 assert_eq!(tfd::YesNo::from(YesNo::Yes), tfd::YesNo::Yes);
541 }
542
543 #[cfg(not(any(target_os = "android", target_os = "ios")))]
544 #[test]
545 fn msg_box_icon_maps_to_the_matching_tfd_icon() {
546 let mapping = [
547 (MsgBoxIcon::Info, MessageBoxIcon::Info),
548 (MsgBoxIcon::Warning, MessageBoxIcon::Warning),
549 (MsgBoxIcon::Error, MessageBoxIcon::Error),
550 (MsgBoxIcon::Question, MessageBoxIcon::Question),
551 ];
552 for (ours, theirs) in mapping {
553 assert_eq!(MessageBoxIcon::from(ours), theirs, "wrong icon for {ours:?}");
554 }
555
556 let encoded: Vec<MessageBoxIcon> = mapping
558 .iter()
559 .map(|(ours, _)| MessageBoxIcon::from(*ours))
560 .collect();
561 for (i, a) in encoded.iter().enumerate() {
562 for b in encoded.iter().skip(i + 1) {
563 assert_ne!(a, b, "two MsgBoxIcon variants map to the same tfd icon");
564 }
565 }
566 }
567
568 #[cfg(not(any(target_os = "android", target_os = "ios")))]
573 #[test]
574 fn apply_filter_with_no_patterns_does_not_panic() {
575 let dialog = apply_filter(tfd::FileDialog::new("title"), file_type_list(&[], ""));
576 assert!(dialog.filter_patterns().is_empty());
577 assert_eq!(dialog.filter_description(), "");
578 }
579
580 #[cfg(not(any(target_os = "android", target_os = "ios")))]
581 #[test]
582 fn apply_filter_with_a_default_constructed_string_vec_does_not_panic() {
583 let filter = FileTypeList {
586 document_types: StringVec::new(),
587 document_descriptor: s("no types"),
588 };
589 let dialog = apply_filter(tfd::FileDialog::new("title"), filter);
590 assert!(dialog.filter_patterns().is_empty());
591 assert_eq!(dialog.filter_description(), "no types");
592 }
593
594 #[cfg(not(any(target_os = "android", target_os = "ios")))]
595 #[test]
596 fn apply_filter_preserves_patterns_verbatim_and_in_order() {
597 let filter = file_type_list(&["*.png", "*.jpg", "*.png", ""], "Images");
598 let dialog = apply_filter(tfd::FileDialog::new("title"), filter);
599
600 assert_eq!(dialog.filter_patterns(), &["*.png", "*.jpg", "*.png", ""]);
603 assert_eq!(dialog.filter_description(), "Images");
604 }
605
606 #[cfg(not(any(target_os = "android", target_os = "ios")))]
607 #[test]
608 fn apply_filter_preserves_unicode_patterns() {
609 let patterns = [
610 "*.图片", "*.🎨", "*.مِلَف", "*.e\u{0301}xt", "*.\u{200B}zwsp", ];
616 let filter = file_type_list(&patterns, "Ünïcödé — файлы 🎨");
617 let dialog = apply_filter(tfd::FileDialog::new("title"), filter);
618
619 assert_eq!(dialog.filter_patterns(), &patterns);
620 assert_eq!(dialog.filter_description(), "Ünïcödé — файлы 🎨");
621 }
622
623 #[cfg(not(any(target_os = "android", target_os = "ios")))]
624 #[test]
625 fn apply_filter_does_not_truncate_at_interior_nul_bytes() {
626 let filter = file_type_list(&["*.pn\0g", "\0", "a\u{1}b\u{7f}"], "desc\0ription");
631 let dialog = apply_filter(tfd::FileDialog::new("title"), filter);
632
633 assert_eq!(dialog.filter_patterns(), &["*.pn\0g", "\0", "a\u{1}b\u{7f}"]);
634 assert_eq!(dialog.filter_description(), "desc\0ription");
635 assert_eq!(dialog.filter_patterns()[0].len(), 6); }
637
638 #[cfg(not(any(target_os = "android", target_os = "ios")))]
639 #[test]
640 fn apply_filter_passes_shell_metacharacters_through_unchanged() {
641 let hostile = ["\"", "'", "$(id)", "`id`", "a;b", "x\ny", "--", "*"];
646 let filter = file_type_list(&hostile, "\"quoted\" $(id)");
647 let dialog = apply_filter(tfd::FileDialog::new("title"), filter);
648
649 assert_eq!(dialog.filter_patterns(), &hostile);
650 assert_eq!(dialog.filter_description(), "\"quoted\" $(id)");
651 }
652
653 #[cfg(not(any(target_os = "android", target_os = "ios")))]
654 #[test]
655 fn apply_filter_survives_a_huge_filter_list() {
656 let patterns: Vec<String> = (0..2000).map(|i| format!("*.ext{i}")).collect();
657 let descriptor = "d".repeat(64 * 1024);
658 let filter = FileTypeList {
659 document_types: StringVec::from_vec(
660 patterns.iter().map(|p| s(p)).collect::<Vec<AzString>>(),
661 ),
662 document_descriptor: s(&descriptor),
663 };
664
665 let dialog = apply_filter(tfd::FileDialog::new("title"), filter);
666
667 assert_eq!(dialog.filter_patterns().len(), 2000);
668 assert_eq!(dialog.filter_patterns()[0], "*.ext0");
669 assert_eq!(dialog.filter_patterns()[1999], "*.ext1999");
670 assert_eq!(dialog.filter_description().len(), 64 * 1024);
671 }
672
673 #[cfg(not(any(target_os = "android", target_os = "ios")))]
674 #[test]
675 fn apply_filter_overwrites_rather_than_appends() {
676 let dialog = tfd::FileDialog::new("title");
680 let dialog = apply_filter(dialog, file_type_list(&["*.png"], "Images"));
681 let dialog = apply_filter(dialog, file_type_list(&["*.txt"], "Text"));
682
683 assert_eq!(dialog.filter_patterns(), &["*.txt"]);
684 assert_eq!(dialog.filter_description(), "Text");
685 }
686
687 #[cfg(not(any(target_os = "android", target_os = "ios")))]
688 #[test]
689 fn apply_filter_leaves_the_rest_of_the_dialog_alone() {
690 let dialog = tfd::FileDialog::new("title")
693 .with_path("/tmp/somewhere")
694 .with_multiple_selection(true);
695 let dialog = apply_filter(dialog, file_type_list(&["*.png"], "Images"));
696
697 assert_eq!(dialog.path(), "/tmp/somewhere");
698 assert!(dialog.multiple_selection());
699 assert_eq!(dialog.filter_patterns(), &["*.png"]);
700 }
701
702 #[test]
707 fn string_vec_round_trips_through_into_library_owned_vec() {
708 let original: Vec<AzString> = vec![s("*.png"), s(""), s("*.🎨"), s("a\0b")];
710 let round_tripped = StringVec::from_vec(original.clone()).into_library_owned_vec();
711 assert_eq!(round_tripped, original);
712
713 let empty = StringVec::from_vec(Vec::<AzString>::new()).into_library_owned_vec();
715 assert!(empty.is_empty());
716 }
717
718 #[test]
719 fn file_type_list_clone_is_equal_and_orders_reflexively() {
720 use std::cmp::Ordering;
721
722 let filter = file_type_list(&["*.png", "*.jpg"], "Images");
723 let cloned = filter.clone();
724
725 assert_eq!(cloned, filter);
726 assert_eq!(filter.partial_cmp(&filter), Some(Ordering::Equal));
727 assert_eq!(cloned.document_types.len(), 2);
728 assert_eq!(cloned.document_descriptor.as_str(), "Images");
729 }
730
731 #[test]
732 fn file_type_list_ordering_follows_the_descriptor_when_types_match() {
733 use std::cmp::Ordering;
734
735 let a = file_type_list(&["*.png"], "aaa");
736 let b = file_type_list(&["*.png"], "bbb");
737 assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
738 assert_eq!(b.partial_cmp(&a), Some(Ordering::Greater));
739 assert_ne!(a, b);
740 }
741
742 #[test]
743 fn option_file_type_list_round_trips() {
744 let filter = file_type_list(&["*.png"], "Images");
745
746 let some = OptionFileTypeList::Some(filter.clone());
747 assert!(some.is_some());
748 assert!(!some.is_none());
749 assert_eq!(some.as_option(), Some(&filter));
750 assert_eq!(some.clone().into_option(), Some(filter));
751
752 let none = OptionFileTypeList::None;
753 assert!(none.is_none());
754 assert_eq!(none.as_option(), None);
755 assert_eq!(OptionFileTypeList::default(), OptionFileTypeList::None);
756 }
757
758 #[test]
763 fn modal_entry_points_keep_their_ffi_signatures() {
764 let _ok: fn(AzString, AzString, MsgBoxIcon) = MsgBox::ok;
769 let _ok_cancel: fn(AzString, AzString, MsgBoxIcon, OkCancel) -> OkCancel = MsgBox::ok_cancel;
770 let _yes_no: fn(AzString, AzString, MsgBoxIcon, YesNo) -> YesNo = MsgBox::yes_no;
771 let _info: fn(AzString) = MsgBox::info;
772 let _color: fn(AzString, OptionColorU) -> OptionColorU = ColorPickerDialog::open;
773 let _open_file: fn(AzString, OptionString, OptionFileTypeList) -> OptionString =
774 FileDialog::open_file;
775 let _open_dir: fn(AzString, OptionString) -> OptionString = FileDialog::open_directory;
776 let _open_many: fn(AzString, OptionString, OptionFileTypeList) -> OptionStringVec =
777 FileDialog::open_multiple_files;
778 let _save_file: fn(AzString, OptionString) -> OptionString = FileDialog::save_file;
779 let _msg_box: fn(&str) = msg_box;
780 }
781
782 #[cfg(any(target_os = "android", target_os = "ios"))]
787 #[test]
788 fn mobile_message_boxes_are_silent_no_ops() {
789 MsgBox::ok(s("title"), s("message"), MsgBoxIcon::Error);
790 MsgBox::info(s(""));
791 msg_box("");
792 msg_box("\0\u{1}🎨");
793 }
794
795 #[cfg(any(target_os = "android", target_os = "ios"))]
796 #[test]
797 fn mobile_answer_dialogs_echo_the_default_back() {
798 for default in [OkCancel::Ok, OkCancel::Cancel] {
799 let answer = MsgBox::ok_cancel(s("t"), s("m"), MsgBoxIcon::Question, default);
800 assert_eq!(answer, default);
801 }
802 for default in [YesNo::Yes, YesNo::No] {
803 let answer = MsgBox::yes_no(s("t"), s("m"), MsgBoxIcon::Question, default);
804 assert_eq!(answer, default);
805 }
806 }
807
808 #[cfg(any(target_os = "android", target_os = "ios"))]
809 #[test]
810 fn mobile_color_picker_echoes_the_default_back() {
811 let default = ColorU {
812 r: 1,
813 g: 2,
814 b: 3,
815 a: 4,
816 };
817 let picked = ColorPickerDialog::open(s("t"), OptionColorU::Some(default));
818 match picked.as_option() {
819 Some(c) => {
820 assert_eq!((c.r, c.g, c.b), (1, 2, 3));
821 assert_eq!(c.a, 4);
824 }
825 None => panic!("mobile stub must return the default it was given"),
826 }
827 assert!(ColorPickerDialog::open(s("t"), OptionColorU::None).is_none());
828 }
829
830 #[cfg(any(target_os = "android", target_os = "ios"))]
831 #[test]
832 fn mobile_file_dialogs_report_cancellation() {
833 assert!(FileDialog::open_file(s("t"), OptionString::None, OptionFileTypeList::None).is_none());
834 assert!(FileDialog::open_directory(s("t"), OptionString::None).is_none());
835 assert!(FileDialog::save_file(s("t"), OptionString::Some(s("/tmp"))).is_none());
836 assert!(
837 FileDialog::open_multiple_files(s("t"), OptionString::None, OptionFileTypeList::None)
838 .is_none()
839 );
840 }
841}