mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 18:47:07 -06:00
Fix #1275: open the commands' file dialogs without blocking the frame on Linux
rfd's Linux backend goes through the XDG desktop portal, and a blocking
`rfd::FileDialog` leaves the main thread waiting on the portal's answer
without pumping winit's events: while the Open dialog is up the window stops
answering the compositor, which offers to force-quit the app ("EffectCraft is
not responding"), until a file is picked.
The non-blocking host hook for the commands' dialogs (`rfd::AsyncFileDialog`
on a worker thread, run when the dialog closes) was macOS-only since #511.
Install it on Linux too. Windows keeps the blocking pickers (its dialog pumps
its own messages), and the panel buttons' pickers (Settings, effect file
pickers) are unchanged.
Checked headlessly against a mock XDG portal: with the hook the main thread
stays in the event loop while a worker blocks in the portal wait, `ui.inspect`
still answers 7.5 s after the dialog opened, and an answered dialog runs
File ▸ Import; without it the main thread itself blocks in the portal wait and
the app answers nothing until the dialog closes.
Co-Authored-By: DeepSeek V4.1 Flash Free <noreply@deepseek.com>
This commit is contained in:
co-authored by
DeepSeek V4.1 Flash Free
parent
2d805d1b69
commit
b113278a1c
@@ -23,8 +23,8 @@ mod control_server;
|
||||
mod launch_guard;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
// Built everywhere so it stays checked; only the macOS app uses it.
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
// Built everywhere so it stays checked; the macOS and Linux apps use it.
|
||||
#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), allow(dead_code))]
|
||||
mod sheet_dialogs;
|
||||
mod window;
|
||||
|
||||
@@ -122,6 +122,22 @@ fn control_port(name: &str, value: Option<&str>) -> Result<u16, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The commands' file dialogs open without blocking the frame where the host's backend would:
|
||||
/// on macOS as sheets instead of nested modal loops (#511), on Linux through the desktop
|
||||
/// portal from a worker thread — a blocking portal dialog leaves the main thread waiting on its
|
||||
/// answer without pumping events, so the window stops answering the compositor and the desktop
|
||||
/// offers to force-quit it while the Open dialog is up (#1275). Windows' blocking dialog pumps
|
||||
/// its own messages, so it keeps [`effectcraft_ui_egui::Hooks::pick_files`] and friends.
|
||||
fn install_file_dialogs(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
let ctx = ctx.clone();
|
||||
app.hooks.file_dialog_async = Some(Box::new(move |dialog, done| sheet_dialogs::show(dialog, done, ctx.clone())));
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
let _ = (app, ctx);
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
// First, so the panic hook and every start-up warning are recorded. Records go to stderr
|
||||
// and the session log; Help ▸ Enable Logging writes through this logger too, and warnings
|
||||
@@ -301,11 +317,11 @@ fn main() -> eframe::Result {
|
||||
let file = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||||
d.set_file_name(file).save_file().map(|p| p.to_string_lossy().to_string())
|
||||
}));
|
||||
// The commands' file dialogs don't block the frame where the platform's backend
|
||||
// would (#511, #1275); see [`install_file_dialogs`].
|
||||
install_file_dialogs(&mut app, &cc.egui_ctx);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// The commands' file dialogs open as sheets instead of nested modal loops (#511).
|
||||
let ctx = cc.egui_ctx.clone();
|
||||
app.hooks.file_dialog_async = Some(Box::new(move |dialog, done| sheet_dialogs::show(dialog, done, ctx.clone())));
|
||||
app.hooks.app_action = Some(Box::new(native_menu::app_action));
|
||||
app.hooks.clipboard_text = Some(Box::new(native_menu::clipboard_text));
|
||||
}
|
||||
@@ -597,4 +613,17 @@ mod tests {
|
||||
assert_eq!(parse_env(&[], Err(not_utf8)).unwrap_err(), format!("{need}, not `9\u{fffd}`"));
|
||||
}
|
||||
}
|
||||
|
||||
/// #1275: a blocking desktop-portal dialog on Linux leaves the window not answering the
|
||||
/// compositor while it is open, so the commands' file dialogs go through the non-blocking
|
||||
/// host hook there (as they do on macOS, #511). Windows' dialog pumps its own messages.
|
||||
#[test]
|
||||
fn commands_file_dialogs_do_not_block_the_frame() {
|
||||
let mut app = effectcraft_ui_egui::EffectcraftApp::new(effectcraft_engine::Session::default());
|
||||
super::install_file_dialogs(&mut app, &egui::Context::default());
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
assert!(app.hooks.file_dialog_async.is_some(), "the commands' file dialogs must not block the frame");
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
assert!(app.hooks.file_dialog_async.is_none(), "the blocking dialog pumps its own messages here");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
//! File dialogs that don't block the frame (macOS), for `Hooks::file_dialog_async`.
|
||||
//! File dialogs that don't block the frame (macOS and Linux), for `Hooks::file_dialog_async`.
|
||||
//!
|
||||
//! A blocking `rfd::FileDialog` runs a nested modal event loop inside eframe's frame, that is
|
||||
//! inside winit's event handler. On macOS a file dragged over the window while that loop ran
|
||||
//! re-entered winit's handler, which panics across the Objective-C boundary and aborts the app
|
||||
//! (#511). `rfd::AsyncFileDialog` shows the dialog as a sheet on the window and returns at once;
|
||||
//! the answer arrives when the sheet closes, outside the frame, and the app runs the command then.
|
||||
//! (#511). On Linux the blocking dialog is worse than slow: rfd uses the XDG desktop portal, so
|
||||
//! the main thread sits waiting on the portal's answer and never pumps winit's events — the
|
||||
//! window stops answering the compositor and the desktop offers to force-quit the app while the
|
||||
//! Open dialog is up (#1275). `rfd::AsyncFileDialog` shows the dialog as a sheet on the window
|
||||
//! (macOS) or through the portal from a worker thread (Linux) and returns at once; the answer
|
||||
//! arrives when the dialog closes, outside the frame, and the app runs the command then.
|
||||
|
||||
use effectcraft_ui_egui::{DialogDone, FileDialog};
|
||||
use rfd::{AsyncFileDialog, FileHandle};
|
||||
|
||||
@@ -163,8 +163,10 @@ pub struct Hooks {
|
||||
pub app_action: Option<Box<dyn Fn(&str) -> bool>>,
|
||||
/// Shows a command's file dialog without blocking the frame and calls `done` with the
|
||||
/// answer (the host then asks for a repaint). Used in place of the blocking pickers above for
|
||||
/// the commands' dialogs (File ▸ Import, Open, Save As…) where a modal dialog's nested event
|
||||
/// loop inside a frame is unsafe: on macOS a drag over the window then aborted the app (#511).
|
||||
/// the commands' dialogs (File ▸ Import, Open, Save As…) where blocking the frame is unsafe or
|
||||
/// leaves the window unresponsive: on macOS a nested modal event loop there aborted the app on
|
||||
/// a drag over the window (#511), and on Linux a blocking desktop-portal dialog never pumps
|
||||
/// events, so the compositor marks the window as not responding (#1275).
|
||||
pub file_dialog_async: Option<Box<dyn Fn(FileDialog, DialogDone)>>,
|
||||
/// Tells the user the graphics device was lost and the window is closing, once. `saved` is
|
||||
/// the auto-save of unsaved changes (`None`: nothing was unsaved), its path or why it failed.
|
||||
|
||||
@@ -1381,9 +1381,11 @@ fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) -> Option<Res
|
||||
"mp4", "mov", "m4v", "mkv", "webm", "mxf", "png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "exr", "dpx", "cin", "wav", "aif", "aiff", "mp3",
|
||||
"flac", "ogg", "opus", "svg", "pdf", "ai", "eps", "psd", "psb", "tga", "hdr", "gltf", "glb", "obj", "stl", "fbx", "json", "csv", "tsv",
|
||||
];
|
||||
// A host whose modal dialogs would run a nested event loop inside this frame (macOS: a
|
||||
// drag over the window then re-entered winit's event handler and aborted, #511) shows the
|
||||
// dialog without blocking; the command continues when the answer arrives.
|
||||
// A host whose blocking dialogs are unsafe inside this frame shows the dialog without
|
||||
// blocking — macOS: the nested event loop re-entered winit's handler on a drag over the
|
||||
// window and aborted the app (#511); Linux: the desktop portal's answer never gets pumped,
|
||||
// so the compositor marks the window as not responding (#1275) — and the command continues
|
||||
// when the answer arrives.
|
||||
if let Some(spawn) = app.hooks.file_dialog_async.as_ref() {
|
||||
let request = match ask {
|
||||
Ask::Folder => crate::FileDialog::Folder,
|
||||
|
||||
Reference in New Issue
Block a user