From 7d4ddc3679e24ef71760095c7c1ba743f77d4e50 Mon Sep 17 00:00:00 2001 From: moppeeer <339764117+moppeeer@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:21:02 +0200 Subject: [PATCH] TUT4.2: reject malformed previews and cancel replaced dialogs --- crates/ui-egui/src/control.rs | 27 +++++++++++++++++++++++++++ crates/ui-egui/src/dialogs.rs | 24 +++++++++++++++++++++++- docs/ui-parity.md | 4 ++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 8ee456520..d7c25b98b 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -339,6 +339,9 @@ fn handle_request(app: &mut DesignApp, ctx: &egui::Context, req: &ControlRequest }, "ui.dialog.open" => { let Some(id) = s("id") else { return err("missing `id`") }; + // Replacing a modal cancels its transaction just like its Cancel button. + // In particular, an uncommitted style preview must not leak into the new dialog. + crate::dialogs::cancel(app); if id == "findChange" || app.ui.dialog.as_ref().is_some_and(|d| d.id == "findChange") { app.session.reset_find_scope(); } @@ -440,6 +443,30 @@ pub(crate) fn page_index(p: &Value, default: usize, count: usize) -> Result DesignApp { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); app.session.execute("file.new", &json!({"pages":2})).unwrap(); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index ab83f1765..a94bdbd6d 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2834,7 +2834,9 @@ fn style_options_preview(app: &mut DesignApp, d: &mut Dialog) { if params["para"][attr].is_null() { params["para"][attr] = json!({}); } - params["para"][attr][field] = value.clone(); + if let Some(fields) = params["para"][attr].as_object_mut() { + fields.insert(field.into(), value.clone()); + } } else { params["para"][attr] = value.clone(); } @@ -4954,6 +4956,26 @@ mod tests { assert!(app.session.doc().unwrap().interaction.is_none()); } + #[test] + fn style_preview_rejects_conflicting_scalar_and_nested_rule_fields_without_panicking() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let before = app.session.doc().unwrap().doc.clone(); + let mut dialog = Dialog::new( + "paragraphStyleOptions", + json!({ + "name": designcraft_doc::BASIC_PARAGRAPH, "preview": true, + "p.ruleAbove": false, "p.ruleAbove.weight": 2 + }), + ); + style_options_preview(&mut app, &mut dialog); + assert!(dialog.fields.contains_key("status")); + assert_eq!(app.session.doc().unwrap().doc, before); + app.ui.dialog = Some(dialog); + cancel(&mut app); + assert!(app.session.doc().unwrap().interaction.is_none()); + } + #[test] fn step_and_repeat_dialog_previews_and_makes_the_copies_as_one_undo_step() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 0bf86ec2d..ef004bf1c 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -61,6 +61,10 @@ context menus, typing at a caret) are things a layout artist does every minute, ## Remaining effort +Style Preview rejects conflicting scalar/nested automation fields without a panic. Replacing a +previewing dialog through the control channel cancels its transaction, restoring the original +document and normal undo recording before the replacement opens. + Paragraph and Character Style Options now preview edits to existing styles. Preview updates every use, remains outside undo history, and restores the starting document when disabled or cancelled. OK applies the final edit once. The color-preview step at about 4:00 in