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