Keep Open and Save asynchronous to prevent macOS resize crashes (#1127)

Open and Save use parented AsyncFileDialog sheets; their answers run on the UI thread in a later frame, so resizing the window while a picker is open no longer re-enters winit and aborts. Editors, tab changes, Close/Quit and model-changing control requests wait for the answer; the local library Open and Export in the Appearance dialog use the same flow. Fixes #1088.
This commit is contained in:
Karl Von Richter authored and GitHub committed 2026-10-14 13:41:29 -04:00
1 parent f425089983
commit 37547f8eba
18 files changed
+564 -137

No files matched your search

+40 -10
View File
@@ -50,6 +50,8 @@ pub struct Documents {
pub quitting: Option<Vec<usize>>,
/// The "save changes?" button the arrow keys have picked; Enter gives it.
pub choice: Answer,
/// Save As is answering a Close/Quit prompt.
saving: bool,
}
impl Documents {
@@ -111,6 +113,9 @@ fn swap_in(app: &mut SolveApp, slot: DocSlot) -> DocSlot {
/// Make tab `i` the active design.
pub fn switch(app: &mut SolveApp, i: usize) {
if app.file_picker_busy() {
return;
}
app.home.open = false;
if i == app.docs.active || i >= app.docs.slots.len() {
return;
@@ -167,6 +172,9 @@ pub fn adopt(app: &mut SolveApp, session: Session) {
/// A new, empty design in its own tab.
pub fn new_design(app: &mut SolveApp) {
if app.file_picker_busy() {
return;
}
app.home.open = false;
if active_is_blank(app) {
return;
@@ -188,6 +196,9 @@ pub fn prepare_open(app: &mut SolveApp) {
/// Close tab `i`: asks first when it has unsaved changes (`force`: close anyway).
pub fn close(app: &mut SolveApp, i: usize, force: bool) {
if app.file_picker_busy() {
return;
}
let n = app.docs.count();
if i >= n {
return;
@@ -227,6 +238,9 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
/// design with unsaved changes, then sets `app.quit_requested` for the host to close the window.
/// Cancel, or a Save that didn't save, stays open.
pub fn request_quit(app: &mut SolveApp) {
if app.file_picker_busy() {
return;
}
if app.docs.quitting.is_none() {
app.docs.quitting = Some(Vec::new());
}
@@ -536,21 +550,18 @@ pub fn prompt(app: &mut SolveApp, ctx: &egui::Context) {
/// Act on the "save changes?" answer (also the control channel's `ui.documents {answer}`).
pub fn answer_prompt(app: &mut SolveApp, answer: Answer) {
if app.file_picker_busy() {
return;
}
let Some(i) = app.docs.closing else { return };
app.docs.choice = Answer::Save;
match answer {
Answer::Save => {
switch(app, i);
app.docs.saving = true;
crate::toolbar::save(app);
let saved = !app.session.is_dirty();
app.docs.closing = None;
if app.docs.quitting.is_some() {
// Quit only once it is saved; a cancelled picker or a failed save stays open.
if !saved {
app.docs.quitting = None;
}
} else if saved {
close(app, app.docs.active, true);
if !app.file_picker_busy() {
saved_for_close(app);
}
}
Answer::DontSave => {
@@ -569,6 +580,23 @@ pub fn answer_prompt(app: &mut SolveApp, answer: Answer) {
quit_step(app);
}
/// Resume Close/Quit only after Save As has answered and the write succeeded.
pub(crate) fn saved_for_close(app: &mut SolveApp) {
if !std::mem::take(&mut app.docs.saving) || app.docs.closing.is_none() {
return;
}
let saved = !app.session.is_dirty();
app.docs.closing = None;
if app.docs.quitting.is_some() {
if !saved {
app.docs.quitting = None;
}
} else if saved {
close(app, app.docs.active, true);
}
quit_step(app);
}
#[cfg(test)]
mod tests {
use super::*;
@@ -723,9 +751,11 @@ mod tests {
let file = std::env::temp_dir().join(format!("solidcraft-quit-saveas-{}.solidcraft", std::process::id()));
let _ = std::fs::remove_file(&file);
let to = file.to_string_lossy().to_string();
a.services.pick_save = Some(Box::new(move |_: &str, _: &[&str]| Some(to.clone())));
a.services.pick_save = Some(Box::new(move |_: &str, _: &[&str]| Box::pin(std::future::ready(Some(to.clone())))));
request_quit(&mut a);
answer_prompt(&mut a, Answer::Save);
assert!(!a.quit_requested && a.file_picker_busy());
crate::file_picker::tick(&mut a, &egui::Context::default());
assert!(a.quit_requested && file.exists());
let _ = std::fs::remove_file(&file);
}