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