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

+29 -18
View File
@@ -337,9 +337,11 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
close = true;
}
if item(ui, &format!("{}…", app.tr("ui.open")), "Ctrl+O") {
if let Some(p) = app.services.pick_open.as_ref().and_then(|f| f()) {
app.open_path_async(&p);
}
app.pick_open(|app, path| {
if let Some(path) = path {
app.open_path_async(&path);
}
});
close = true;
}
ui.menu_button(app.tr("ui.open_recent"), |ui| {
@@ -455,9 +457,12 @@ fn save_as(app: &mut SolveApp) {
fn save_as_with_extension(app: &mut SolveApp, extension: &str) {
let name = format!("{}.{extension}", app.session.doc.name);
if let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name, &[solidcraft_engine::io::DESIGN_EXT, "json"])) {
let _ = app.run("file.save_as", json!({"path": p}));
}
app.pick_save(&name, &[solidcraft_engine::io::DESIGN_EXT, "json"], |app, path| {
if let Some(path) = path {
let _ = app.run("file.save_as", json!({"path": path}));
}
crate::documents::saved_for_close(app);
});
}
fn export(app: &mut SolveApp, ext: &str) {
@@ -467,14 +472,17 @@ fn export(app: &mut SolveApp, ext: &str) {
/// Export to a file with extension `ext`, in `format` when the extension alone doesn't name it.
fn export_as(app: &mut SolveApp, ext: &str, format: Option<&str>) {
let name = format!("{}.{ext}", app.session.doc.name);
let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name, &[ext])) else { return };
let params = match format {
Some(f) => json!({"path": p, "format": f}),
None => json!({"path": p}),
};
if app.run("file.export", params).is_ok() {
app.set_status(format!("Exported {p}"), false);
}
let format = format.map(str::to_string);
app.pick_save(&name, &[ext], move |app, path| {
let Some(path) = path else { return };
let mut params = json!({"path": path});
if let Some(format) = format {
params["format"] = json!(format);
}
if app.run("file.export", params).is_ok() {
app.set_status(format!("Exported {path}"), false);
}
});
}
pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
@@ -1152,9 +1160,11 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
Key::N => crate::documents::new_design(app),
Key::Comma => crate::prefs::open_at(app, 0),
Key::O => {
if let Some(p) = app.services.pick_open.as_ref().and_then(|f| f()) {
app.open_path_async(&p);
}
app.pick_open(|app, path| {
if let Some(path) = path {
app.open_path_async(&path);
}
});
}
_ if !shown => {}
Key::Z => drop(app.run("edit.undo", json!({}))),
@@ -1215,7 +1225,8 @@ mod header_report_tests {
let mut h = crate::scenario::Harness::new();
h.app.run("solid.box", json!({"length":2,"width":3,"height":4})).unwrap();
let dir = h.dir.clone();
h.app.services.pick_save = Some(Box::new(move |name, _extensions| Some(dir.join(name).to_string_lossy().into_owned())));
h.app.services.pick_save =
Some(Box::new(move |name, _extensions| Box::pin(std::future::ready(Some(dir.join(name).to_string_lossy().into_owned())))));
h.frames(3);
h.step(&json!({"click":{"handle":"appbar:folder"}})).unwrap();
h.step(&json!({"widget":"Save As JSON…"})).unwrap();