From ce0ae4e3873ffdbabb50cbb24445f189cbcabd77 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 08:38:14 -0700 Subject: [PATCH] Fix #341: answered file dialogs run before the close decision The close decision moved to App::logic, which runs before App::ui, while the queued file-dialog answers still ran in ui. An answer that arrived in the frame that closed the window changed the project after the decision, so the window could close without asking to save. The answers now run in logic, before the decision. --- crates/ui-egui/src/lib.rs | 3 ++- crates/ui-egui/src/panels/unsaved.rs | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 4e0b8b10c..5a34927b2 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1623,7 +1623,6 @@ impl EffectcraftApp { fn frame(&mut self, ui: &mut egui::Ui) { let ctx = ui.ctx().clone(); self.apply_gpu_failure(&ctx); - self.run_dialog_answers(); self.auto.begin_frame(); self.frames.set_context(&ctx); if self.session.render_job.is_some() { @@ -1920,6 +1919,8 @@ impl eframe::App for EffectcraftApp { // window while AppKit's run loop is running (#341). craft_quit::on_close_request(ctx); } else { + // An answered file dialog can change the project, so it runs before the close decision. + self.run_dialog_answers(); panels::unsaved::close_or_quit(self, ctx); } } diff --git a/crates/ui-egui/src/panels/unsaved.rs b/crates/ui-egui/src/panels/unsaved.rs index 9c8c08870..4a94265b4 100644 --- a/crates/ui-egui/src/panels/unsaved.rs +++ b/crates/ui-egui/src/panels/unsaved.rs @@ -406,4 +406,22 @@ mod tests { assert!(canceled, "the close is canceled"); assert_eq!(app.dialog, Some(Dialog::UnsavedChanges)); } + + /// A file dialog answered in the frame that closes the window runs its command before the + /// close decision, so a change it makes is asked about. + #[test] + fn a_dialog_answer_runs_before_the_close_decision() { + let (mut app, ctx) = app(); + assert!(!app.session.is_dirty()); + let answer = crate::DialogAnswer { command: "comp.new".into(), key: "name", params: json!({"width": 64, "height": 64}), paths: vec!["Main".into()] }; + app.dialog_answers.0.send(answer).unwrap(); + let mut raw = egui::RawInput::default(); + raw.viewports.entry(egui::ViewportId::ROOT).or_default().events.push(egui::ViewportEvent::Close); + let mut frame = eframe::Frame::_new_kittest(); + let out = ctx.run_logic(&raw, |ctx| eframe::App::logic(&mut app, ctx, &mut frame)); + let canceled = out.viewport_commands.get(&egui::ViewportId::ROOT).is_some_and(|c| c.contains(&egui::ViewportCommand::CancelClose)); + assert!(app.session.is_dirty(), "the answer ran"); + assert!(canceled, "the close is canceled"); + assert_eq!(app.dialog, Some(Dialog::UnsavedChanges)); + } }