From 3a6770e7842558d8599dc50027be163835e3ba3d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 30 Oct 2026 22:25:32 -0700 Subject: [PATCH] Brushes: Yes in Delete Brush asks again when a stroke started using a brush meanwhile The question for unused brushes checks again when Yes is chosen. If a stroke uses one of the brushes by then, the dialog asks with Remove Strokes and Cancel instead of deleting. --- crates/ui-egui/src/dialogs/brush_delete.rs | 22 ++++++++++++++++++- .../ui-egui/src/dialogs/brush_delete/tests.rs | 19 ++++++++++++++++ crates/ui-egui/src/panels/brushes.rs | 4 +--- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/dialogs/brush_delete.rs b/crates/ui-egui/src/dialogs/brush_delete.rs index ee4d6c964..84f2786d4 100644 --- a/crates/ui-egui/src/dialogs/brush_delete.rs +++ b/crates/ui-egui/src/dialogs/brush_delete.rs @@ -28,6 +28,21 @@ pub fn ask(app: &mut VectorcraftApp, params: Value, used: bool) { app.ui.dialog = Some(Dialog::new(KIND, json!({"params": params, "used": used, "message": message}))); } +/// Whether a stroke uses one of `names` (`brush.unused`), or `None` when the query fails. +pub fn used(app: &mut VectorcraftApp, names: &[String]) -> Option { + let unused = app.run("brush.unused", json!({})).ok()?; + let unused = unused.get("names").and_then(Value::as_array).cloned().unwrap_or_default(); + Some(names.iter().any(|name| !unused.iter().any(|n| n.as_str() == Some(name)))) +} + +/// The brush names in `brush.delete` params (`name` or `names`). +fn names(params: &Value) -> Vec { + match params.get("name").and_then(Value::as_str) { + Some(name) => vec![name.to_string()], + None => params.get("names").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect(), + } +} + fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> DialogAction { let mut action = DialogAction::None; ui.horizontal(|ui| { @@ -50,9 +65,14 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> DialogActi action } -/// Closes before running, so a dialog the command opens stays open. +/// Closes before running, so a dialog the command opens stays open. Yes on brushes that a stroke +/// has started to use since the question opened asks again with Remove Strokes and Cancel. fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let params = d.fields.get("params").cloned().unwrap_or_else(|| json!({})); + if !d.bool("used") && used(app, &names(¶ms)) == Some(true) { + ask(app, params, true); + return Ok(json!({})); + } app.ui.dialog = None; app.run("brush.delete", params) } diff --git a/crates/ui-egui/src/dialogs/brush_delete/tests.rs b/crates/ui-egui/src/dialogs/brush_delete/tests.rs index 407351c79..6f753505c 100644 --- a/crates/ui-egui/src/dialogs/brush_delete/tests.rs +++ b/crates/ui-egui/src/dialogs/brush_delete/tests.rs @@ -50,6 +50,25 @@ fn visible_yes_and_no_buttons_control_unused_deletion() { assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1); } +#[test] +fn yes_asks_again_when_a_stroke_started_using_the_brush() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100})).unwrap(); + let name = "10 pt. Oval"; + let ctx = context(); + ask(&mut app, json!({"names": [name]}), false); + let path = NodeId(app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 60, "height": 40})).unwrap()["id"].as_u64().unwrap()); + app.run("brush.apply", json!({"name": name})).unwrap(); + let before = app.session.active().unwrap().history.undo.len(); + press(&ctx, &mut app, "Yes", 0.0); + assert!(app.ui.dialog.as_ref().is_some_and(|d| d.kind == KIND && d.bool("used")), "asks again with Remove Strokes"); + assert_eq!(app.session.active().unwrap().history.undo.len(), before); + let stroke = app.session.active().unwrap().doc.node(path).and_then(|n| n.appearance.stroke()).unwrap(); + assert_eq!(stroke.brush.as_deref(), Some(name)); + press(&ctx, &mut app, "Remove Strokes", 1.0); + assert!(app.run("brush.get", json!({"name": name})).is_err()); +} + #[test] fn visible_remove_strokes_and_cancel_buttons_control_used_deletion() { for choice in ["Cancel", "Remove Strokes"] { diff --git a/crates/ui-egui/src/panels/brushes.rs b/crates/ui-egui/src/panels/brushes.rs index 279677527..6f99c8a36 100644 --- a/crates/ui-egui/src/panels/brushes.rs +++ b/crates/ui-egui/src/panels/brushes.rs @@ -207,11 +207,9 @@ fn run_on(app: &mut VectorcraftApp, id: &str, names: &[String]) { /// Delete `names` after asking: Yes or No when none of them is in use, else Remove Strokes or /// Cancel. With `now` (Alt-click on the bottom button), brushes that are not in use go at once. fn delete(app: &mut VectorcraftApp, names: &[String], now: bool) { - let Ok(unused) = app.run("brush.unused", json!({})) else { + let Some(used) = crate::dialogs::brush_delete::used(app, names) else { return; }; - let unused = unused.get("names").and_then(Value::as_array).cloned().unwrap_or_default(); - let used = names.iter().any(|name| !unused.iter().any(|n| n.as_str() == Some(name))); if now && !used { run_on(app, "brush.delete", names); return;