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}