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}