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