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.