// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/dialog/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/dialog) //! //! Native system dialogs for opening and saving files along with message dialogs. #![doc( html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" )] use serde::Serialize; use tauri::{ plugin::{Builder, TauriPlugin}, Manager, Runtime, }; use std::{ path::{Path, PathBuf}, sync::mpsc::sync_channel, }; pub use models::*; pub use tauri_plugin_fs::FilePath; #[cfg(desktop)] mod desktop; #[cfg(mobile)] mod mobile; mod commands; mod error; mod models; pub use error::{Error, Result}; #[cfg(desktop)] use desktop::*; #[cfg(mobile)] use mobile::*; pub(crate) const OK: &str = "Ok"; pub(crate) const CANCEL: &str = "Cancel"; pub(crate) const YES: &str = "Yes"; pub(crate) const NO: &str = "No"; macro_rules! blocking_fn { ($self:ident, $fn:ident) => {{ let (tx, rx) = sync_channel(0); let cb = move |response| { tx.send(response).unwrap(); }; $self.$fn(cb); rx.recv().unwrap() }}; } /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the dialog APIs. pub trait DialogExt { fn dialog(&self) -> &Dialog; } impl> crate::DialogExt for T { fn dialog(&self) -> &Dialog { self.state::>().inner() } } impl Dialog { /// Create a new messaging dialog builder. /// The dialog can optionally ask the user for confirmation or include an OK button. /// /// # Examples /// /// - Message dialog: /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// /// tauri::Builder::default() /// .setup(|app| { /// app /// .dialog() /// .message("Tauri is Awesome!") /// .show(|_| { /// println!("dialog closed"); /// }); /// Ok(()) /// }); /// ``` /// /// - Ask dialog: /// /// ``` /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; /// /// tauri::Builder::default() /// .setup(|app| { /// app.dialog() /// .message("Are you sure?") /// .buttons(MessageDialogButtons::OkCancelCustom("Yes", "No")) /// .show(|yes| { /// println!("user said {}", if yes { "yes" } else { "no" }); /// }); /// Ok(()) /// }); /// ``` /// /// - Message dialog with OK button: /// /// ``` /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; /// /// tauri::Builder::default() /// .setup(|app| { /// app.dialog() /// .message("Job completed successfully") /// .buttons(MessageDialogButtons::Ok) /// .show(|_| { /// println!("dialog closed"); /// }); /// Ok(()) /// }); /// ``` /// /// # `show` vs `blocking_show` /// /// The dialog builder includes two separate APIs for rendering the dialog: `show` and `blocking_show`. /// The `show` function is asynchronous and takes a closure to be executed when the dialog is closed. /// To block the current thread until the user acted on the dialog, you can use `blocking_show`, /// but note that it cannot be executed on the main thread as it will freeze your application. /// /// ``` /// use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; /// /// tauri::Builder::default() /// .setup(|app| { /// let handle = app.handle().clone(); /// std::thread::spawn(move || { /// let yes = handle.dialog() /// .message("Are you sure?") /// .buttons(MessageDialogButtons::OkCancelCustom("Yes", "No")) /// .blocking_show(); /// }); /// /// Ok(()) /// }); /// ``` pub fn message(&self, message: impl Into) -> MessageDialogBuilder { MessageDialogBuilder::new( self.clone(), self.app_handle().package_info().name.clone(), message, ) } /// Creates a new builder for dialogs that lets the user select file(s) or folder(s). pub fn file(&self) -> FileDialogBuilder { FileDialogBuilder::new(self.clone()) } } /// Initializes the plugin. pub fn init() -> TauriPlugin { #[allow(unused_mut)] let mut builder = Builder::new("dialog"); // Dialogs are implemented natively on Android #[cfg(not(target_os = "android"))] { builder = builder.js_init_script(include_str!("init-iife.js").to_string()); } builder .invoke_handler(tauri::generate_handler![ commands::open, commands::save, commands::message, commands::ask, commands::confirm ]) .setup(|app, api| { #[cfg(mobile)] let dialog = mobile::init(app, api)?; #[cfg(desktop)] let dialog = desktop::init(app, api)?; app.manage(dialog); Ok(()) }) .build() } /// A builder for message dialogs. pub struct MessageDialogBuilder { #[allow(dead_code)] pub(crate) dialog: Dialog, pub(crate) title: String, pub(crate) message: String, pub(crate) kind: MessageDialogKind, pub(crate) buttons: MessageDialogButtons, #[cfg(desktop)] pub(crate) parent: Option, } /// Payload for the message dialog mobile API. #[cfg(mobile)] #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct MessageDialogPayload<'a> { title: &'a String, message: &'a String, kind: &'a MessageDialogKind, ok_button_label: Option<&'a str>, cancel_button_label: Option<&'a str>, } // raw window handle :( unsafe impl Send for MessageDialogBuilder {} impl MessageDialogBuilder { /// Creates a new message dialog builder. pub fn new(dialog: Dialog, title: impl Into, message: impl Into) -> Self { Self { dialog, title: title.into(), message: message.into(), kind: Default::default(), buttons: Default::default(), #[cfg(desktop)] parent: None, } } #[cfg(mobile)] pub(crate) fn payload(&self) -> MessageDialogPayload<'_> { let (ok_button_label, cancel_button_label) = match &self.buttons { MessageDialogButtons::Ok => (Some(OK), None), MessageDialogButtons::OkCancel => (Some(OK), Some(CANCEL)), MessageDialogButtons::YesNo => (Some(YES), Some(NO)), MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), Some(CANCEL)), MessageDialogButtons::OkCancelCustom(ok, cancel) => { (Some(ok.as_str()), Some(cancel.as_str())) } }; MessageDialogPayload { title: &self.title, message: &self.message, kind: &self.kind, ok_button_label, cancel_button_label, } } /// Sets the dialog title. pub fn title(mut self, title: impl Into) -> Self { self.title = title.into(); self } /// Set parent windows explicitly (optional) #[cfg(desktop)] pub fn parent( mut self, parent: &W, ) -> Self { if let (Ok(window_handle), Ok(display_handle)) = (parent.window_handle(), parent.display_handle()) { self.parent.replace(crate::desktop::WindowHandle::new( window_handle.as_raw(), display_handle.as_raw(), )); } self } /// Sets the dialog buttons. pub fn buttons(mut self, buttons: MessageDialogButtons) -> Self { self.buttons = buttons; self } /// Set type of a dialog. /// /// Depending on the system it can result in type specific icon to show up, /// the will inform user it message is a error, warning or just information. pub fn kind(mut self, kind: MessageDialogKind) -> Self { self.kind = kind; self } /// Shows a message dialog pub fn show(self, f: F) { show_message_dialog(self, f) } /// Shows a message dialog. /// This is a blocking operation, /// and should *NOT* be used when running on the main thread context. pub fn blocking_show(self) -> bool { blocking_fn!(self, show) } } #[derive(Debug, Serialize)] pub(crate) struct Filter { pub name: String, pub extensions: Vec, } /// The file dialog builder. /// /// Constructs file picker dialogs that can select single/multiple files or directories. #[derive(Debug)] pub struct FileDialogBuilder { #[allow(dead_code)] pub(crate) dialog: Dialog, pub(crate) filters: Vec, pub(crate) starting_directory: Option, pub(crate) file_name: Option, pub(crate) title: Option, pub(crate) can_create_directories: Option, #[cfg(desktop)] pub(crate) parent: Option, } #[cfg(mobile)] #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct FileDialogPayload<'a> { file_name: &'a Option, filters: &'a Vec, multiple: bool, } // raw window handle :( unsafe impl Send for FileDialogBuilder {} impl FileDialogBuilder { /// Gets the default file dialog builder. pub fn new(dialog: Dialog) -> Self { Self { dialog, filters: Vec::new(), starting_directory: None, file_name: None, title: None, can_create_directories: None, #[cfg(desktop)] parent: None, } } #[cfg(mobile)] pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> { FileDialogPayload { file_name: &self.file_name, filters: &self.filters, multiple, } } /// Add file extension filter. Takes in the name of the filter, and list of extensions #[must_use] pub fn add_filter(mut self, name: impl Into, extensions: &[&str]) -> Self { self.filters.push(Filter { name: name.into(), extensions: extensions.iter().map(|e| e.to_string()).collect(), }); self } /// Set starting directory of the dialog. #[must_use] pub fn set_directory>(mut self, directory: P) -> Self { self.starting_directory.replace(directory.as_ref().into()); self } /// Set starting file name of the dialog. #[must_use] pub fn set_file_name(mut self, file_name: impl Into) -> Self { self.file_name.replace(file_name.into()); self } /// Sets the parent window of the dialog. #[cfg(desktop)] #[must_use] pub fn set_parent< W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle, >( mut self, parent: &W, ) -> Self { if let (Ok(window_handle), Ok(display_handle)) = (parent.window_handle(), parent.display_handle()) { self.parent.replace(crate::desktop::WindowHandle::new( window_handle.as_raw(), display_handle.as_raw(), )); } self } /// Set the title of the dialog. #[must_use] pub fn set_title(mut self, title: impl Into) -> Self { self.title.replace(title.into()); self } /// Set whether it should be possible to create new directories in the dialog. Enabled by default. **macOS only**. pub fn set_can_create_directories(mut self, can: bool) -> Self { self.can_create_directories.replace(can); self } /// Shows the dialog to select a single file. /// This is not a blocking operation, /// and should be used when running on the main thread to avoid deadlocks with the event loop. /// /// For usage in other contexts such as commands, prefer [`Self::pick_file`]. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { /// app.dialog().file().pick_file(|file_path| { /// // do something with the optional file path here /// // the file path is `None` if the user closed the dialog /// }); /// Ok(()) /// }); /// ``` pub fn pick_file) + Send + 'static>(self, f: F) { pick_file(self, f) } /// Shows the dialog to select multiple files. /// This is not a blocking operation, /// and should be used when running on the main thread to avoid deadlocks with the event loop. /// /// # Reading the files /// /// The file paths cannot be read directly on Android as they are behind a content URI. /// The recommended way to read the files is using the [`fs`](https://v2.tauri.app/plugin/file-system/) plugin: /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// use tauri_plugin_fs::FsExt; /// tauri::Builder::default() /// .setup(|app| { /// let handle = app.handle().clone(); /// app.dialog().file().pick_file(move |file_path| { /// let Some(path) = file_path else { return }; /// let Ok(contents) = handle.fs().read_to_string(path) else { /// eprintln!("failed to read file, "); /// return; /// }; /// }); /// Ok(()) /// }); /// ``` /// /// See for more information. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { /// app.dialog().file().pick_files(|file_paths| { /// // do something with the optional file paths here /// // the file paths value is `None` if the user closed the dialog /// }); /// Ok(()) /// }); /// ``` pub fn pick_files>) + Send + 'static>(self, f: F) { pick_files(self, f) } /// Shows the dialog to select a single folder. /// This is not a blocking operation, /// and should be used when running on the main thread to avoid deadlocks with the event loop. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { /// app.dialog().file().pick_folder(|folder_path| { /// // do something with the optional folder path here /// // the folder path is `None` if the user closed the dialog /// }); /// Ok(()) /// }); /// ``` #[cfg(desktop)] pub fn pick_folder) + Send + 'static>(self, f: F) { pick_folder(self, f) } /// Shows the dialog to select multiple folders. /// This is not a blocking operation, /// and should be used when running on the main thread to avoid deadlocks with the event loop. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { /// app.dialog().file().pick_folders(|file_paths| { /// // do something with the optional folder paths here /// // the folder paths value is `None` if the user closed the dialog /// }); /// Ok(()) /// }); /// ``` #[cfg(desktop)] pub fn pick_folders>) + Send + 'static>(self, f: F) { pick_folders(self, f) } /// Shows the dialog to save a file. /// /// This is not a blocking operation, /// and should be used when running on the main thread to avoid deadlocks with the event loop. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// tauri::Builder::default() /// .setup(|app| { /// app.dialog().file().save_file(|file_path| { /// // do something with the optional file path here /// // the file path is `None` if the user closed the dialog /// }); /// Ok(()) /// }); /// ``` pub fn save_file) + Send + 'static>(self, f: F) { save_file(self, f) } } /// Blocking APIs. impl FileDialogBuilder { /// Shows the dialog to select a single file. /// This is a blocking operation, /// and should *NOT* be used when running on the main thread context. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { /// let file_path = app.dialog().file().blocking_pick_file(); /// // do something with the optional file path here /// // the file path is `None` if the user closed the dialog /// } /// ``` pub fn blocking_pick_file(self) -> Option { blocking_fn!(self, pick_file) } /// Shows the dialog to select multiple files. /// This is a blocking operation, /// and should *NOT* be used when running on the main thread context. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { /// let file_path = app.dialog().file().blocking_pick_files(); /// // do something with the optional file paths here /// // the file paths value is `None` if the user closed the dialog /// } /// ``` pub fn blocking_pick_files(self) -> Option> { blocking_fn!(self, pick_files) } /// Shows the dialog to select a single folder. /// This is a blocking operation, /// and should *NOT* be used when running on the main thread context. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { /// let folder_path = app.dialog().file().blocking_pick_folder(); /// // do something with the optional folder path here /// // the folder path is `None` if the user closed the dialog /// } /// ``` #[cfg(desktop)] pub fn blocking_pick_folder(self) -> Option { blocking_fn!(self, pick_folder) } /// Shows the dialog to select multiple folders. /// This is a blocking operation, /// and should *NOT* be used when running on the main thread context. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { /// let folder_paths = app.dialog().file().blocking_pick_folders(); /// // do something with the optional folder paths here /// // the folder paths value is `None` if the user closed the dialog /// } /// ``` #[cfg(desktop)] pub fn blocking_pick_folders(self) -> Option> { blocking_fn!(self, pick_folders) } /// Shows the dialog to save a file. /// This is a blocking operation, /// and should *NOT* be used when running on the main thread context. /// /// # Examples /// /// ``` /// use tauri_plugin_dialog::DialogExt; /// #[tauri::command] /// async fn my_command(app: tauri::AppHandle) { /// let file_path = app.dialog().file().blocking_save_file(); /// // do something with the optional file path here /// // the file path is `None` if the user closed the dialog /// } /// ``` pub fn blocking_save_file(self) -> Option { blocking_fn!(self, save_file) } }