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