Skip to main content

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