From 9dbbe52a3c3d34cd2a0e139780bb94d340c1c908 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 07:13:31 -0800 Subject: [PATCH 1/2] Variables: Edit Data Set keeps an empty text value when OK changes nothing A row's empty text value fills its field with nothing, the same as a row without a value for that variable, and OK left every empty field out of dataset.set. An untouched OK therefore removed the empty value, and selecting the row afterward left the previous row's text on the art. Edit Data Set now marks the fields that hold the row's empty text value and keeps those values on OK. Typing in such a field, or setting it with ui.dialog.set, removes the mark, so emptying it still unsets the variable. --- crates/ui-egui/src/dialogs.rs | 6 +- crates/ui-egui/src/dialogs/variables.rs | 79 +++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 6e86304e2..82cd2b8b0 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -443,11 +443,15 @@ pub fn accepts_field(d: &Dialog, field: &str) -> bool { } /// 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. +/// Pattern Brush Options corner tile set this way is saved by OK, as one chosen from its menu is, +/// and an Edit Data Set field set this way is no longer the row's empty text value. pub fn set_field(d: &mut Dialog, field: String, value: Value) { if d.kind == brush_options::KIND { brush_art_options::note_set(d, &field); } + if d.kind == variables::KIND { + variables::note_set(d, &field); + } d.fields.insert(field, value); } diff --git a/crates/ui-egui/src/dialogs/variables.rs b/crates/ui-egui/src/dialogs/variables.rs index d63ea5562..58cb18c86 100644 --- a/crates/ui-egui/src/dialogs/variables.rs +++ b/crates/ui-egui/src/dialogs/variables.rs @@ -86,6 +86,13 @@ pub fn open(app: &mut VectorcraftApp, id: &str, preselect: Option<&str>) -> bool let Some(current) = vars.dataset(&name) else { return true }; fields.insert("name".into(), json!(name)); value_fields(&vars, Some(¤t.values), &mut fields); + // An empty text value is a value, but its field looks like a field with no value: + // mark it so OK keeps it until the field is edited. + for (var, value) in ¤t.values { + if matches!(value, DataValue::Text(t) if t.is_empty()) { + fields.insert(empty_marker(&value_key(var)), json!(true)); + } + } } "dataset.select" => { fields.insert("name".into(), json!("")); @@ -142,6 +149,12 @@ fn value_key(name: &str) -> String { format!("value:{name}") } +/// The key that marks the field `key` as holding the edited row's empty text value. Editing the +/// field removes the mark. +fn empty_marker(key: &str) -> String { + format!("__empty:{key}") +} + fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { @@ -227,11 +240,23 @@ fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> egui::Respo let mut s = d.str(key); let r = ui.add(egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(width)); if r.changed() { - d.fields.insert(key.into(), json!(s)); + set_text(d, key, s); } r } +/// Write a typed `s` into `key`. +fn set_text(d: &mut Dialog, key: &str, s: String) { + note_set(d, key); + d.fields.insert(key.into(), json!(s)); +} + +/// Records that `field` was typed in or set by `ui.dialog.set`: it no longer holds the row's +/// empty text value, so emptying it unsets the variable. +pub(super) fn note_set(d: &mut Dialog, field: &str) { + d.fields.remove(&empty_marker(field)); +} + /// A checkbox for `key`, written back as it is toggled, which screen readers call `name`. fn checkbox(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str) { let b = d.bool(key); @@ -256,7 +281,8 @@ fn dropdown(ui: &mut egui::Ui, d: &mut Dialog, key: &str, options: &[&str], widt } /// The `values` object the dialog's fields mean: a text, image or graph variable whose field is -/// empty carries no value in this row, and a visibility checkbox is always a value — its false, +/// empty carries no value in this row, except a text variable whose value in the edited row was +/// already the empty text, which keeps it. A visibility checkbox is always a value — its false, /// too. fn values_of(d: &Dialog, vars: &Variables) -> Value { let mut out = serde_json::Map::new(); @@ -265,7 +291,7 @@ fn values_of(d: &Dialog, vars: &Variables) -> Value { match v.kind { VariableKind::Text | VariableKind::Image | VariableKind::Graph => { let text = d.str(&key); - if !text.is_empty() { + if !text.is_empty() || (v.kind == VariableKind::Text && d.bool(&empty_marker(&key))) { out.insert(v.name.clone(), json!(text)); } } @@ -282,7 +308,7 @@ fn text_area(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) { let mut s = d.str(key); let r = ui.add(egui::TextEdit::multiline(&mut s).desired_width(width).desired_rows(4)); if r.changed() { - d.fields.insert(key.into(), json!(s)); + set_text(d, key, s); } } @@ -371,6 +397,51 @@ mod tests { assert_eq!(sets["datasets"][0]["values"]["Badge"], json!({"visible": true})); } + #[test] + fn an_untouched_edit_keeps_an_empty_text_value() { + let mut app = app_with_variables(); + let id = app.run("text.create", json!({"x": 10, "y": 10, "text": "x"})).unwrap()["id"].as_u64().unwrap(); + app.run("variable.bind", json!({"variable": "Title", "ids": [id]})).unwrap(); + app.run("dataset.new", json!({"name": "Row 1", "values": {"Title": "", "Badge": true}})).unwrap(); + app.run("dataset.new", json!({"name": "Row 2", "values": {"Title": "Bob", "Badge": true}})).unwrap(); + app.run("dataset.select", json!({"name": "Row 1"})).unwrap(); + open(&mut app, "dataset.set", None); + assert_eq!(app.ui.dialog.as_ref().unwrap().str(&value_key("Title")), ""); + crate::dialogs::confirm(&mut app).unwrap(); + let sets = app.run("dataset.list", json!({})).unwrap(); + assert_eq!(sets["datasets"][0]["values"]["Title"], json!({"text": ""}), "OK with nothing changed keeps the empty text"); + app.run("dataset.select", json!({"name": "Row 2"})).unwrap(); + app.run("dataset.select", json!({"name": "Row 1"})).unwrap(); + assert_eq!(app.run("text.getRange", json!({"id": id})).unwrap()["text"], "", "the row still empties the text"); + // A field the user empties on a row that had a value still leaves the variable unset. + app.run("dataset.select", json!({"name": "Row 2"})).unwrap(); + open(&mut app, "dataset.set", None); + app.ui.dialog.as_mut().unwrap().fields.insert(value_key("Title"), json!("")); + crate::dialogs::confirm(&mut app).unwrap(); + let sets = app.run("dataset.list", json!({})).unwrap(); + assert_eq!(sets["datasets"][1]["values"]["Title"], json!(null)); + // Typing into the empty text value's field and emptying it again unsets it too, and so + // does the same edit through `ui.dialog.set`. + for typed in [true, false] { + app.run("dataset.set", json!({"name": "Row 1", "values": {"Title": "", "Badge": true}})).unwrap(); + app.run("dataset.select", json!({"name": "Row 1"})).unwrap(); + open(&mut app, "dataset.set", None); + { + let d = app.ui.dialog.as_mut().unwrap(); + for s in ["a", ""] { + if typed { + set_text(d, &value_key("Title"), s.into()); + } else { + crate::dialogs::set_field(d, value_key("Title"), json!(s)); + } + } + } + crate::dialogs::confirm(&mut app).unwrap(); + let sets = app.run("dataset.list", json!({})).unwrap(); + assert_eq!(sets["datasets"][0]["values"]["Title"], json!(null), "typed: {typed}"); + } + } + #[test] fn selecting_a_dataset_from_the_dialog_applies_it() { let mut app = app_with_variables(); From 43b2733fc96b865416b0fd25fb05495b4a4dc66e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 12:17:39 -0800 Subject: [PATCH 2/2] Tests: move the empty text value test next to the new data set test --- crates/ui-egui/src/dialogs/variables.rs | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/ui-egui/src/dialogs/variables.rs b/crates/ui-egui/src/dialogs/variables.rs index 58cb18c86..7fd8639ab 100644 --- a/crates/ui-egui/src/dialogs/variables.rs +++ b/crates/ui-egui/src/dialogs/variables.rs @@ -378,25 +378,6 @@ mod tests { assert_eq!(sets["datasets"][0]["values"]["Badge"], json!({"visible": false})); } - #[test] - fn editing_a_dataset_opens_with_the_rows_own_values() { - let mut app = app_with_variables(); - app.run("dataset.new", json!({"name": "Row 1", "values": {"Title": "Hello", "Badge": false}})).unwrap(); - app.run("dataset.set", json!({"name": "Row 1", "values": {"Badge": true}})).unwrap(); - open(&mut app, "dataset.set", None); - { - let d = app.ui.dialog.as_ref().unwrap(); - assert_eq!(d.str("name"), "Row 1"); - assert_eq!(d.str(&value_key("Title")), "", "the row carries no value for it"); - assert!(d.bool(&value_key("Badge")), "the row's own value"); - } - app.ui.dialog.as_mut().unwrap().fields.insert(value_key("Title"), json!("Changed")); - crate::dialogs::confirm(&mut app).unwrap(); - let sets = app.run("dataset.list", json!({})).unwrap(); - assert_eq!(sets["datasets"][0]["values"]["Title"], json!({"text": "Changed"})); - assert_eq!(sets["datasets"][0]["values"]["Badge"], json!({"visible": true})); - } - #[test] fn an_untouched_edit_keeps_an_empty_text_value() { let mut app = app_with_variables(); @@ -442,6 +423,25 @@ mod tests { } } + #[test] + fn editing_a_dataset_opens_with_the_rows_own_values() { + let mut app = app_with_variables(); + app.run("dataset.new", json!({"name": "Row 1", "values": {"Title": "Hello", "Badge": false}})).unwrap(); + app.run("dataset.set", json!({"name": "Row 1", "values": {"Badge": true}})).unwrap(); + open(&mut app, "dataset.set", None); + { + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!(d.str("name"), "Row 1"); + assert_eq!(d.str(&value_key("Title")), "", "the row carries no value for it"); + assert!(d.bool(&value_key("Badge")), "the row's own value"); + } + app.ui.dialog.as_mut().unwrap().fields.insert(value_key("Title"), json!("Changed")); + crate::dialogs::confirm(&mut app).unwrap(); + let sets = app.run("dataset.list", json!({})).unwrap(); + assert_eq!(sets["datasets"][0]["values"]["Title"], json!({"text": "Changed"})); + assert_eq!(sets["datasets"][0]["values"]["Badge"], json!({"visible": true})); + } + #[test] fn selecting_a_dataset_from_the_dialog_applies_it() { let mut app = app_with_variables();