// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! Use native message and file open/save dialogs. //! //! This module exposes non-blocking APIs on its root, relying on callback closures //! to give results back. This is particularly useful when running dialogs from the main thread. //! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended. use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; use rfd::{AsyncFileDialog, AsyncMessageDialog}; use serde::de::DeserializeOwned; use tauri::{plugin::PluginApi, AppHandle, Runtime}; use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder, OK}; pub fn init( app: &AppHandle, _api: PluginApi, ) -> crate::Result> { Ok(Dialog(app.clone())) } /// Access to the dialog APIs. #[derive(Debug)] pub struct Dialog(AppHandle); impl Clone for Dialog { fn clone(&self) -> Self { Self(self.0.clone()) } } impl Dialog { pub(crate) fn app_handle(&self) -> &AppHandle { &self.0 } } impl From for rfd::MessageLevel { fn from(kind: MessageDialogKind) -> Self { match kind { MessageDialogKind::Info => Self::Info, MessageDialogKind::Warning => Self::Warning, MessageDialogKind::Error => Self::Error, } } } #[derive(Debug)] pub(crate) struct WindowHandle { window_handle: RawWindowHandle, display_handle: RawDisplayHandle, } impl WindowHandle { pub(crate) fn new(window_handle: RawWindowHandle, display_handle: RawDisplayHandle) -> Self { Self { window_handle, display_handle, } } } impl HasWindowHandle for WindowHandle { fn window_handle( &self, ) -> Result, raw_window_handle::HandleError> { Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(self.window_handle) }) } } impl HasDisplayHandle for WindowHandle { fn display_handle( &self, ) -> Result, raw_window_handle::HandleError> { Ok(unsafe { raw_window_handle::DisplayHandle::borrow_raw(self.display_handle) }) } } impl From> for AsyncFileDialog { fn from(d: FileDialogBuilder) -> Self { let mut builder = AsyncFileDialog::new(); if let Some(title) = d.title { builder = builder.set_title(title); } if let Some(starting_directory) = d.starting_directory { builder = builder.set_directory(starting_directory); } if let Some(file_name) = d.file_name { builder = builder.set_file_name(file_name); } for filter in d.filters { let v: Vec<&str> = filter.extensions.iter().map(|x| &**x).collect(); builder = builder.add_filter(&filter.name, &v); } #[cfg(desktop)] if let Some(parent) = d.parent { builder = builder.set_parent(&parent); } builder = builder.set_can_create_directories(d.can_create_directories.unwrap_or(true)); builder } } impl From for rfd::MessageButtons { fn from(value: MessageDialogButtons) -> Self { match value { MessageDialogButtons::Ok => Self::Ok, MessageDialogButtons::OkCancel => Self::OkCancel, MessageDialogButtons::YesNo => Self::YesNo, MessageDialogButtons::OkCustom(ok) => Self::OkCustom(ok), MessageDialogButtons::OkCancelCustom(ok, cancel) => Self::OkCancelCustom(ok, cancel), } } } impl From> for AsyncMessageDialog { fn from(d: MessageDialogBuilder) -> Self { let mut dialog = AsyncMessageDialog::new() .set_title(&d.title) .set_description(&d.message) .set_level(d.kind.into()) .set_buttons(d.buttons.into()); if let Some(parent) = d.parent { dialog = dialog.set_parent(&parent); } dialog } } pub fn pick_file) + Send + 'static>( dialog: FileDialogBuilder, f: F, ) { let f = |path: Option| f(path.map(|p| p.path().to_path_buf().into())); let handle = dialog.dialog.app_handle().to_owned(); let _ = handle.run_on_main_thread(move || { let dialog = AsyncFileDialog::from(dialog).pick_file(); std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); }); } pub fn pick_files>) + Send + 'static>( dialog: FileDialogBuilder, f: F, ) { let f = |paths: Option>| { f(paths.map(|list| { list.into_iter() .map(|p| p.path().to_path_buf().into()) .collect() })) }; let handle = dialog.dialog.app_handle().to_owned(); let _ = handle.run_on_main_thread(move || { let dialog = AsyncFileDialog::from(dialog).pick_files(); std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); }); } pub fn pick_folder) + Send + 'static>( dialog: FileDialogBuilder, f: F, ) { let f = |path: Option| f(path.map(|p| p.path().to_path_buf().into())); let handle = dialog.dialog.app_handle().to_owned(); let _ = handle.run_on_main_thread(move || { let dialog = AsyncFileDialog::from(dialog).pick_folder(); std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); }); } pub fn pick_folders>) + Send + 'static>( dialog: FileDialogBuilder, f: F, ) { let f = |paths: Option>| { f(paths.map(|list| { list.into_iter() .map(|p| p.path().to_path_buf().into()) .collect() })) }; let handle = dialog.dialog.app_handle().to_owned(); let _ = handle.run_on_main_thread(move || { let dialog = AsyncFileDialog::from(dialog).pick_folders(); std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); }); } pub fn save_file) + Send + 'static>( dialog: FileDialogBuilder, f: F, ) { let f = |path: Option| f(path.map(|p| p.path().to_path_buf().into())); let handle = dialog.dialog.app_handle().to_owned(); let _ = handle.run_on_main_thread(move || { let dialog = AsyncFileDialog::from(dialog).save_file(); std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); }); } /// Shows a message dialog pub fn show_message_dialog( dialog: MessageDialogBuilder, f: F, ) { use rfd::MessageDialogResult; let ok_label = match &dialog.buttons { MessageDialogButtons::OkCustom(ok) => Some(ok.clone()), MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()), _ => None, }; let f = move |res| { f(match res { MessageDialogResult::Ok | MessageDialogResult::Yes => true, MessageDialogResult::Custom(s) => ok_label.map_or(s == OK, |ok_label| ok_label == s), _ => false, }); }; let handle = dialog.dialog.app_handle().to_owned(); let _ = handle.run_on_main_thread(move || { let dialog = AsyncMessageDialog::from(dialog).show(); std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog))); }); }