Skip to main content

azul_layout/desktop/
dialogs.rs

1#![allow(missing_copy_implementations)]
2
3use core::ffi::c_void;
4
5use azul_core::window::AzStringPair;
6use azul_css::{impl_option, impl_option_inner, props::basic::color::ColorU, AzString, StringVec};
7use tfd::{DefaultColorValue, MessageBoxIcon};
8
9/// Button dialog wrapper for reserved integration purposes
10#[derive(Debug)]
11pub struct MsgBox {
12    /// reserved pointer (currently nullptr) for potential C extension
13    pub _reserved: *mut c_void,
14}
15
16/// File dialog wrapper for reserved integration purposes
17#[derive(Debug)]
18pub struct FileDialog {
19    /// reserved pointer (currently nullptr) for potential C extension
20    pub _reserved: *mut c_void,
21}
22
23/// Color picker dialog wrapper for reserved integration purposes
24#[derive(Debug)]
25pub struct ColorPickerDialog {
26    /// reserved pointer (currently nullptr) for potential C extension
27    pub _reserved: *mut c_void,
28}
29
30#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
31#[repr(C)]
32pub enum OkCancel {
33    Ok,
34    Cancel,
35}
36
37impl From<tfd::OkCancel> for OkCancel {
38    #[inline]
39    fn from(e: tfd::OkCancel) -> Self {
40        match e {
41            tfd::OkCancel::Ok => OkCancel::Ok,
42            tfd::OkCancel::Cancel => OkCancel::Cancel,
43        }
44    }
45}
46
47impl From<OkCancel> for tfd::OkCancel {
48    #[inline]
49    fn from(e: OkCancel) -> Self {
50        match e {
51            OkCancel::Ok => tfd::OkCancel::Ok,
52            OkCancel::Cancel => tfd::OkCancel::Cancel,
53        }
54    }
55}
56
57/// "Ok / Cancel" MsgBox (title, message, icon, default)
58pub fn msg_box_ok_cancel(
59    title: &str,
60    message: &str,
61    icon: MessageBoxIcon,
62    default: OkCancel,
63) -> OkCancel {
64    let msg_box = tfd::MessageBox::new(title, message)
65        .with_icon(icon)
66        .run_modal_ok_cancel(default.into());
67    msg_box.into()
68}
69
70/// Yes or No result, returned from the `msg_box_yes_no` function
71#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
72#[repr(C)]
73pub enum YesNo {
74    Yes,
75    No,
76}
77
78impl 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
88impl From<tfd::YesNo> for YesNo {
89    #[inline]
90    fn from(e: tfd::YesNo) -> Self {
91        match e {
92            tfd::YesNo::Yes => YesNo::Yes,
93            tfd::YesNo::No => YesNo::No,
94        }
95    }
96}
97
98/// MsgBox icon to use in the `msg_box_*` functions
99#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
100#[repr(C)]
101pub enum MsgBoxIcon {
102    Info,
103    Warning,
104    Error,
105    Question,
106}
107
108impl From<MsgBoxIcon> for MessageBoxIcon {
109    #[inline]
110    fn from(e: MsgBoxIcon) -> Self {
111        match e {
112            MsgBoxIcon::Info => MessageBoxIcon::Info,
113            MsgBoxIcon::Warning => MessageBoxIcon::Warning,
114            MsgBoxIcon::Error => MessageBoxIcon::Error,
115            MsgBoxIcon::Question => MessageBoxIcon::Question,
116        }
117    }
118}
119
120/// "Y/N" MsgBox (title, message, icon, default)
121pub fn msg_box_yes_no(title: &str, message: &str, icon: MessageBoxIcon, default: YesNo) -> YesNo {
122    let msg_box = tfd::MessageBox::new(title, message)
123        .with_icon(icon)
124        .run_modal_yes_no(default.into());
125    msg_box.into()
126}
127
128/// "Ok" MsgBox (title, message, icon)
129pub fn msg_box_ok(title: &str, message: &str, icon: MessageBoxIcon) {
130    let mut msg = message.to_string();
131
132    msg = msg.replace('\"', "");
133    msg = msg.replace('\'', "");
134
135    tfd::MessageBox::new(title, &msg)
136        .with_icon(icon)
137        .run_modal();
138}
139
140/// Wrapper around `message_box_ok` with the default title "Info" + an info icon.
141pub fn msg_box(content: &str) {
142    msg_box_ok("Info", content, MessageBoxIcon::Info);
143}
144
145/// Opens the default color picker dialog
146pub fn color_picker_dialog(title: &str, default_value: Option<ColorU>) -> Option<ColorU> {
147    let rgb = default_value.map_or([0, 0, 0], |c| [c.r, c.g, c.b]);
148
149    let default_color = DefaultColorValue::RGB(rgb);
150    let result = tfd::ColorChooser::new(title)
151        .with_default_color(default_color)
152        .run_modal()?;
153
154    Some(ColorU {
155        r: result.1[0],
156        g: result.1[1],
157        b: result.1[2],
158        a: ColorU::ALPHA_OPAQUE,
159    })
160}
161
162#[derive(Debug, Clone, PartialEq, PartialOrd)]
163#[repr(C)]
164pub struct FileTypeList {
165    pub document_types: StringVec,
166    pub document_descriptor: AzString,
167}
168
169impl_option!(
170    FileTypeList,
171    OptionFileTypeList,
172    copy = false,
173    [Debug, Clone, PartialEq, PartialOrd]
174);
175
176/// Open a single file, returns `None` if the user canceled the dialog.
177///
178/// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow
179/// "doc" and "docx" files
180pub fn open_file_dialog(
181    title: &str,
182    default_path: Option<&str>,
183    filter_list: Option<FileTypeList>,
184) -> Option<AzString> {
185    let mut dialog = tfd::FileDialog::new(title);
186
187    if let Some(path) = default_path {
188        dialog = dialog.with_path(path);
189    }
190
191    if let Some(filter) = filter_list {
192        let v = filter.document_types.clone().into_library_owned_vec();
193
194        let patterns: Vec<&str> = v.iter().map(|s| s.as_str()).collect();
195
196        dialog = dialog.with_filter(&patterns, filter.document_descriptor.as_str());
197    }
198
199    dialog.open_file().map(|s| s.into())
200}
201
202/// Open a directory, returns `None` if the user canceled the dialog
203pub fn open_directory_dialog(title: &str, default_path: Option<&str>) -> Option<AzString> {
204    let mut dialog = tfd::FileDialog::new(title);
205
206    if let Some(path) = default_path {
207        dialog = dialog.with_path(path);
208    }
209
210    dialog.select_folder().map(|s| s.into())
211}
212
213/// Open multiple files at once, returns `None` if the user canceled the dialog,
214/// otherwise returns the `Vec<String>` with the given file paths
215///
216/// Filters are the file extensions, i.e. `Some(&["doc", "docx"])` to only allow
217/// "doc" and "docx" files
218pub fn open_multiple_files_dialog(
219    title: &str,
220    default_path: Option<&str>,
221    filter_list: Option<FileTypeList>,
222) -> Option<StringVec> {
223    let mut dialog = tfd::FileDialog::new(title).with_multiple_selection(true);
224
225    if let Some(path) = default_path {
226        dialog = dialog.with_path(path);
227    }
228
229    if let Some(filter) = filter_list {
230        let v = filter.document_types.clone().into_library_owned_vec();
231
232        let patterns: Vec<&str> = v.iter().map(|s| s.as_str()).collect();
233
234        dialog = dialog.with_filter(&patterns, filter.document_descriptor.as_str());
235    }
236
237    dialog.open_files().map(|s| s.into())
238}
239
240/// Opens a save file dialog, returns `None` if the user canceled the dialog
241pub fn save_file_dialog(title: &str, default_path: Option<&str>) -> Option<AzString> {
242    let mut dialog = tfd::FileDialog::new(title);
243
244    if let Some(path) = default_path {
245        dialog = dialog.with_path(path);
246    }
247
248    dialog.save_file().map(|s| s.into())
249}