Skip to content

Commit 2088cd0

Browse files
authored
refactor(core): handle dialog threading internally, closes #2223 (#2429)
* refactor(core): handle dialog threading internally, closes #2223 * thread spawn
1 parent dd5e1ed commit 2088cd0

File tree

11 files changed

+132
-128
lines changed

11 files changed

+132
-128
lines changed

.changes/dialog-thread-handling.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Allow the `tauri::api::dialog` APIs to be executed on any thread.
6+
**Breaking change:** All dialog APIs now takes a closure instead of returning the response on the function call.

core/tauri/src/api/dialog.rs

+61-36
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@
55
#[cfg(any(dialog_open, dialog_save))]
66
use std::path::{Path, PathBuf};
77

8+
#[cfg(not(target_os = "linux"))]
9+
macro_rules! run_dialog {
10+
($e:expr, $h: ident) => {{
11+
std::thread::spawn(move || {
12+
let response = $e;
13+
$h(response);
14+
});
15+
}};
16+
}
17+
18+
#[cfg(target_os = "linux")]
19+
macro_rules! run_dialog {
20+
($e:expr, $h: ident) => {{
21+
std::thread::spawn(move || {
22+
let context = glib::MainContext::default();
23+
context.invoke_with_priority(glib::PRIORITY_HIGH, move || {
24+
let response = $e;
25+
$h(response);
26+
});
27+
});
28+
}};
29+
}
30+
831
/// The file dialog builder.
932
/// Constructs file picker dialogs that can select single/multiple files or directories.
1033
#[cfg(any(dialog_open, dialog_save))]
@@ -44,55 +67,57 @@ impl FileDialogBuilder {
4467
}
4568

4669
/// Pick one file.
47-
pub fn pick_file(self) -> Option<PathBuf> {
48-
self.0.pick_file()
70+
pub fn pick_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
71+
run_dialog!(self.0.pick_file(), f)
4972
}
5073

5174
/// Pick multiple files.
52-
pub fn pick_files(self) -> Option<Vec<PathBuf>> {
53-
self.0.pick_files()
75+
pub fn pick_files<F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(self, f: F) {
76+
run_dialog!(self.0.pick_files(), f)
5477
}
5578

5679
/// Pick one folder.
57-
pub fn pick_folder(self) -> Option<PathBuf> {
58-
self.0.pick_folder()
80+
pub fn pick_folder<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
81+
run_dialog!(self.0.pick_folder(), f)
5982
}
6083

6184
/// Opens save file dialog.
62-
pub fn save_file(self) -> Option<PathBuf> {
63-
self.0.save_file()
85+
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
86+
run_dialog!(self.0.save_file(), f)
6487
}
6588
}
6689

67-
/// Response for the ask dialog
68-
#[derive(Debug, Clone, PartialEq, Eq)]
69-
pub enum AskResponse {
70-
/// User confirmed.
71-
Yes,
72-
/// User denied.
73-
No,
74-
}
75-
76-
/// Displays a dialog with a message and an optional title with a "yes" and a "no" button
77-
pub fn ask(title: impl AsRef<str>, message: impl AsRef<str>) -> AskResponse {
78-
match rfd::MessageDialog::new()
79-
.set_title(title.as_ref())
80-
.set_description(message.as_ref())
81-
.set_buttons(rfd::MessageButtons::YesNo)
82-
.set_level(rfd::MessageLevel::Info)
83-
.show()
84-
{
85-
true => AskResponse::Yes,
86-
false => AskResponse::No,
87-
}
90+
/// Displays a dialog with a message and an optional title with a "yes" and a "no" button.
91+
pub fn ask<F: FnOnce(bool) + Send + 'static>(
92+
title: impl AsRef<str>,
93+
message: impl AsRef<str>,
94+
f: F,
95+
) {
96+
let title = title.as_ref().to_string();
97+
let message = message.as_ref().to_string();
98+
run_dialog!(
99+
rfd::MessageDialog::new()
100+
.set_title(&title)
101+
.set_description(&message)
102+
.set_buttons(rfd::MessageButtons::YesNo)
103+
.set_level(rfd::MessageLevel::Info)
104+
.show(),
105+
f
106+
)
88107
}
89108

90-
/// Displays a message dialog
109+
/// Displays a message dialog.
91110
pub fn message(title: impl AsRef<str>, message: impl AsRef<str>) {
92-
rfd::MessageDialog::new()
93-
.set_title(title.as_ref())
94-
.set_description(message.as_ref())
95-
.set_buttons(rfd::MessageButtons::Ok)
96-
.set_level(rfd::MessageLevel::Info)
97-
.show();
111+
let title = title.as_ref().to_string();
112+
let message = message.as_ref().to_string();
113+
let cb = |_| {};
114+
run_dialog!(
115+
rfd::MessageDialog::new()
116+
.set_title(&title)
117+
.set_description(&message)
118+
.set_buttons(rfd::MessageButtons::Ok)
119+
.set_level(rfd::MessageLevel::Info)
120+
.show(),
121+
cb
122+
)
98123
}

core/tauri/src/app.rs

-9
Original file line numberDiff line numberDiff line change
@@ -483,18 +483,9 @@ impl<R: Runtime> App<R> {
483483
let updater_config = self.manager.config().tauri.updater.clone();
484484
let package_info = self.manager.package_info().clone();
485485

486-
#[cfg(not(target_os = "linux"))]
487486
crate::async_runtime::spawn(async move {
488487
updater::check_update_with_dialog(updater_config, package_info, window).await
489488
});
490-
491-
#[cfg(target_os = "linux")]
492-
{
493-
let context = glib::MainContext::default();
494-
context.spawn_with_priority(glib::PRIORITY_HIGH, async move {
495-
updater::check_update_with_dialog(updater_config, package_info, window).await
496-
});
497-
}
498489
}
499490

500491
/// Listen updater events when dialog are disabled.

core/tauri/src/endpoints.rs

-13
Original file line numberDiff line numberDiff line change
@@ -111,25 +111,12 @@ impl Module {
111111
.and_then(|r| r.json)
112112
.map_err(InvokeError::from)
113113
}),
114-
// on macOS, the dialog must run on another thread: https://github.com/rust-windowing/winit/issues/1779
115-
// we do the same on Windows just to stay consistent with `tao` (and it also improves UX because of the event loop)
116-
#[cfg(not(target_os = "linux"))]
117114
Self::Dialog(cmd) => resolver.respond_async(async move {
118115
cmd
119116
.run(window)
120117
.and_then(|r| r.json)
121118
.map_err(InvokeError::from)
122119
}),
123-
// on Linux, the dialog must run on the rpc task.
124-
#[cfg(target_os = "linux")]
125-
Self::Dialog(cmd) => {
126-
resolver.respond_closure(move || {
127-
cmd
128-
.run(window)
129-
.and_then(|r| r.json)
130-
.map_err(InvokeError::from)
131-
});
132-
}
133120
Self::Cli(cmd) => {
134121
if let Some(cli_config) = config.tauri.cli.clone() {
135122
resolver.respond_async(async move {

core/tauri/src/endpoints/dialog.rs

+18-13
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ use super::InvokeResponse;
66
#[cfg(any(dialog_open, dialog_save))]
77
use crate::api::dialog::FileDialogBuilder;
88
use crate::{
9-
api::dialog::{ask as ask_dialog, message as message_dialog, AskResponse},
9+
api::dialog::{ask as ask_dialog, message as message_dialog},
1010
runtime::Runtime,
1111
Window,
1212
};
1313
use serde::Deserialize;
1414

15-
use std::path::PathBuf;
15+
use std::{path::PathBuf, sync::mpsc::channel};
1616

1717
#[allow(dead_code)]
1818
#[derive(Deserialize)]
@@ -175,14 +175,18 @@ pub fn open<R: Runtime>(
175175
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
176176
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
177177
}
178-
let response = if options.directory {
179-
dialog_builder.pick_folder().into()
178+
179+
let (tx, rx) = channel();
180+
181+
if options.directory {
182+
dialog_builder.pick_folder(move |p| tx.send(p.into()).unwrap());
180183
} else if options.multiple {
181-
dialog_builder.pick_files().into()
184+
dialog_builder.pick_files(move |p| tx.send(p.into()).unwrap());
182185
} else {
183-
dialog_builder.pick_file().into()
184-
};
185-
Ok(response)
186+
dialog_builder.pick_file(move |p| tx.send(p.into()).unwrap());
187+
}
188+
189+
Ok(rx.recv().unwrap())
186190
}
187191

188192
/// Shows a save dialog.
@@ -204,13 +208,14 @@ pub fn save<R: Runtime>(
204208
let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
205209
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
206210
}
207-
Ok(dialog_builder.save_file().into())
211+
let (tx, rx) = channel();
212+
dialog_builder.save_file(move |p| tx.send(p).unwrap());
213+
Ok(rx.recv().unwrap().into())
208214
}
209215

210216
/// Shows a dialog with a yes/no question.
211217
pub fn ask(title: String, message: String) -> crate::Result<InvokeResponse> {
212-
match ask_dialog(title, message) {
213-
AskResponse::Yes => Ok(true.into()),
214-
_ => Ok(false.into()),
215-
}
218+
let (tx, rx) = channel();
219+
ask_dialog(title, message, move |m| tx.send(m).unwrap());
220+
Ok(rx.recv().unwrap().into())
216221
}

core/tauri/src/endpoints/notification.rs

+15-12
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,23 @@ pub fn request_permission(config: &Config, package_info: &PackageInfo) -> crate:
105105
PERMISSION_DENIED.to_string()
106106
});
107107
}
108-
let answer = crate::api::dialog::ask(
108+
let (tx, rx) = std::sync::mpsc::channel();
109+
crate::api::dialog::ask(
109110
"Permissions",
110111
"This app wants to show notifications. Do you allow?",
112+
move |answer| {
113+
tx.send(answer).unwrap();
114+
},
111115
);
112-
match answer {
113-
crate::api::dialog::AskResponse::Yes => {
114-
settings.allow_notification = Some(true);
115-
crate::settings::write_settings(config, package_info, settings)?;
116-
Ok(PERMISSION_GRANTED.to_string())
117-
}
118-
crate::api::dialog::AskResponse::No => {
119-
settings.allow_notification = Some(false);
120-
crate::settings::write_settings(config, package_info, settings)?;
121-
Ok(PERMISSION_DENIED.to_string())
122-
}
116+
117+
let answer = rx.recv().unwrap();
118+
119+
settings.allow_notification = Some(answer);
120+
crate::settings::write_settings(config, package_info, settings)?;
121+
122+
if answer {
123+
Ok(PERMISSION_GRANTED.to_string())
124+
} else {
125+
Ok(PERMISSION_DENIED.to_string())
123126
}
124127
}

core/tauri/src/updater/mod.rs

+22-32
Original file line numberDiff line numberDiff line change
@@ -333,15 +333,13 @@ mod error;
333333
pub use self::error::Error;
334334

335335
use crate::{
336-
api::{
337-
config::UpdaterConfig,
338-
dialog::{ask, AskResponse},
339-
process::restart,
340-
},
336+
api::{config::UpdaterConfig, dialog::ask, process::restart},
341337
runtime::Runtime,
342338
Window,
343339
};
344340

341+
use std::sync::mpsc::channel;
342+
345343
/// Check for new updates
346344
pub const EVENT_CHECK_UPDATE: &str = "tauri://update";
347345
/// New update available
@@ -526,9 +524,11 @@ async fn prompt_for_install(
526524
// remove single & double quote
527525
let escaped_body = body.replace(&['\"', '\''][..], "");
528526

527+
let (tx, rx) = channel();
528+
529529
// todo(lemarier): We should review this and make sure we have
530530
// something more conventional.
531-
let should_install = ask(
531+
ask(
532532
format!(r#"A new version of {} is available! "#, app_name),
533533
format!(
534534
r#"{} {} is now available -- you have {}.
@@ -539,36 +539,26 @@ Release Notes:
539539
{}"#,
540540
app_name, updater.version, updater.current_version, escaped_body,
541541
),
542+
move |should_install| tx.send(should_install).unwrap(),
542543
);
543544

544-
match should_install {
545-
AskResponse::Yes => {
546-
// Launch updater download process
547-
// macOS we display the `Ready to restart dialog` asking to restart
548-
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
549-
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
550-
updater.download_and_install(pubkey.clone()).await?;
551-
552-
// Ask user if we need to restart the application
553-
let should_exit = ask(
554-
"Ready to Restart",
555-
"The installation was successful, do you want to restart the application now?",
556-
);
557-
match should_exit {
558-
AskResponse::Yes => {
545+
if rx.recv().unwrap() {
546+
// Launch updater download process
547+
// macOS we display the `Ready to restart dialog` asking to restart
548+
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
549+
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
550+
updater.download_and_install(pubkey.clone()).await?;
551+
552+
// Ask user if we need to restart the application
553+
ask(
554+
"Ready to Restart",
555+
"The installation was successful, do you want to restart the application now?",
556+
|should_exit| {
557+
if should_exit {
559558
restart();
560-
// safely exit even if the process
561-
// should be killed
562-
return Ok(());
563-
}
564-
AskResponse::No => {
565-
// Do nothing -- maybe we can emit some event here
566559
}
567-
}
568-
}
569-
AskResponse::No => {
570-
// Do nothing -- maybe we can emit some event here
571-
}
560+
},
561+
);
572562
}
573563

574564
Ok(())

examples/api/public/build/bundle.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/api/public/build/bundle.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/api/src-tauri/src/main.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use std::path::PathBuf;
1515

1616
use serde::Serialize;
1717
use tauri::{
18-
CustomMenuItem, Event, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder,
19-
WindowUrl,
18+
api::dialog::ask, CustomMenuItem, Event, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu,
19+
WindowBuilder, WindowUrl,
2020
};
2121

2222
#[derive(Serialize)]
@@ -160,8 +160,12 @@ fn main() {
160160
app.run(|app_handle, e| {
161161
if let Event::CloseRequested { label, api, .. } = e {
162162
api.prevent_close();
163-
let window = app_handle.get_window(&label).unwrap();
164-
window.emit("close-requested", ()).unwrap();
163+
let app_handle = app_handle.clone();
164+
ask("Tauri API", "Are you sure?", move |answer| {
165+
if answer {
166+
app_handle.get_window(&label).unwrap().close().unwrap();
167+
}
168+
});
165169
}
166170
})
167171
}

0 commit comments

Comments
 (0)