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}