1
//! Native OS dialog wrappers (message boxes, file open/save, color picker).
2
//!
3
//! Desktop targets back this with the `tfd` (tiny-file-dialogs) crate; on
4
//! Android / iOS every method is a no-op that returns the "cancelled / safe
5
//! default" answer (there is no equivalent of `tfd` on those platforms from
6
//! a pure-Rust crate, and `tfd 0.1.0` does not cross-compile for them
7
//! anyway). The public type surface is identical on every target so
8
//! consumer code keeps compiling.
9

            
10
use 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")))]
18
use tfd::{DefaultColorValue, MessageBoxIcon};
19

            
20
/// Static-method namespace for `tfd`-backed message-box dialogs.
21
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
22
#[repr(C)]
23
pub struct MsgBox {
24
    pub _reserved: u8,
25
}
26

            
27
/// Static-method namespace for `tfd`-backed file dialogs.
28
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
29
#[repr(C)]
30
pub struct FileDialog {
31
    pub _reserved: u8,
32
}
33

            
34
/// Static-method namespace for the `tfd`-backed color picker.
35
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
36
#[repr(C)]
37
pub struct ColorPickerDialog {
38
    pub _reserved: u8,
39
}
40

            
41
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
42
#[repr(C)]
43
pub enum OkCancel {
44
    Ok,
45
    Cancel,
46
}
47

            
48
#[cfg(not(any(target_os = "android", target_os = "ios")))]
49
impl From<tfd::OkCancel> for OkCancel {
50
    #[inline]
51
    fn from(e: tfd::OkCancel) -> Self {
52
        match e {
53
            tfd::OkCancel::Ok => OkCancel::Ok,
54
            tfd::OkCancel::Cancel => OkCancel::Cancel,
55
        }
56
    }
57
}
58

            
59
#[cfg(not(any(target_os = "android", target_os = "ios")))]
60
impl From<OkCancel> for tfd::OkCancel {
61
    #[inline]
62
    fn from(e: OkCancel) -> Self {
63
        match e {
64
            OkCancel::Ok => tfd::OkCancel::Ok,
65
            OkCancel::Cancel => tfd::OkCancel::Cancel,
66
        }
67
    }
68
}
69

            
70
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
71
#[repr(C)]
72
pub enum YesNo {
73
    Yes,
74
    No,
75
}
76

            
77
#[cfg(not(any(target_os = "android", target_os = "ios")))]
78
impl From<YesNo> for tfd::YesNo {
79
    #[inline]
80
    fn from(e: YesNo) -> Self {
81
        match e {
82
            YesNo::Yes => tfd::YesNo::Yes,
83
            YesNo::No => tfd::YesNo::No,
84
        }
85
    }
86
}
87

            
88
#[cfg(not(any(target_os = "android", target_os = "ios")))]
89
impl From<tfd::YesNo> for YesNo {
90
    #[inline]
91
    fn from(e: tfd::YesNo) -> Self {
92
        match e {
93
            tfd::YesNo::Yes => YesNo::Yes,
94
            tfd::YesNo::No => YesNo::No,
95
        }
96
    }
97
}
98

            
99
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
100
#[repr(C)]
101
pub enum MsgBoxIcon {
102
    Info,
103
    Warning,
104
    Error,
105
    Question,
106
}
107

            
108
#[cfg(not(any(target_os = "android", target_os = "ios")))]
109
impl From<MsgBoxIcon> for MessageBoxIcon {
110
    #[inline]
111
    fn from(e: MsgBoxIcon) -> Self {
112
        match e {
113
            MsgBoxIcon::Info => MessageBoxIcon::Info,
114
            MsgBoxIcon::Warning => MessageBoxIcon::Warning,
115
            MsgBoxIcon::Error => MessageBoxIcon::Error,
116
            MsgBoxIcon::Question => MessageBoxIcon::Question,
117
        }
118
    }
119
}
120

            
121
impl MsgBox {
122
    /// Returns a zero-initialised namespace handle. The struct itself carries
123
    /// no state — instances exist only so the FFI layer can hang static
124
    /// methods off the type.
125
    pub const fn new() -> Self {
126
        Self { _reserved: 0 }
127
    }
128

            
129
    /// "Ok" message box — title, message, icon. Quotes are stripped from the
130
    /// message to work around `tfd` misinterpreting them as shell metacharacters
131
    /// on some platforms.
132
    pub fn ok(title: AzString, message: AzString, icon: MsgBoxIcon) {
133
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
134
        {
135
            let mut msg = message.as_str().to_string();
136
            msg = msg.replace('\"', "");
137
            msg = msg.replace('\'', "");
138
            tfd::MessageBox::new(title.as_str(), &msg)
139
                .with_icon(icon.into())
140
                .run_modal();
141
        }
142
        #[cfg(any(target_os = "android", target_os = "ios"))]
143
        {
144
            let _ = (title, message, icon);
145
        }
146
    }
147

            
148
    /// "Ok / Cancel" message box — title, message, icon, default button.
149
    pub fn ok_cancel(
150
        title: AzString,
151
        message: AzString,
152
        icon: MsgBoxIcon,
153
        default: OkCancel,
154
    ) -> OkCancel {
155
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
156
        {
157
            tfd::MessageBox::new(title.as_str(), message.as_str())
158
                .with_icon(icon.into())
159
                .run_modal_ok_cancel(default.into())
160
                .into()
161
        }
162
        #[cfg(any(target_os = "android", target_os = "ios"))]
163
        {
164
            let _ = (title, message, icon);
165
            default
166
        }
167
    }
168

            
169
    /// "Yes / No" message box — title, message, icon, default button.
170
    pub fn yes_no(
171
        title: AzString,
172
        message: AzString,
173
        icon: MsgBoxIcon,
174
        default: YesNo,
175
    ) -> YesNo {
176
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
177
        {
178
            tfd::MessageBox::new(title.as_str(), message.as_str())
179
                .with_icon(icon.into())
180
                .run_modal_yes_no(default.into())
181
                .into()
182
        }
183
        #[cfg(any(target_os = "android", target_os = "ios"))]
184
        {
185
            let _ = (title, message, icon);
186
            default
187
        }
188
    }
189

            
190
    /// Convenience: "Ok" message box with the title "Info" and an info icon.
191
    pub fn info(content: AzString) {
192
        Self::ok(AzString::from("Info"), content, MsgBoxIcon::Info);
193
    }
194
}
195

            
196
impl ColorPickerDialog {
197
    /// Returns a zero-initialised namespace handle. Static-only — the struct
198
    /// is just a hook for the FFI layer.
199
    pub const fn new() -> Self {
200
        Self { _reserved: 0 }
201
    }
202

            
203
    /// Opens the default color picker dialog. Returns `None` if cancelled.
204
    pub fn open(title: AzString, default_value: OptionColorU) -> OptionColorU {
205
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
206
        {
207
            let rgb = default_value
208
                .into_option()
209
                .map_or([0, 0, 0], |c| [c.r, c.g, c.b]);
210
            let default_color = DefaultColorValue::RGB(rgb);
211
            let result = tfd::ColorChooser::new(title.as_str())
212
                .with_default_color(default_color)
213
                .run_modal();
214
            match result {
215
                Some(r) => OptionColorU::Some(ColorU {
216
                    r: r.1[0],
217
                    g: r.1[1],
218
                    b: r.1[2],
219
                    a: ColorU::ALPHA_OPAQUE,
220
                }),
221
                None => OptionColorU::None,
222
            }
223
        }
224
        #[cfg(any(target_os = "android", target_os = "ios"))]
225
        {
226
            let _ = title;
227
            default_value
228
        }
229
    }
230
}
231

            
232
#[derive(Debug, Clone, PartialEq, PartialOrd)]
233
#[repr(C)]
234
pub struct FileTypeList {
235
    pub document_types: StringVec,
236
    pub document_descriptor: AzString,
237
}
238

            
239
impl_option!(
240
    FileTypeList,
241
    OptionFileTypeList,
242
    copy = false,
243
    [Debug, Clone, PartialEq, PartialOrd]
244
);
245

            
246
/// Apply a [`FileTypeList`] filter to a `tfd::FileDialog`.
247
#[cfg(not(any(target_os = "android", target_os = "ios")))]
248
fn apply_filter(mut dialog: tfd::FileDialog, filter: FileTypeList) -> tfd::FileDialog {
249
    let v = filter.document_types.clone().into_library_owned_vec();
250
    let patterns: Vec<&str> = v.iter().map(|s| s.as_str()).collect();
251
    dialog = dialog.with_filter(&patterns, filter.document_descriptor.as_str());
252
    dialog
253
}
254

            
255
impl FileDialog {
256
    /// Returns a zero-initialised namespace handle. Static-only — the struct
257
    /// is just a hook for the FFI layer.
258
    pub const fn new() -> Self {
259
        Self { _reserved: 0 }
260
    }
261

            
262
    /// Open a single file. Returns `None` if the user cancelled.
263
    pub fn open_file(
264
        title: AzString,
265
        default_path: OptionString,
266
        filter_list: OptionFileTypeList,
267
    ) -> OptionString {
268
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
269
        {
270
            let mut dialog = tfd::FileDialog::new(title.as_str());
271
            if let Some(path) = default_path.as_option() {
272
                dialog = dialog.with_path(path.as_str());
273
            }
274
            if let Some(filter) = filter_list.into_option() {
275
                dialog = apply_filter(dialog, filter);
276
            }
277
            dialog.open_file().map(AzString::from).into()
278
        }
279
        #[cfg(any(target_os = "android", target_os = "ios"))]
280
        {
281
            let _ = (title, default_path, filter_list);
282
            OptionString::None
283
        }
284
    }
285

            
286
    /// Open a directory. Returns `None` if the user cancelled.
287
    pub fn open_directory(title: AzString, default_path: OptionString) -> OptionString {
288
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
289
        {
290
            let mut dialog = tfd::FileDialog::new(title.as_str());
291
            if let Some(path) = default_path.as_option() {
292
                dialog = dialog.with_path(path.as_str());
293
            }
294
            dialog.select_folder().map(AzString::from).into()
295
        }
296
        #[cfg(any(target_os = "android", target_os = "ios"))]
297
        {
298
            let _ = (title, default_path);
299
            OptionString::None
300
        }
301
    }
302

            
303
    /// Open multiple files. Returns `None` if the user cancelled.
304
    pub fn open_multiple_files(
305
        title: AzString,
306
        default_path: OptionString,
307
        filter_list: OptionFileTypeList,
308
    ) -> OptionStringVec {
309
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
310
        {
311
            let mut dialog =
312
                tfd::FileDialog::new(title.as_str()).with_multiple_selection(true);
313
            if let Some(path) = default_path.as_option() {
314
                dialog = dialog.with_path(path.as_str());
315
            }
316
            if let Some(filter) = filter_list.into_option() {
317
                dialog = apply_filter(dialog, filter);
318
            }
319
            dialog.open_files().map(StringVec::from).into()
320
        }
321
        #[cfg(any(target_os = "android", target_os = "ios"))]
322
        {
323
            let _ = (title, default_path, filter_list);
324
            OptionStringVec::None
325
        }
326
    }
327

            
328
    /// Save file dialog. Returns `None` if the user cancelled.
329
    pub fn save_file(title: AzString, default_path: OptionString) -> OptionString {
330
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
331
        {
332
            let mut dialog = tfd::FileDialog::new(title.as_str());
333
            if let Some(path) = default_path.as_option() {
334
                dialog = dialog.with_path(path.as_str());
335
            }
336
            dialog.save_file().map(AzString::from).into()
337
        }
338
        #[cfg(any(target_os = "android", target_os = "ios"))]
339
        {
340
            let _ = (title, default_path);
341
            OptionString::None
342
        }
343
    }
344
}
345

            
346
/// Convenience shim: show a default "Info" message box.
347
pub fn msg_box(content: &str) {
348
    MsgBox::info(AzString::from(content));
349
}