1use std::path::{Path, PathBuf};
13
14#[cfg(target_os = "macos")]
16mod macos;
17#[cfg(all(unix, not(target_os = "macos")))]
18mod unix;
19#[cfg(target_os = "windows")]
20mod windows;
21#[cfg(target_os = "ios")]
22mod ios;
23#[cfg(target_os = "android")]
24mod android;
25
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum MessageBoxIcon {
28 Info,
29 Warning,
30 Error,
31 Question,
32}
33
34impl MessageBoxIcon {
35 fn to_str(&self) -> &'static str {
36 match *self {
37 MessageBoxIcon::Info => "info",
38 MessageBoxIcon::Warning => "warning",
39 MessageBoxIcon::Error => "error",
40 MessageBoxIcon::Question => "question",
41 }
42 }
43}
44
45#[derive(Debug, PartialEq, Copy, Clone)]
46pub enum OkCancel {
47 Cancel = 0,
48 Ok = 1,
49}
50
51#[derive(Debug, PartialEq, Copy, Clone)]
52pub enum YesNo {
53 No = 0,
54 Yes = 1,
55}
56
57#[derive(Debug, PartialEq, Copy, Clone)]
58pub enum YesNoCancel {
59 Cancel = 0,
60 Yes = 1,
61 No = 2,
62}
63
64pub struct Dialog {
66 title: String,
67 message: String,
68}
69
70impl Dialog {
71 pub fn new<S: Into<String>, Q: Into<String>>(title: S, message: Q) -> Self {
72 Self {
73 title: title.into(),
74 message: message.into(),
75 }
76 }
77
78 pub fn title(&self) -> &str {
79 &self.title
80 }
81
82 pub fn message(&self) -> &str {
83 &self.message
84 }
85
86 pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
87 self.title = title.into();
88 self
89 }
90
91 pub fn with_message<S: Into<String>>(mut self, message: S) -> Self {
92 self.message = message.into();
93 self
94 }
95
96 fn sanitize_input(input: &str) -> String {
98 input
99 .replace("\"", "\\\"")
100 .replace("'", "\\'")
101 .replace("`", "\\`")
102 }
103
104 fn verify_path(path: &str) -> Option<PathBuf> {
106 let path = Path::new(path);
107 if path.exists() {
108 Some(path.to_path_buf())
109 } else {
110 None
111 }
112 }
113}
114
115pub struct MessageBox {
117 dialog: Dialog,
118 icon: MessageBoxIcon,
119}
120
121impl MessageBox {
122 pub fn new<S: Into<String>>(title: S, message: S) -> Self {
123 Self {
124 dialog: Dialog::new(title, message),
125 icon: MessageBoxIcon::Info,
126 }
127 }
128
129 pub fn with_icon(mut self, icon: MessageBoxIcon) -> Self {
130 self.icon = icon;
131 self
132 }
133
134 pub fn icon(&self) -> MessageBoxIcon {
135 self.icon
136 }
137
138 pub fn run_modal(&self) {
139 #[cfg(target_os = "macos")]
140 macos::message_box_ok(self);
141
142 #[cfg(all(unix, not(target_os = "macos")))]
143 unix::message_box_ok(self);
144
145 #[cfg(target_os = "windows")]
146 windows::message_box_ok(self);
147 }
148
149 pub fn run_modal_ok_cancel(&self, default: OkCancel) -> OkCancel {
150 #[cfg(target_os = "macos")]
151 return macos::message_box_ok_cancel(self, default);
152
153 #[cfg(all(unix, not(target_os = "macos")))]
154 return unix::message_box_ok_cancel(self, default);
155
156 #[cfg(target_os = "windows")]
157 return windows::message_box_ok_cancel(self, default);
158
159 #[allow(unreachable_code)]
160 OkCancel::Cancel
161 }
162
163 pub fn run_modal_yes_no(&self, default: YesNo) -> YesNo {
164 #[cfg(target_os = "macos")]
165 return macos::message_box_yes_no(self, default);
166
167 #[cfg(all(unix, not(target_os = "macos")))]
168 return unix::message_box_yes_no(self, default);
169
170 #[cfg(target_os = "windows")]
171 return windows::message_box_yes_no(self, default);
172
173 #[allow(unreachable_code)]
174 YesNo::No
175 }
176
177 pub fn run_modal_yes_no_cancel(&self, default: YesNoCancel) -> YesNoCancel {
178 #[cfg(target_os = "macos")]
179 return macos::message_box_yes_no_cancel(self, default);
180
181 #[cfg(all(unix, not(target_os = "macos")))]
182 return unix::message_box_yes_no_cancel(self, default);
183
184 #[cfg(target_os = "windows")]
185 return windows::message_box_yes_no_cancel(self, default);
186
187 #[allow(unreachable_code)]
188 YesNoCancel::Cancel
189 }
190}
191
192pub struct InputBox {
194 dialog: Dialog,
195 default_value: Option<String>,
196 is_password: bool,
197}
198
199impl InputBox {
200 pub fn new<S: Into<String>>(title: S, message: S) -> Self {
201 Self {
202 dialog: Dialog::new(title, message),
203 default_value: None,
204 is_password: false,
205 }
206 }
207
208 pub fn with_default<S: Into<String>>(mut self, default: S) -> Self {
209 self.default_value = Some(default.into());
210 self
211 }
212
213 pub fn password(mut self, is_password: bool) -> Self {
214 self.is_password = is_password;
215 self
216 }
217
218 pub fn default_value(&self) -> Option<&str> {
219 self.default_value.as_deref()
220 }
221
222 pub fn is_password(&self) -> bool {
223 self.is_password
224 }
225
226 pub fn run_modal(&self) -> Option<String> {
227 #[cfg(target_os = "macos")]
228 return macos::input_box(self);
229
230 #[cfg(all(unix, not(target_os = "macos")))]
231 return unix::input_box(self);
232
233 #[cfg(target_os = "windows")]
234 return windows::input_box(self);
235
236 #[allow(unreachable_code)]
237 None
238 }
239}
240
241pub struct FileDialog {
243 dialog: Dialog,
244 path: String,
245 filter_patterns: Vec<String>,
246 filter_description: String,
247 multiple_selection: bool,
248}
249
250impl FileDialog {
251 pub fn new<S: Into<String>>(title: S) -> Self {
252 Self {
253 dialog: Dialog::new(title, ""),
254 path: String::new(),
255 filter_patterns: Vec::new(),
256 filter_description: String::new(),
257 multiple_selection: false,
258 }
259 }
260
261 pub fn with_path<S: Into<String>>(mut self, path: S) -> Self {
262 self.path = path.into();
263 self
264 }
265
266 pub fn with_filter<S: Into<String>>(mut self, patterns: &[&str], description: S) -> Self {
267 self.filter_patterns = patterns.iter().map(|&s| s.to_string()).collect();
268 self.filter_description = description.into();
269 self
270 }
271
272 pub fn with_multiple_selection(mut self, allow_multi: bool) -> Self {
273 self.multiple_selection = allow_multi;
274 self
275 }
276
277 pub fn path(&self) -> &str {
278 &self.path
279 }
280
281 pub fn filter_patterns(&self) -> &[String] {
282 &self.filter_patterns
283 }
284
285 pub fn filter_description(&self) -> &str {
286 &self.filter_description
287 }
288
289 pub fn multiple_selection(&self) -> bool {
290 self.multiple_selection
291 }
292
293 pub fn save_file(&self) -> Option<String> {
294 #[cfg(target_os = "macos")]
295 return macos::save_file_dialog(self);
296
297 #[cfg(all(unix, not(target_os = "macos")))]
298 return unix::save_file_dialog(self);
299
300 #[cfg(target_os = "windows")]
301 return windows::save_file_dialog(self);
302
303 #[allow(unreachable_code)]
304 None
305 }
306
307 pub fn open_file(&self) -> Option<String> {
308 self.open_files().and_then(|v| v.into_iter().next())
309 }
310
311 pub fn open_files(&self) -> Option<Vec<String>> {
312 #[cfg(target_os = "macos")]
313 return macos::open_file_dialog(self);
314
315 #[cfg(all(unix, not(target_os = "macos")))]
316 return unix::open_file_dialog(self);
317
318 #[cfg(target_os = "windows")]
319 return windows::open_file_dialog(self);
320
321 #[allow(unreachable_code)]
322 None
323 }
324
325 pub fn select_folder(&self) -> Option<String> {
326 #[cfg(target_os = "macos")]
327 return macos::select_folder_dialog(self);
328
329 #[cfg(all(unix, not(target_os = "macos")))]
330 return unix::select_folder_dialog(self);
331
332 #[cfg(target_os = "windows")]
333 return windows::select_folder_dialog(self);
334
335 #[allow(unreachable_code)]
336 None
337 }
338}
339
340pub enum DefaultColorValue {
341 Hex(String),
342 RGB([u8; 3]),
343}
344
345pub struct ColorChooser {
346 dialog: Dialog,
347 default_color: DefaultColorValue,
348}
349
350impl ColorChooser {
351 pub fn new<S: Into<String>>(title: S) -> Self {
352 Self {
353 dialog: Dialog::new(title, String::new()),
354 default_color: DefaultColorValue::RGB([0, 0, 0]),
355 }
356 }
357
358 pub fn with_default_color(mut self, default: DefaultColorValue) -> Self {
359 self.default_color = default;
360 self
361 }
362
363 pub fn default_color(&self) -> &DefaultColorValue {
364 &self.default_color
365 }
366
367 pub fn run_modal(&self) -> Option<(String, [u8; 3])> {
368 #[cfg(target_os = "macos")]
369 return macos::color_chooser_dialog(self);
370
371 #[cfg(all(unix, not(target_os = "macos")))]
372 return unix::color_chooser_dialog(self);
373
374 #[cfg(target_os = "windows")]
375 return windows::color_chooser_dialog(self);
376
377 #[allow(unreachable_code)]
378 None
379 }
380}
381
382pub struct Notification {
383 title: String,
384 message: String,
385 subtitle: Option<String>,
386 sound: Option<String>,
387}
388
389impl Notification {
390 pub fn new<S: Into<String>>(title: S, message: S) -> Self {
391 Self {
392 title: title.into(),
393 message: message.into(),
394 subtitle: None,
395 sound: None,
396 }
397 }
398
399 pub fn with_subtitle<S: Into<String>>(mut self, subtitle: S) -> Self {
400 self.subtitle = Some(subtitle.into());
401 self
402 }
403
404 pub fn with_sound<S: Into<String>>(mut self, sound: S) -> Self {
405 self.sound = Some(sound.into());
406 self
407 }
408
409 pub fn title(&self) -> &str {
410 &self.title
411 }
412
413 pub fn message(&self) -> &str {
414 &self.message
415 }
416
417 pub fn subtitle(&self) -> Option<&str> {
418 self.subtitle.as_deref()
419 }
420
421 pub fn sound(&self) -> Option<&str> {
422 self.sound.as_deref()
423 }
424
425 pub fn show(&self) -> bool {
426 #[cfg(target_os = "macos")]
427 return macos::notification(self);
428
429 #[cfg(all(unix, not(target_os = "macos")))]
430 return unix::notification(self);
431
432 #[cfg(target_os = "windows")]
433 return windows::notification(self);
434
435 #[allow(unreachable_code)]
436 false
437 }
438}
439
440fn hex_to_rgb(hex: &str) -> [u8; 3] {
442 let hex = hex.trim_start_matches('#');
443 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
444 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
445 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
446 [r, g, b]
447}
448
449fn rgb_to_hex(rgb: &[u8; 3]) -> String {
450 format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])
451}