tauri_plugin_dialog/
desktop.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//! Use native message and file open/save dialogs.
6//!
7//! This module exposes non-blocking APIs on its root, relying on callback closures
8//! to give results back. This is particularly useful when running dialogs from the main thread.
9//! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended.
10
11use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle};
12use rfd::{AsyncFileDialog, AsyncMessageDialog};
13use serde::de::DeserializeOwned;
14use tauri::{plugin::PluginApi, AppHandle, Runtime};
15
16use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder};
17
18pub fn init<R: Runtime, C: DeserializeOwned>(
19    app: &AppHandle<R>,
20    _api: PluginApi<R, C>,
21) -> crate::Result<Dialog<R>> {
22    Ok(Dialog(app.clone()))
23}
24
25/// Access to the dialog APIs.
26#[derive(Debug)]
27pub struct Dialog<R: Runtime>(AppHandle<R>);
28
29impl<R: Runtime> Clone for Dialog<R> {
30    fn clone(&self) -> Self {
31        Self(self.0.clone())
32    }
33}
34
35impl<R: Runtime> Dialog<R> {
36    pub(crate) fn app_handle(&self) -> &AppHandle<R> {
37        &self.0
38    }
39}
40
41impl From<MessageDialogKind> for rfd::MessageLevel {
42    fn from(kind: MessageDialogKind) -> Self {
43        match kind {
44            MessageDialogKind::Info => Self::Info,
45            MessageDialogKind::Warning => Self::Warning,
46            MessageDialogKind::Error => Self::Error,
47        }
48    }
49}
50
51#[derive(Debug)]
52pub(crate) struct WindowHandle {
53    window_handle: RawWindowHandle,
54    display_handle: RawDisplayHandle,
55}
56
57impl WindowHandle {
58    pub(crate) fn new(window_handle: RawWindowHandle, display_handle: RawDisplayHandle) -> Self {
59        Self {
60            window_handle,
61            display_handle,
62        }
63    }
64}
65
66impl HasWindowHandle for WindowHandle {
67    fn window_handle(
68        &self,
69    ) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
70        Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(self.window_handle) })
71    }
72}
73
74impl HasDisplayHandle for WindowHandle {
75    fn display_handle(
76        &self,
77    ) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
78        Ok(unsafe { raw_window_handle::DisplayHandle::borrow_raw(self.display_handle) })
79    }
80}
81
82impl<R: Runtime> From<FileDialogBuilder<R>> for AsyncFileDialog {
83    fn from(d: FileDialogBuilder<R>) -> Self {
84        let mut builder = AsyncFileDialog::new();
85
86        if let Some(title) = d.title {
87            builder = builder.set_title(title);
88        }
89        if let Some(starting_directory) = d.starting_directory {
90            builder = builder.set_directory(starting_directory);
91        }
92        if let Some(file_name) = d.file_name {
93            builder = builder.set_file_name(file_name);
94        }
95        for filter in d.filters {
96            let v: Vec<&str> = filter.extensions.iter().map(|x| &**x).collect();
97            builder = builder.add_filter(&filter.name, &v);
98        }
99        #[cfg(desktop)]
100        if let Some(parent) = d.parent {
101            builder = builder.set_parent(&parent);
102        }
103
104        builder = builder.set_can_create_directories(d.can_create_directories.unwrap_or(true));
105
106        builder
107    }
108}
109
110impl From<MessageDialogButtons> for rfd::MessageButtons {
111    fn from(value: MessageDialogButtons) -> Self {
112        match value {
113            MessageDialogButtons::Ok => Self::Ok,
114            MessageDialogButtons::OkCancel => Self::OkCancel,
115            MessageDialogButtons::YesNo => Self::YesNo,
116            MessageDialogButtons::OkCustom(ok) => Self::OkCustom(ok),
117            MessageDialogButtons::OkCancelCustom(ok, cancel) => Self::OkCancelCustom(ok, cancel),
118            MessageDialogButtons::YesNoCancel => Self::YesNoCancel,
119            MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => {
120                Self::YesNoCancelCustom(yes, no, cancel)
121            }
122        }
123    }
124}
125
126impl<R: Runtime> From<MessageDialogBuilder<R>> for AsyncMessageDialog {
127    fn from(d: MessageDialogBuilder<R>) -> Self {
128        let mut dialog = AsyncMessageDialog::new()
129            .set_title(&d.title)
130            .set_description(&d.message)
131            .set_level(d.kind.into())
132            .set_buttons(d.buttons.into());
133
134        if let Some(parent) = d.parent {
135            dialog = dialog.set_parent(&parent);
136        }
137
138        dialog
139    }
140}
141
142pub fn pick_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
143    dialog: FileDialogBuilder<R>,
144    f: F,
145) {
146    let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
147    let handle = dialog.dialog.app_handle().to_owned();
148    let _ = handle.run_on_main_thread(move || {
149        let dialog = AsyncFileDialog::from(dialog).pick_file();
150        std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog)));
151    });
152}
153
154pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
155    dialog: FileDialogBuilder<R>,
156    f: F,
157) {
158    let f = |paths: Option<Vec<rfd::FileHandle>>| {
159        f(paths.map(|list| {
160            list.into_iter()
161                .map(|p| p.path().to_path_buf().into())
162                .collect()
163        }))
164    };
165    let handle = dialog.dialog.app_handle().to_owned();
166    let _ = handle.run_on_main_thread(move || {
167        let dialog = AsyncFileDialog::from(dialog).pick_files();
168        std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog)));
169    });
170}
171
172pub fn pick_folder<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
173    dialog: FileDialogBuilder<R>,
174    f: F,
175) {
176    let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
177    let handle = dialog.dialog.app_handle().to_owned();
178    let _ = handle.run_on_main_thread(move || {
179        let dialog = AsyncFileDialog::from(dialog).pick_folder();
180        std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog)));
181    });
182}
183
184pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
185    dialog: FileDialogBuilder<R>,
186    f: F,
187) {
188    let f = |paths: Option<Vec<rfd::FileHandle>>| {
189        f(paths.map(|list| {
190            list.into_iter()
191                .map(|p| p.path().to_path_buf().into())
192                .collect()
193        }))
194    };
195    let handle = dialog.dialog.app_handle().to_owned();
196    let _ = handle.run_on_main_thread(move || {
197        let dialog = AsyncFileDialog::from(dialog).pick_folders();
198        std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog)));
199    });
200}
201
202pub fn save_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
203    dialog: FileDialogBuilder<R>,
204    f: F,
205) {
206    let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
207    let handle = dialog.dialog.app_handle().to_owned();
208    let _ = handle.run_on_main_thread(move || {
209        let dialog = AsyncFileDialog::from(dialog).save_file();
210        std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog)));
211    });
212}
213
214/// Shows a message dialog
215pub fn show_message_dialog<R: Runtime, F: FnOnce(MessageDialogResult) + Send + 'static>(
216    dialog: MessageDialogBuilder<R>,
217    callback: F,
218) {
219    let f = move |res: rfd::MessageDialogResult| callback(res.into());
220
221    let handle = dialog.dialog.app_handle().to_owned();
222    let _ = handle.run_on_main_thread(move || {
223        let buttons = dialog.buttons.clone();
224        let dialog = AsyncMessageDialog::from(dialog).show();
225        std::thread::spawn(move || {
226            let result = tauri::async_runtime::block_on(dialog);
227            // on Linux rfd does not return rfd::MessageDialogResult::Custom, so we must map manually
228            let result = match (result, buttons) {
229                (rfd::MessageDialogResult::Ok, MessageDialogButtons::OkCustom(s)) => {
230                    rfd::MessageDialogResult::Custom(s)
231                }
232                (
233                    rfd::MessageDialogResult::Ok,
234                    MessageDialogButtons::OkCancelCustom(ok, _cancel),
235                ) => rfd::MessageDialogResult::Custom(ok),
236                (
237                    rfd::MessageDialogResult::Cancel,
238                    MessageDialogButtons::OkCancelCustom(_ok, cancel),
239                ) => rfd::MessageDialogResult::Custom(cancel),
240                (
241                    rfd::MessageDialogResult::Yes,
242                    MessageDialogButtons::YesNoCancelCustom(yes, _no, _cancel),
243                ) => rfd::MessageDialogResult::Custom(yes),
244                (
245                    rfd::MessageDialogResult::No,
246                    MessageDialogButtons::YesNoCancelCustom(_yes, no, _cancel),
247                ) => rfd::MessageDialogResult::Custom(no),
248                (
249                    rfd::MessageDialogResult::Cancel,
250                    MessageDialogButtons::YesNoCancelCustom(_yes, _no, cancel),
251                ) => rfd::MessageDialogResult::Custom(cancel),
252                (result, _) => result,
253            };
254            f(result);
255        });
256    });
257}