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