From 1c4894b2f744fdbb026f8f73eb8115fd1377db1d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 28 Oct 2026 08:01:14 -0700 Subject: [PATCH] Brushes: Pattern Brush Options' OK leaves corner tiles that were not set OK used to save None for a corner tile the dialog showed as None because the brush had no art or source for it, so that corner stopped drawing the other kind of corner's art once the brush got some. It also copied the art a corner borrows from the other kind of corner when that corner kept its art. Now OK saves a corner tile's choice only when it was set from its menu or with `ui.dialog.set`, or when the art it borrows goes away. --- crates/ui-egui/src/control.rs | 2 +- crates/ui-egui/src/dialogs.rs | 9 +++++ .../ui-egui/src/dialogs/brush_art_options.rs | 29 +++++++++++++- .../src/dialogs/brush_options/tests.rs | 38 +++++++++++++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 14d6fe831..68bba9ade 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -327,7 +327,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques return err(format!("the `{}` dialog has no field `{f}`; its fields: {names}", d.kind)); } for (f, value) in fields { - d.fields.insert(f, value); + crate::dialogs::set_field(d, f, value); } ok(serde_json::to_value(&*d).unwrap_or_default()) } diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 49dfe37b9..6920c8466 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -410,6 +410,15 @@ pub fn accepts_field(d: &Dialog, field: &str) -> bool { || (spec.passes)(d, field) } +/// Set `field` of `d` to `value` for `ui.dialog.set`, as the dialog's own control for it does: a +/// Pattern Brush Options corner tile set this way is saved by OK, as one chosen from its menu is. +pub fn set_field(d: &mut Dialog, field: String, value: Value) { + if d.kind == brush_options::KIND { + brush_art_options::note_set(d, &field); + } + d.fields.insert(field, value); +} + /// The fields `ui.dialog.set` can set on `d`, sorted, without the `__` ones: those it holds, /// `discard`, `preview` and the spec's `inputs` for its kind. pub fn field_names(d: &Dialog) -> Vec { diff --git a/crates/ui-egui/src/dialogs/brush_art_options.rs b/crates/ui-egui/src/dialogs/brush_art_options.rs index 2c150b4bd..f9b5bf558 100644 --- a/crates/ui-egui/src/dialogs/brush_art_options.rs +++ b/crates/ui-egui/src/dialogs/brush_art_options.rs @@ -63,6 +63,24 @@ fn corner_art_field<'a>(def: Option<&Value>, field: &'a str) -> Option<&'a str> } } +/// The dialog field that records that corner tile `key`'s menu was set. +fn corner_set_key(key: &str) -> String { + format!("__{key}Set") +} + +/// Sets corner tile `key` to `choice` from its menu. +pub(super) fn choose_corner(d: &mut Dialog, key: &str, choice: &str) { + d.fields.insert(key.into(), json!(choice)); + note_set(d, key); +} + +/// Records that field `field` was set, from its menu or `ui.dialog.set`, when it is a corner tile. +pub(super) fn note_set(d: &mut Dialog, field: &str) { + if CORNER_TILES.iter().any(|(key, ..)| *key == field) { + d.fields.insert(corner_set_key(field), json!(true)); + } +} + /// The most key colours offered. const MAX_COLORS: usize = 16; @@ -236,14 +254,21 @@ pub(super) fn params(d: &Dialog) -> Value { }); // None or an automatic tile replaces the corner art. Original keeps the art the corner // draws, the other kind of corner's art included, so changing the other kind leaves it. + // A None the menu was not set to, on a corner without art or a source, leaves the + // definition as it is: that corner still draws the other kind's art when it gets some. let def = d.fields.get("__def"); - for (key, art, source, _) in CORNER_TILES { + for (key, art, source, other) in CORNER_TILES { let choice = d.str(key); + let unset = |f: &str| def.and_then(|d| d.get(f)).is_none_or(Value::is_null); + if choice == CornerSource::None.id() && !d.bool(&corner_set_key(key)) && unset(source) && corner_art_field(def, art).is_none() { + continue; + } if let Some(s) = CornerSource::parse(&choice) { p[art] = Value::Null; p[source] = json!(s.id()); } else if choice == "original" && let Some(from) = corner_art_field(def, art).filter(|f| *f != art) + && CORNER_TILES.iter().find(|(_, a, _, _)| *a == other).is_some_and(|(k, ..)| d.str(k) != "original") { p[art] = def.and_then(|d| d.get(from)).cloned().unwrap_or_default(); } @@ -480,7 +505,7 @@ fn tiles(ui: &mut egui::Ui, d: &mut Dialog) { if let Some((id, _)) = widgets::dropdown_named(ui, ("brush-pattern-corner", key), Some(name), current, &labels, 76.0).and_then(|i| choices.get(i)) { - d.fields.insert(key.into(), json!(id)); + choose_corner(d, key, id); } } }); diff --git a/crates/ui-egui/src/dialogs/brush_options/tests.rs b/crates/ui-egui/src/dialogs/brush_options/tests.rs index 094a49f1e..9adeffed1 100644 --- a/crates/ui-egui/src/dialogs/brush_options/tests.rs +++ b/crates/ui-egui/src/dialogs/brush_options/tests.rs @@ -509,6 +509,44 @@ fn pattern_options_keep_the_corner_art_a_corner_borrows() { assert_eq!(def["outer_source"], json!("none")); } +/// OK leaves the corner fields of a corner tile the menus were not set to as they were: a corner +/// without art keeps borrowing the other kind's art when it gets some, and a corner that borrows +/// art still borrows it. A None picked from the menu is saved, after another choice too. +#[test] +fn pattern_options_ok_leaves_corner_tiles_the_menus_were_not_set_to() { + let mut app = app(); + app.run("ui.brushOptions", json!({"name": "Diamonds"})).unwrap(); + set(&mut app, json!({"scale": 150})); + super::super::confirm(&mut app).unwrap(); + let def = app.run("brush.get", json!({"name": "Diamonds"})).unwrap(); + assert!(def.get("outer_source").is_none() && def.get("inner_source").is_none(), "{def}"); + let art = app.run("brush.get", json!({"name": "Chain"})).unwrap()["outer_corner"].clone(); + app.run("brush.options", json!({"name": "Diamonds", "params": {"outer_corner": art.clone()}})).unwrap(); + app.run("ui.brushOptions", json!({"name": "Diamonds"})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().str("innerCornerTile"), "original", "the inner corners draw the outer art"); + super::super::confirm(&mut app).unwrap(); + let def = app.run("brush.get", json!({"name": "Diamonds"})).unwrap(); + assert!(def["outer_corner"].is_object() && def.get("inner_corner").is_none(), "{def}"); + + app.run("brush.new", json!({"type": "pattern", "name": "Plain", "params": {"side": art}})).unwrap(); + app.run("ui.brushOptions", json!({"name": "Plain"})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + crate::dialogs::brush_art_options::choose_corner(d, "outerCornerTile", "autoCentered"); + crate::dialogs::brush_art_options::choose_corner(d, "outerCornerTile", "none"); + super::super::confirm(&mut app).unwrap(); + let def = app.run("brush.get", json!({"name": "Plain"})).unwrap(); + assert_eq!(def["outer_source"], json!("none")); + assert!(def.get("inner_source").is_none(), "{def}"); + // None set through the control channel is saved too. + app.run("ui.brushOptions", json!({"name": "Plain"})).unwrap(); + let (set, _) = crate::control::ControlRequest::new("ui.dialog.set", json!({"field": "innerCornerTile", "value": "none"})); + let crate::control::Outcome::Done(r) = crate::control::handle(&mut app, &egui::Context::default(), &set) else { panic!("not done") }; + assert!(r.get("error").is_none(), "{r}"); + super::super::confirm(&mut app).unwrap(); + let def = app.run("brush.get", json!({"name": "Plain"})).unwrap(); + assert_eq!(def["inner_source"], json!("none")); +} + /// Art Brush Options' Width and Pattern Brush Options' Scale each have a slider beside their /// field, which screen readers name as the row, with the field's number and text over the /// value's whole range.