From b113278a1c07ada44818c90c56c8184d88a2fa8a Mon Sep 17 00:00:00 2001 From: Madhav Agarwal Date: Sun, 18 Oct 2026 00:45:09 +0530 Subject: [PATCH] Fix #1275: open the commands' file dialogs without blocking the frame on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/effectcraft/src/main.rs | 39 +++++++++++++++++++++++---- apps/effectcraft/src/sheet_dialogs.rs | 10 ++++--- crates/ui-egui/src/lib.rs | 6 +++-- crates/ui-egui/src/menus.rs | 8 +++--- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/apps/effectcraft/src/main.rs b/apps/effectcraft/src/main.rs index 2f99bd5e..a1fb6e4a 100644 --- a/apps/effectcraft/src/main.rs +++ b/apps/effectcraft/src/main.rs @@ -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 { } } +/// 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"); + } } diff --git a/apps/effectcraft/src/sheet_dialogs.rs b/apps/effectcraft/src/sheet_dialogs.rs index 577eb0b6..7a75ef2a 100644 --- a/apps/effectcraft/src/sheet_dialogs.rs +++ b/apps/effectcraft/src/sheet_dialogs.rs @@ -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}; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 105a075e..72016cc5 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -163,8 +163,10 @@ pub struct Hooks { pub app_action: Option 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>, /// 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. diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 26a7b66f..5edc2540 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1381,9 +1381,11 @@ fn file_dialog(app: &mut EffectcraftApp, id: &str, params: &Value) -> Option crate::FileDialog::Folder,