tauri_plugin_dialog/lib.rs
1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Native system dialogs for opening and saving files along with message dialogs.
6
7#![doc(
8 html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
9 html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
10)]
11
12use serde::Serialize;
13use tauri::{
14 plugin::{Builder, TauriPlugin},
15 Manager, Runtime,
16};
17
18use std::{
19 path::{Path, PathBuf},
20 sync::mpsc::sync_channel,
21};
22
23pub use models::*;
24
25pub use tauri_plugin_fs::FilePath;
26#[cfg(desktop)]
27mod desktop;
28#[cfg(mobile)]
29mod mobile;
30
31mod commands;
32mod error;
33mod models;
34
35pub use error::{Error, Result};
36
37#[cfg(desktop)]
38use desktop::*;
39#[cfg(mobile)]
40use mobile::*;
41
42#[cfg(desktop)]
43pub use desktop::Dialog;
44#[cfg(mobile)]
45pub use mobile::Dialog;
46
47pub(crate) const OK: &str = "Ok";
48pub(crate) const CANCEL: &str = "Cancel";
49pub(crate) const YES: &str = "Yes";
50pub(crate) const NO: &str = "No";
51
52macro_rules! blocking_fn {
53 ($self:ident, $fn:ident) => {{
54 let (tx, rx) = sync_channel(0);
55 let cb = move |response| {
56 tx.send(response).unwrap();
57 };
58 $self.$fn(cb);
59 rx.recv().unwrap()
60 }};
61}
62
63/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the dialog APIs.
64pub trait DialogExt<R: Runtime> {
65 fn dialog(&self) -> &Dialog<R>;
66}
67
68impl<R: Runtime, T: Manager<R>> crate::DialogExt<R> for T {
69 fn dialog(&self) -> &Dialog<R> {
70 self.state::<Dialog<R>>().inner()
71 }
72}
73
74impl<R: Runtime> Dialog<R> {
75 /// Create a new messaging dialog builder.
76 /// The dialog can optionally ask the user for confirmation or include an OK button.
77 ///
78 /// # Examples
79 ///
80 /// - Message dialog:
81 ///
82 /// ```
83 /// use tauri_plugin_dialog::DialogExt;
84 ///
85 /// tauri::Builder::default()
86 /// .setup(|app| {
87 /// app
88 /// .dialog()
89 /// .message("Tauri is Awesome!")
90 /// .show(|_| {
91 /// println!("dialog closed");
92 /// });
93 /// Ok(())
94 /// });
95 /// ```
96 ///
97 /// - Ask dialog:
98 ///
99 /// ```
100 /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
101 ///
102 /// tauri::Builder::default()
103 /// .setup(|app| {
104 /// app.dialog()
105 /// .message("Are you sure?")
106 /// .buttons(MessageDialogButtons::OkCancelCustom("Yes", "No"))
107 /// .show(|yes| {
108 /// println!("user said {}", if yes { "yes" } else { "no" });
109 /// });
110 /// Ok(())
111 /// });
112 /// ```
113 ///
114 /// - Message dialog with OK button:
115 ///
116 /// ```
117 /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
118 ///
119 /// tauri::Builder::default()
120 /// .setup(|app| {
121 /// app.dialog()
122 /// .message("Job completed successfully")
123 /// .buttons(MessageDialogButtons::Ok)
124 /// .show(|_| {
125 /// println!("dialog closed");
126 /// });
127 /// Ok(())
128 /// });
129 /// ```
130 ///
131 /// # `show` vs `blocking_show`
132 ///
133 /// The dialog builder includes two separate APIs for rendering the dialog: `show` and `blocking_show`.
134 /// The `show` function is asynchronous and takes a closure to be executed when the dialog is closed.
135 /// To block the current thread until the user acted on the dialog, you can use `blocking_show`,
136 /// but note that it cannot be executed on the main thread as it will freeze your application.
137 ///
138 /// ```
139 /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
140 ///
141 /// tauri::Builder::default()
142 /// .setup(|app| {
143 /// let handle = app.handle().clone();
144 /// std::thread::spawn(move || {
145 /// let yes = handle.dialog()
146 /// .message("Are you sure?")
147 /// .buttons(MessageDialogButtons::OkCancelCustom("Yes", "No"))
148 /// .blocking_show();
149 /// });
150 ///
151 /// Ok(())
152 /// });
153 /// ```
154 pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> {
155 MessageDialogBuilder::new(
156 self.clone(),
157 self.app_handle().package_info().name.clone(),
158 message,
159 )
160 }
161
162 /// Creates a new builder for dialogs that lets the user select file(s) or folder(s).
163 pub fn file(&self) -> FileDialogBuilder<R> {
164 FileDialogBuilder::new(self.clone())
165 }
166}
167
168/// Initializes the plugin.
169pub fn init<R: Runtime>() -> TauriPlugin<R> {
170 #[allow(unused_mut)]
171 let mut builder = Builder::new("dialog");
172
173 // Dialogs are implemented natively on Android
174 #[cfg(not(target_os = "android"))]
175 {
176 builder = builder.js_init_script(include_str!("init-iife.js").to_string());
177 }
178
179 builder
180 .invoke_handler(tauri::generate_handler![
181 commands::open,
182 commands::save,
183 commands::message,
184 commands::ask,
185 commands::confirm
186 ])
187 .setup(|app, api| {
188 #[cfg(mobile)]
189 let dialog = mobile::init(app, api)?;
190 #[cfg(desktop)]
191 let dialog = desktop::init(app, api)?;
192 app.manage(dialog);
193 Ok(())
194 })
195 .build()
196}
197
198/// A builder for message dialogs.
199pub struct MessageDialogBuilder<R: Runtime> {
200 #[allow(dead_code)]
201 pub(crate) dialog: Dialog<R>,
202 pub(crate) title: String,
203 pub(crate) message: String,
204 pub(crate) kind: MessageDialogKind,
205 pub(crate) buttons: MessageDialogButtons,
206 #[cfg(desktop)]
207 pub(crate) parent: Option<crate::desktop::WindowHandle>,
208}
209
210/// Payload for the message dialog mobile API.
211#[cfg(mobile)]
212#[derive(Serialize)]
213#[serde(rename_all = "camelCase")]
214pub(crate) struct MessageDialogPayload<'a> {
215 title: &'a String,
216 message: &'a String,
217 kind: &'a MessageDialogKind,
218 ok_button_label: Option<&'a str>,
219 no_button_label: Option<&'a str>,
220 cancel_button_label: Option<&'a str>,
221}
222
223// raw window handle :(
224unsafe impl<R: Runtime> Send for MessageDialogBuilder<R> {}
225
226impl<R: Runtime> MessageDialogBuilder<R> {
227 /// Creates a new message dialog builder.
228 pub fn new(dialog: Dialog<R>, title: impl Into<String>, message: impl Into<String>) -> Self {
229 Self {
230 dialog,
231 title: title.into(),
232 message: message.into(),
233 kind: Default::default(),
234 buttons: Default::default(),
235 #[cfg(desktop)]
236 parent: None,
237 }
238 }
239
240 #[cfg(mobile)]
241 pub(crate) fn payload(&self) -> MessageDialogPayload<'_> {
242 let (ok_button_label, no_button_label, cancel_button_label) = match &self.buttons {
243 MessageDialogButtons::Ok => (Some(OK), None, None),
244 MessageDialogButtons::OkCancel => (Some(OK), None, Some(CANCEL)),
245 MessageDialogButtons::YesNo => (Some(YES), Some(NO), None),
246 MessageDialogButtons::YesNoCancel => (Some(YES), Some(NO), Some(CANCEL)),
247 MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), None, None),
248 MessageDialogButtons::OkCancelCustom(ok, cancel) => {
249 (Some(ok.as_str()), None, Some(cancel.as_str()))
250 }
251 MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => {
252 (Some(yes.as_str()), Some(no.as_str()), Some(cancel.as_str()))
253 }
254 };
255 MessageDialogPayload {
256 title: &self.title,
257 message: &self.message,
258 kind: &self.kind,
259 ok_button_label,
260 no_button_label,
261 cancel_button_label,
262 }
263 }
264
265 /// Sets the dialog title.
266 pub fn title(mut self, title: impl Into<String>) -> Self {
267 self.title = title.into();
268 self
269 }
270
271 /// Set parent windows explicitly (optional)
272 #[cfg(desktop)]
273 pub fn parent<W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle>(
274 mut self,
275 parent: &W,
276 ) -> Self {
277 if let (Ok(window_handle), Ok(display_handle)) =
278 (parent.window_handle(), parent.display_handle())
279 {
280 self.parent.replace(crate::desktop::WindowHandle::new(
281 window_handle.as_raw(),
282 display_handle.as_raw(),
283 ));
284 }
285 self
286 }
287
288 /// Sets the dialog buttons.
289 pub fn buttons(mut self, buttons: MessageDialogButtons) -> Self {
290 self.buttons = buttons;
291 self
292 }
293
294 /// Set type of a dialog.
295 ///
296 /// Depending on the system it can result in type specific icon to show up,
297 /// the will inform user it message is a error, warning or just information.
298 pub fn kind(mut self, kind: MessageDialogKind) -> Self {
299 self.kind = kind;
300 self
301 }
302
303 /// Shows a message dialog
304 ///
305 /// Returns `true` if the user pressed the OK/Yes button,
306 pub fn show<F: FnOnce(bool) + Send + 'static>(self, f: F) {
307 let ok_label = match &self.buttons {
308 MessageDialogButtons::OkCustom(ok) => Some(ok.clone()),
309 MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()),
310 MessageDialogButtons::YesNoCancelCustom(yes, _, _) => Some(yes.clone()),
311 _ => None,
312 };
313
314 show_message_dialog(self, move |res| {
315 let sucess = match res {
316 MessageDialogResult::Ok | MessageDialogResult::Yes => true,
317 MessageDialogResult::Custom(s) => {
318 ok_label.map_or(s == OK, |ok_label| ok_label == s)
319 }
320 _ => false,
321 };
322
323 f(sucess)
324 })
325 }
326
327 /// Shows a message dialog and returns the button that was pressed.
328 ///
329 /// Returns a [`MessageDialogResult`] enum that indicates which button was pressed.
330 pub fn show_with_result<F: FnOnce(MessageDialogResult) + Send + 'static>(self, f: F) {
331 show_message_dialog(self, f)
332 }
333
334 /// Shows a message dialog.
335 ///
336 /// Returns `true` if the user pressed the OK/Yes button,
337 ///
338 /// This is a blocking operation,
339 /// and should *NOT* be used when running on the main thread context.
340 pub fn blocking_show(self) -> bool {
341 blocking_fn!(self, show)
342 }
343
344 /// Shows a message dialog and returns the button that was pressed.
345 ///
346 /// Returns a [`MessageDialogResult`] enum that indicates which button was pressed.
347 ///
348 /// This is a blocking operation,
349 /// and should *NOT* be used when running on the main thread context.
350 pub fn blocking_show_with_result(self) -> MessageDialogResult {
351 blocking_fn!(self, show_with_result)
352 }
353}
354#[derive(Debug, Serialize)]
355pub(crate) struct Filter {
356 pub name: String,
357 pub extensions: Vec<String>,
358}
359
360/// The file dialog builder.
361///
362/// Constructs file picker dialogs that can select single/multiple files or directories.
363#[derive(Debug)]
364pub struct FileDialogBuilder<R: Runtime> {
365 #[allow(dead_code)]
366 pub(crate) dialog: Dialog<R>,
367 pub(crate) filters: Vec<Filter>,
368 pub(crate) starting_directory: Option<PathBuf>,
369 pub(crate) file_name: Option<String>,
370 pub(crate) title: Option<String>,
371 pub(crate) can_create_directories: Option<bool>,
372 #[cfg(desktop)]
373 pub(crate) parent: Option<crate::desktop::WindowHandle>,
374}
375
376#[cfg(mobile)]
377#[derive(Serialize)]
378#[serde(rename_all = "camelCase")]
379pub(crate) struct FileDialogPayload<'a> {
380 file_name: &'a Option<String>,
381 filters: &'a Vec<Filter>,
382 multiple: bool,
383}
384
385// raw window handle :(
386unsafe impl<R: Runtime> Send for FileDialogBuilder<R> {}
387
388impl<R: Runtime> FileDialogBuilder<R> {
389 /// Gets the default file dialog builder.
390 pub fn new(dialog: Dialog<R>) -> Self {
391 Self {
392 dialog,
393 filters: Vec::new(),
394 starting_directory: None,
395 file_name: None,
396 title: None,
397 can_create_directories: None,
398 #[cfg(desktop)]
399 parent: None,
400 }
401 }
402
403 #[cfg(mobile)]
404 pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
405 FileDialogPayload {
406 file_name: &self.file_name,
407 filters: &self.filters,
408 multiple,
409 }
410 }
411
412 /// Add file extension filter. Takes in the name of the filter, and list of extensions
413 #[must_use]
414 pub fn add_filter(mut self, name: impl Into<String>, extensions: &[&str]) -> Self {
415 self.filters.push(Filter {
416 name: name.into(),
417 extensions: extensions.iter().map(|e| e.to_string()).collect(),
418 });
419 self
420 }
421
422 /// Set starting directory of the dialog.
423 #[must_use]
424 pub fn set_directory<P: AsRef<Path>>(mut self, directory: P) -> Self {
425 self.starting_directory.replace(directory.as_ref().into());
426 self
427 }
428
429 /// Set starting file name of the dialog.
430 #[must_use]
431 pub fn set_file_name(mut self, file_name: impl Into<String>) -> Self {
432 self.file_name.replace(file_name.into());
433 self
434 }
435
436 /// Sets the parent window of the dialog.
437 #[cfg(desktop)]
438 #[must_use]
439 pub fn set_parent<
440 W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle,
441 >(
442 mut self,
443 parent: &W,
444 ) -> Self {
445 if let (Ok(window_handle), Ok(display_handle)) =
446 (parent.window_handle(), parent.display_handle())
447 {
448 self.parent.replace(crate::desktop::WindowHandle::new(
449 window_handle.as_raw(),
450 display_handle.as_raw(),
451 ));
452 }
453 self
454 }
455
456 /// Set the title of the dialog.
457 #[must_use]
458 pub fn set_title(mut self, title: impl Into<String>) -> Self {
459 self.title.replace(title.into());
460 self
461 }
462
463 /// Set whether it should be possible to create new directories in the dialog. Enabled by default. **macOS only**.
464 pub fn set_can_create_directories(mut self, can: bool) -> Self {
465 self.can_create_directories.replace(can);
466 self
467 }
468
469 /// Shows the dialog to select a single file.
470 /// This is not a blocking operation,
471 /// and should be used when running on the main thread to avoid deadlocks with the event loop.
472 ///
473 /// For usage in other contexts such as commands, prefer [`Self::pick_file`].
474 ///
475 /// # Examples
476 ///
477 /// ```
478 /// use tauri_plugin_dialog::DialogExt;
479 /// tauri::Builder::default()
480 /// .setup(|app| {
481 /// app.dialog().file().pick_file(|file_path| {
482 /// // do something with the optional file path here
483 /// // the file path is `None` if the user closed the dialog
484 /// });
485 /// Ok(())
486 /// });
487 /// ```
488 pub fn pick_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
489 pick_file(self, f)
490 }
491
492 /// Shows the dialog to select multiple files.
493 /// This is not a blocking operation,
494 /// and should be used when running on the main thread to avoid deadlocks with the event loop.
495 ///
496 /// # Reading the files
497 ///
498 /// The file paths cannot be read directly on Android as they are behind a content URI.
499 /// The recommended way to read the files is using the [`fs`](https://v2.tauri.app/plugin/file-system/) plugin:
500 ///
501 /// ```
502 /// use tauri_plugin_dialog::DialogExt;
503 /// use tauri_plugin_fs::FsExt;
504 /// tauri::Builder::default()
505 /// .setup(|app| {
506 /// let handle = app.handle().clone();
507 /// app.dialog().file().pick_file(move |file_path| {
508 /// let Some(path) = file_path else { return };
509 /// let Ok(contents) = handle.fs().read_to_string(path) else {
510 /// eprintln!("failed to read file, <todo add error handling!>");
511 /// return;
512 /// };
513 /// });
514 /// Ok(())
515 /// });
516 /// ```
517 ///
518 /// See <https://developer.android.com/guide/topics/providers/content-provider-basics> for more information.
519 ///
520 /// # Examples
521 ///
522 /// ```
523 /// use tauri_plugin_dialog::DialogExt;
524 /// tauri::Builder::default()
525 /// .setup(|app| {
526 /// app.dialog().file().pick_files(|file_paths| {
527 /// // do something with the optional file paths here
528 /// // the file paths value is `None` if the user closed the dialog
529 /// });
530 /// Ok(())
531 /// });
532 /// ```
533 pub fn pick_files<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
534 pick_files(self, f)
535 }
536
537 /// Shows the dialog to select a single folder.
538 /// This is not a blocking operation,
539 /// and should be used when running on the main thread to avoid deadlocks with the event loop.
540 ///
541 /// # Examples
542 ///
543 /// ```
544 /// use tauri_plugin_dialog::DialogExt;
545 /// tauri::Builder::default()
546 /// .setup(|app| {
547 /// app.dialog().file().pick_folder(|folder_path| {
548 /// // do something with the optional folder path here
549 /// // the folder path is `None` if the user closed the dialog
550 /// });
551 /// Ok(())
552 /// });
553 /// ```
554 #[cfg(desktop)]
555 pub fn pick_folder<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
556 pick_folder(self, f)
557 }
558
559 /// Shows the dialog to select multiple folders.
560 /// This is not a blocking operation,
561 /// and should be used when running on the main thread to avoid deadlocks with the event loop.
562 ///
563 /// # Examples
564 ///
565 /// ```
566 /// use tauri_plugin_dialog::DialogExt;
567 /// tauri::Builder::default()
568 /// .setup(|app| {
569 /// app.dialog().file().pick_folders(|file_paths| {
570 /// // do something with the optional folder paths here
571 /// // the folder paths value is `None` if the user closed the dialog
572 /// });
573 /// Ok(())
574 /// });
575 /// ```
576 #[cfg(desktop)]
577 pub fn pick_folders<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
578 pick_folders(self, f)
579 }
580
581 /// Shows the dialog to save a file.
582 ///
583 /// This is not a blocking operation,
584 /// and should be used when running on the main thread to avoid deadlocks with the event loop.
585 ///
586 /// # Examples
587 ///
588 /// ```
589 /// use tauri_plugin_dialog::DialogExt;
590 /// tauri::Builder::default()
591 /// .setup(|app| {
592 /// app.dialog().file().save_file(|file_path| {
593 /// // do something with the optional file path here
594 /// // the file path is `None` if the user closed the dialog
595 /// });
596 /// Ok(())
597 /// });
598 /// ```
599 pub fn save_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
600 save_file(self, f)
601 }
602}
603
604/// Blocking APIs.
605impl<R: Runtime> FileDialogBuilder<R> {
606 /// Shows the dialog to select a single file.
607 /// This is a blocking operation,
608 /// and should *NOT* be used when running on the main thread context.
609 ///
610 /// # Examples
611 ///
612 /// ```
613 /// use tauri_plugin_dialog::DialogExt;
614 /// #[tauri::command]
615 /// async fn my_command(app: tauri::AppHandle) {
616 /// let file_path = app.dialog().file().blocking_pick_file();
617 /// // do something with the optional file path here
618 /// // the file path is `None` if the user closed the dialog
619 /// }
620 /// ```
621 pub fn blocking_pick_file(self) -> Option<FilePath> {
622 blocking_fn!(self, pick_file)
623 }
624
625 /// Shows the dialog to select multiple files.
626 /// This is a blocking operation,
627 /// and should *NOT* be used when running on the main thread context.
628 ///
629 /// # Examples
630 ///
631 /// ```
632 /// use tauri_plugin_dialog::DialogExt;
633 /// #[tauri::command]
634 /// async fn my_command(app: tauri::AppHandle) {
635 /// let file_path = app.dialog().file().blocking_pick_files();
636 /// // do something with the optional file paths here
637 /// // the file paths value is `None` if the user closed the dialog
638 /// }
639 /// ```
640 pub fn blocking_pick_files(self) -> Option<Vec<FilePath>> {
641 blocking_fn!(self, pick_files)
642 }
643
644 /// Shows the dialog to select a single folder.
645 /// This is a blocking operation,
646 /// and should *NOT* be used when running on the main thread context.
647 ///
648 /// # Examples
649 ///
650 /// ```
651 /// use tauri_plugin_dialog::DialogExt;
652 /// #[tauri::command]
653 /// async fn my_command(app: tauri::AppHandle) {
654 /// let folder_path = app.dialog().file().blocking_pick_folder();
655 /// // do something with the optional folder path here
656 /// // the folder path is `None` if the user closed the dialog
657 /// }
658 /// ```
659 #[cfg(desktop)]
660 pub fn blocking_pick_folder(self) -> Option<FilePath> {
661 blocking_fn!(self, pick_folder)
662 }
663
664 /// Shows the dialog to select multiple folders.
665 /// This is a blocking operation,
666 /// and should *NOT* be used when running on the main thread context.
667 ///
668 /// # Examples
669 ///
670 /// ```
671 /// use tauri_plugin_dialog::DialogExt;
672 /// #[tauri::command]
673 /// async fn my_command(app: tauri::AppHandle) {
674 /// let folder_paths = app.dialog().file().blocking_pick_folders();
675 /// // do something with the optional folder paths here
676 /// // the folder paths value is `None` if the user closed the dialog
677 /// }
678 /// ```
679 #[cfg(desktop)]
680 pub fn blocking_pick_folders(self) -> Option<Vec<FilePath>> {
681 blocking_fn!(self, pick_folders)
682 }
683
684 /// Shows the dialog to save a file.
685 /// This is a blocking operation,
686 /// and should *NOT* be used when running on the main thread context.
687 ///
688 /// # Examples
689 ///
690 /// ```
691 /// use tauri_plugin_dialog::DialogExt;
692 /// #[tauri::command]
693 /// async fn my_command(app: tauri::AppHandle) {
694 /// let file_path = app.dialog().file().blocking_save_file();
695 /// // do something with the optional file path here
696 /// // the file path is `None` if the user closed the dialog
697 /// }
698 /// ```
699 pub fn blocking_save_file(self) -> Option<FilePath> {
700 blocking_fn!(self, save_file)
701 }
702}