From dd8f144770d4b833fabfcca716be5ed415e81c8d Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 10 Oct 2026 11:28:00 +0700 Subject: [PATCH] Fix #867: macOS file dialogs no longer run inside the window's event handler On macOS the Open and Save panels ran modally on the UI thread, inside the window's event handler. Resizing the panel delivered window events to that handler again, and the app crashed when the mouse was released. They now open from a thread of their own, as on Linux (#592): rfd runs the panel on the main thread from the run loop, outside the handler, and the app picks the answer up when it comes. Co-Authored-By: Claude Opus 5.5 --- apps/vectorcraft/src/main.rs | 15 +++++++++------ crates/ui-egui/src/picks.rs | 7 ++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index f7b6771d..92ab9d88 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -274,9 +274,12 @@ fn write_file(path: &str, bytes: &[u8]) -> Result<(), String> { fileio::write_atomic(std::path::Path::new(path), bytes).map_err(|e| e.to_string()) } -/// Linux: show the dialog for `request` over `parent` on a thread of its own, answering on the -/// receiver. Shown on the UI thread, nothing would answer the compositor meanwhile, which then -/// offers to kill the window as not responding (#592). +/// Linux and macOS: show the dialog for `request` over `parent` on a thread of its own, answering +/// on the receiver. Shown on the UI thread, nothing would answer the compositor meanwhile on Linux, +/// which then offers to kill the window as not responding (#592); on macOS the panel ran modally +/// inside the window's event handler, and resizing it delivered window events to that handler +/// again, which crashed the app (#867). From another thread, rfd runs the panel on the main thread +/// from the run loop, outside the handler. fn start_pick(request: PickRequest, parent: &Parent) -> Option>> { let dialog = file_dialog(&request, parent); let (tx, rx) = std::sync::mpsc::channel(); @@ -296,9 +299,9 @@ fn services(parent: Parent) -> Services { pick_open: Some(Box::new(move |pick: &FilePick| pick_now(PickRequest::Open(pick.clone()), &p1).into_iter().next())), pick_open_multi: Some(Box::new(move || pick_now(PickRequest::OpenMany, &p2))), pick_save: Some(Box::new(move |pick: &FilePick| pick_now(PickRequest::Save(pick.clone()), &p3).into_iter().next())), - // Windows and macOS dialogs run the window's events while they are open; Linux's don't. - start_pick: cfg!(all(unix, not(target_os = "macos"))) - .then(|| Box::new(move |request: PickRequest| start_pick(request, &p5)) as vectorcraft_ui_egui::picks::StartPick), + // Windows dialogs run the window's events while they are open; Linux's don't, and macOS + // ones re-enter the window's event handler (#867). + start_pick: cfg!(unix).then(|| Box::new(move |request: PickRequest| start_pick(request, &p5)) as vectorcraft_ui_egui::picks::StartPick), read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))), write: Some(Box::new(write_file)), // Background Save and Export write from a worker thread. diff --git a/crates/ui-egui/src/picks.rs b/crates/ui-egui/src/picks.rs index 463a47fc..8ee50853 100644 --- a/crates/ui-egui/src/picks.rs +++ b/crates/ui-egui/src/picks.rs @@ -2,9 +2,10 @@ //! //! The app asks for a path where it needs one (Open, Save As, Export, Place, Relink, presets…) //! and goes on with it at once, so a dialog shown on the UI thread holds the window until it -//! closes. On Windows and macOS the dialog runs the window's events meanwhile; on Linux nothing -//! does, so a Wayland compositor finds the window not answering and offers to kill it. There the -//! host shows dialogs on another thread ([`Services::start_pick`]): asking returns no path for now, +//! closes. On Windows the dialog runs the window's events meanwhile; on Linux nothing does, so a +//! Wayland compositor finds the window not answering and offers to kill it, and on macOS they +//! re-enter the window's event handler, which crashed the app when the panel was resized (#867). +//! There the host shows dialogs on another thread ([`Services::start_pick`]): asking returns no path for now, //! and when the dialog answers, what asked (the command, the dialog's OK, a panel's button: the //! *entry*) runs again and gets the picked path at once.