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

+48 -22
View File
@@ -23,6 +23,33 @@ fn display_order(mut catalog: Catalog) -> Catalog {
}
#[cfg(not(target_arch = "wasm32"))]
/// Runs `f` on the open Appearance dialog's state, once a file picker has answered.
fn with_state(app: &mut SolveApp, f: impl FnOnce(&mut SolveApp, &mut Ap)) {
let Some(mut dialog) = app.dialog.take() else { return };
if let crate::dialogs::Kind::Appearance(k) = &mut dialog.kind {
f(app, k);
}
if app.dialog.is_none() {
app.dialog = Some(dialog);
}
}
/// Exports the design's library to `path` and shows it as the local library.
fn export(app: &mut SolveApp, k: &mut Ap, path: String, overwrite: bool) {
k.note = Some(match app.run("library.export", json!({"path":path,"overwrite":overwrite})) {
Ok(result) => {
let parsed = result.get("catalog").cloned().and_then(|v| serde_json::from_value::<Catalog>(v).ok()).map(display_order);
let count = parsed.as_ref().map_or(0, |c| c.entries.len());
k.local.path = path;
k.local.catalog = parsed;
k.local.selected = None;
k.local.search.clear();
format!("Saved {count} reusable definitions. The design is unchanged.")
}
Err(error) => error,
});
}
fn load(app: &mut SolveApp, library: &mut LocalLibrary) -> Result<String, String> {
let result = app.run("library.inspect", json!({"path":library.path}))?;
let catalog: Catalog = serde_json::from_value(result.get("catalog").cloned().ok_or("Library response is missing")?).map_err(|e| e.to_string())?;
@@ -47,23 +74,17 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelInpu
let save = ui.button(crate::i18n::tr_label("Export design library..."));
crate::scenario::publish_handle("library:export", save.rect.center());
if save.clicked() {
let chosen = match app.services.pick_save.as_ref() {
Some(pick) => pick("my-library.solidcraft-library.json", &["json"]),
None => (!k.local.path.is_empty()).then(|| k.local.path.clone()),
};
if let Some(path) = chosen {
k.note = Some(match app.run("library.export", json!({"path":path,"overwrite":k.local.overwrite})) {
Ok(result) => {
let parsed = result.get("catalog").cloned().and_then(|v| serde_json::from_value::<Catalog>(v).ok()).map(display_order);
let count = parsed.as_ref().map_or(0, |c| c.entries.len());
k.local.path = path;
k.local.catalog = parsed;
k.local.selected = None;
k.local.search.clear();
format!("Saved {count} reusable definitions. The design is unchanged.")
let overwrite = k.local.overwrite;
if app.services.pick_save.is_some() {
// The picker answers in a later frame (#1088); the export runs then.
app.pick_save("my-library.solidcraft-library.json", &["json"], move |app, path| {
if let Some(path) = path {
with_state(app, |app, k| export(app, k, path, overwrite));
}
Err(error) => error,
});
} else if !k.local.path.is_empty() {
let path = k.local.path.clone();
export(app, k, path, overwrite);
}
}
ui.label(
@@ -84,13 +105,18 @@ fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelI
k.local.selected = None;
}
ui.horizontal(|ui| {
if ui.button(crate::i18n::tr_label("Open library...")).clicked()
&& let Some(path) = app.services.pick_open.as_ref().and_then(|pick| pick())
{
k.local.path = path;
k.local.catalog = None;
k.local.selected = None;
k.note = Some(load(app, &mut k.local).unwrap_or_else(|error| error));
if ui.button(crate::i18n::tr_label("Open library...")).clicked() {
// The picker answers in a later frame (#1088); the library loads then.
app.pick_open(|app, path| {
if let Some(path) = path {
with_state(app, |app, k| {
k.local.path = path;
k.local.catalog = None;
k.local.selected = None;
k.note = Some(load(app, &mut k.local).unwrap_or_else(|error| error));
});
}
});
}
let load_button = ui.add_enabled(!k.local.path.is_empty(), egui::Button::new(crate::i18n::tr_label("Load library")));
crate::scenario::publish_handle("library:load", load_button.rect.center());