Variables: Variable Options changes a variable's kind as well as its name

Variable Options in the Variables panel only renamed a variable, and no
command changed a variable's kind. variable.rename now takes
{name, newName?, kind?} and renames the variable, changes its kind, or
both, in one undo step, with none when nothing changes. A null or empty
value, an unknown kind, a taken name and locked variables are refused.

A kind change unbinds the objects that can't hold the new kind (text
keeps type with no other text variable, image keeps image objects,
graph keeps graph roots, visibility keeps every object) and drops the
variable's values in every data set. The dialog shows the variable's
name and kind and sends both.
This commit is contained in:
dotCooCoo
2026-11-14 22:47:04 -08:00
parent 0c1ac3bdc5
commit 1f841dbeec
6 changed files with 321 additions and 39 deletions
+70 -17
View File
@@ -245,39 +245,92 @@ fn define(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"name": name, "kind": kind.label()}))
}
/// Whether the object `id`, bound to the variable `name`, can stay bound to it once the variable
/// is of `kind`: a text variable drives type that holds no other text variable, an image variable
/// drives an image object, a graph variable drives a graph root, and a visibility variable drives
/// any object. Locks and hidden layers don't count, as they don't unbind anything else either.
fn keeps_binding(doc: &Document, id: NodeId, name: &str, kind: VariableKind) -> bool {
let Some(n) = doc.node(id) else { return false };
match kind {
VariableKind::Text => {
matches!(n.kind, NodeKind::Text(_))
&& !doc.variables.bindings.get(&id).is_some_and(|bound| {
doc.variables.variables.iter().any(|v| v.kind == VariableKind::Text && v.name != name && bound.contains(&v.name))
})
}
VariableKind::Visibility => true,
VariableKind::Image => matches!(n.kind, NodeKind::Image(_)),
VariableKind::Graph => n.extras().graph.is_some(),
VariableKind::Unknown => false,
}
}
/// Variable Options: rename a variable (`newName`), change its kind (`kind`), or both, in one undo
/// step, carrying its bindings and its values in every data set to the new name. A kind change
/// unbinds the objects that can't hold the new kind ([`keeps_binding`]) and drops the variable's
/// value in every data set. A call that changes nothing adds no undo step.
fn rename(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "variable.rename";
let from = var_name(p, C)?;
let to = named(p.get("newName").and_then(Value::as_str), "newName", C)?;
let st = s.doc()?;
if st.doc.variables.variable(&from).is_none() {
return Err(bad(C, format!("no variable named `{from}`")));
// An omitted key is no change; a null, like any other value that isn't a name or a kind, is refused.
let to = match p.get("newName") {
None => None,
Some(v) => Some(named(v.as_str(), "newName", C)?),
};
let kind = match p.get("kind") {
None => None,
Some(v) => Some(v.as_str().and_then(VariableKind::parse).ok_or_else(|| bad(C, "`kind` must be `text`, `visibility`, `image` or `graph`"))?),
};
if to.is_none() && kind.is_none() {
return Err(bad(C, "missing `newName` or `kind`"));
}
if st.doc.variables.variable(&to).is_some() {
let doc = &s.doc()?.doc;
let Some(old) = doc.variables.variable(&from) else {
return Err(bad(C, format!("no variable named `{from}`")));
};
let to = to.unwrap_or_else(|| from.clone());
if to != from && doc.variables.variable(&to).is_some() {
return Err(bad(C, format!("variable `{to}` already exists")));
}
s.edit("Rename Variable", |d, _| {
let v = d.variables.variables.iter_mut().find(|v| v.name == from).ok_or_else(|| bad(C, format!("no variable named `{from}`")))?;
let kind = kind.unwrap_or(old.kind);
let retyped = kind != old.kind;
let unbound: Vec<NodeId> =
if retyped { doc.variables.objects_of(&from).into_iter().filter(|id| !keeps_binding(doc, *id, &from, kind)).collect() } else { Vec::new() };
let label = if retyped { "Variable Options" } else { "Rename Variable" };
let dropped = edit_variables(s, label, |vars| {
let v = vars.variables.iter_mut().find(|v| v.name == from).ok_or_else(|| bad(C, format!("no variable named `{from}`")))?;
v.name = to.clone();
for bound in d.variables.bindings.values_mut() {
v.kind = kind;
for id in &unbound {
vars.unbind(*id, &from);
}
for bound in vars.bindings.values_mut() {
if bound.remove(&from) {
bound.insert(to.clone());
}
}
for ds in &mut d.variables.datasets {
if let Some(value) = ds.values.remove(&from) {
ds.values.insert(to.clone(), value);
let mut dropped = 0;
for ds in &mut vars.datasets {
match ds.values.remove(&from) {
// A rename alone carries every value, one that doesn't fit the kind included; a kind
// change drops them all, as none was given for the new kind.
Some(value) if !retyped => {
ds.values.insert(to.clone(), value);
}
Some(_) => dropped += 1,
None => {}
}
}
Ok(())
Ok(dropped)
})?;
// The highlight names a variable, so it follows the rename rather than going stale.
// The highlight names a variable, so it follows the rename rather than going stale. A call
// that leaves the name as it was leaves the revision alone too.
let st = s.doc_mut()?;
if st.variables_highlight.as_deref() == Some(from.as_str()) {
if to != from && st.variables_highlight.as_deref() == Some(from.as_str()) {
st.variables_highlight = Some(to.clone());
st.revision += 1;
}
st.revision += 1;
Ok(json!({"from": from, "name": to}))
Ok(json!({"from": from, "name": to, "kind": kind.label(), "unbound": unbound.iter().map(|i| i.0).collect::<Vec<_>>(), "droppedValues": dropped}))
}
/// Make Text / Visibility / Image / Graph Dynamic: define a variable of that kind and bind the
@@ -722,7 +775,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Rename Variable",
["Window", "Variables"],
None,
"{name, newName} rename a variable, with its bindings and its values in every dataset; refused while the variables are locked",
"{name, newName?, kind?: text|visibility|image|graph} Variable Options: rename a variable, with its bindings and its values in every dataset, change its kind, or both, in one undo step (none when the name and the kind are the ones it has). A kind change unbinds every object that can't hold the new kind (a text variable keeps type that has no other text variable, an image variable keeps image objects, a graph variable keeps graph roots, a visibility variable keeps every object) and drops the variable's values in every dataset; locked and hidden objects keep their binding when they can hold it. Needs `newName` or `kind`, and a null or empty one is refused; refused while the variables are locked → {from, name, kind, unbound: [id…], droppedValues}",
variables_unlocked,
rename
),
+104
View File
@@ -1063,6 +1063,110 @@ fn renaming_a_missing_name_onto_a_taken_one_reports_the_missing_name() {
assert!(e.contains("no dataset named `Nope`"), "{e}");
}
/// Variable Options changes a variable's kind: the objects that can't hold the new kind are
/// unbound, the ones that can (locked ones too) stay bound, every value of the old kind goes, and
/// the change is one undo step.
#[test]
fn variable_options_changes_the_kind_and_unbinds_what_cant_hold_it() {
let mut s = session();
let t = text(&mut s, "Alice");
let r = rect(&mut s);
let other = text(&mut s, "Held");
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [t.0, r.0, other.0]})).unwrap();
s.execute("variable.bind", &json!({"variable": "Title", "ids": [other.0]})).unwrap();
s.execute("dataset.new", &json!({"name": "A", "values": {"Show": false, "Title": "Bob"}})).unwrap();
s.execute("dataset.new", &json!({"name": "B", "values": {"Show": true}})).unwrap();
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
s.execute("object.lock", &json!({})).unwrap();
let steps = |s: &Session| s.doc().unwrap().history.undo.len();
let before = steps(&s);
let v = s.execute("variable.rename", &json!({"name": "Show", "kind": "text"})).unwrap();
// The rectangle isn't type, and the other type object already has the text variable Title.
assert_eq!(v["unbound"], json!([r.0, other.0]));
assert_eq!((v["name"].clone(), v["kind"].clone(), v["droppedValues"].clone()), (json!("Show"), json!("text"), json!(2)));
assert_eq!(steps(&s), before + 1, "one undo step");
let vars = &s.doc().unwrap().doc.variables;
assert_eq!(vars.objects_of("Show"), [t], "the locked type object stays bound");
assert_eq!(vars.objects_of("Title"), [other], "the other variable keeps its binding");
let sets = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!((sets["datasets"][0]["values"].clone(), sets["datasets"][1]["values"].clone()), (json!({"Title": {"text": "Bob"}}), json!({})));
s.execute("edit.undo", &json!({})).unwrap();
let vars = &s.doc().unwrap().doc.variables;
assert_eq!(vars.variable("Show").map(|v| v.kind), Some(vectorcraft_doc::VariableKind::Visibility));
assert_eq!(vars.objects_of("Show"), [t, r, other]);
assert_eq!(vars.dataset("A").and_then(|d| d.values.get("Show")), Some(&vectorcraft_doc::DataValue::Visible(false)));
// A new name and a new kind together are one undo step, and every visibility binding stays.
s.execute("variable.rename", &json!({"name": "Title", "newName": "Heading", "kind": "visibility"})).unwrap();
assert_eq!(steps(&s), before + 1, "one step after the undo");
let vars = &s.doc().unwrap().doc.variables;
assert_eq!(
(vars.variable("Heading").map(|v| v.kind), vars.objects_of("Heading")),
(Some(vectorcraft_doc::VariableKind::Visibility), vec![other])
);
// An image variable keeps only image objects.
let v = s.execute("variable.rename", &json!({"name": "Show", "kind": "image"})).unwrap();
assert_eq!(v["unbound"], json!([t.0, r.0, other.0]));
}
/// Variable Options with the variable's own name and kind changes nothing and adds no undo step;
/// a bad kind, a taken name, no change asked for and locked variables are refused.
#[test]
fn variable_options_refuses_bad_params_and_skips_a_change_that_changes_nothing() {
let mut s = session();
let r = rect(&mut s);
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap();
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
let steps = |s: &Session| s.doc().unwrap().history.undo.len();
let before = steps(&s);
for p in [
json!({"name": "Show", "newName": "Show"}),
json!({"name": "Show", "kind": "visibility"}),
json!({"name": "Show", "newName": "Show", "kind": "visible"}),
] {
let v = s.execute("variable.rename", &p).unwrap();
assert_eq!((v["unbound"].clone(), v["droppedValues"].clone()), (json!([]), json!(0)), "{p}");
assert_eq!(steps(&s), before, "{p}: no undo step");
}
for (p, want) in [
(json!({"name": "Show", "kind": "unknown"}), "`kind` must be"),
(json!({"name": "Show", "kind": "number"}), "`kind` must be"),
(json!({"name": "Show", "kind": 1}), "`kind` must be"),
(json!({"name": "Show", "newName": "New", "kind": null}), "`kind` must be"),
(json!({"name": "Show", "newName": null, "kind": "text"}), "missing `newName`"),
(json!({"name": "Show", "newName": " ", "kind": "text"}), "missing `newName`"),
(json!({"name": "Show"}), "missing `newName` or `kind`"),
(json!({"name": "Show", "newName": "Other", "kind": "text"}), "already exists"),
(json!({"name": "Ghost", "kind": "text"}), "no variable named `Ghost`"),
] {
let e = s.execute("variable.rename", &p).unwrap_err().to_string();
assert!(e.contains(want), "{p}: {e}");
}
// A rename alone carries a value that doesn't fit the kind, as a file may hold one.
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc)
.variables
.datasets
.push(vectorcraft_doc::DataSet { name: "Odd".into(), values: [("Show".to_string(), vectorcraft_doc::DataValue::Text("x".into()))].into() });
s.execute("variable.rename", &json!({"name": "Show", "newName": "Shown"})).unwrap();
let odd = s.doc().unwrap().doc.variables.dataset("Odd").and_then(|d| d.values.get("Shown")).cloned();
assert_eq!(odd, Some(vectorcraft_doc::DataValue::Text("x".into())));
s.execute("edit.undo", &json!({})).unwrap();
// A kind change drops it, though it fits the new kind: nobody gave it for that kind.
let v = s.execute("variable.rename", &json!({"name": "Show", "kind": "text"})).unwrap();
assert_eq!(v["droppedValues"], 1);
assert!(s.doc().unwrap().doc.variables.dataset("Odd").is_some_and(|d| d.values.is_empty()));
s.execute("edit.undo", &json!({})).unwrap();
s.execute("variable.lock", &json!({"locked": true})).unwrap();
assert!(s.execute("variable.rename", &json!({"name": "Show", "kind": "text"})).is_err(), "refused while the variables are locked");
let vars = &s.doc().unwrap().doc.variables;
assert_eq!((vars.variable("Show").map(|v| v.kind), vars.objects_of("Show")), (Some(vectorcraft_doc::VariableKind::Visibility), vec![r]));
}
/// `dataset.set` is how a value changes after the row exists: replacing it must not need the
/// dataset to be deleted and rebuilt (which loses its place in the list and the active one).
#[test]
+139 -20
View File
@@ -1,5 +1,7 @@
//! The Variables dialogs (`variable.define`, `variable.bind`, `dataset.new`, `dataset.set`
//! and `dataset.select`), opened from Window › Variables or the command palette.
//! The Variables dialogs (`variable.define`, `variable.rename` as Variable Options, `variable.bind`,
//! `dataset.new`, `dataset.set`, `dataset.rename` and `dataset.select`), opened from Window ›
//! Variables or the command palette. Variable Options shows the variable's name and kind, and OK
//! changes either or both.
//!
//! One dialog covers all of them: what it shows is what the command takes. A variable is a
//! name and a kind; a dataset is a name and one field per variable — a text field for a
@@ -24,6 +26,10 @@ pub const KIND: &str = "variables";
/// renames.
const EDITED: &str = "__dataset";
/// The field of Variable Options that names the variable its Name and Kind fields belong to.
/// When the Variable field names another one, the next frame fills them from that one.
const FILLED: &str = "__filled";
/// The kinds a variable can have, as `variable.define` names them.
const KINDS: [&str; 4] = ["text", "visibility", "image", "graph"];
@@ -50,7 +56,7 @@ fn opened(id: &str) -> Option<&'static str> {
}
/// The row `id` renames, which the dialog offers as a dropdown when the document has more than
/// one. Variable Options renames the one the panel has highlighted, so it starts there.
/// one. Variable Options edits the one the panel has highlighted, so it starts there.
fn rename_of(id: &str) -> Option<&'static str> {
Some(match id {
"variable.rename" => "variable",
@@ -116,8 +122,14 @@ pub fn open(app: &mut VectorcraftApp, id: &str, preselect: Option<&str>) -> bool
app.status(if key == "variable" { tl!("no variable to rename").to_string() } else { tl!("no data set to rename").to_string() });
return true;
}
fields.insert(key.into(), json!(preselect.filter(|p| names.contains(p)).or(names.first().copied()).unwrap_or_default()));
fields.insert("newName".into(), json!(""));
let row = preselect.filter(|p| names.contains(p)).or(names.first().copied()).unwrap_or_default();
fields.insert(key.into(), json!(row));
if key == "variable" {
// Variable Options starts on the variable's own name and kind.
fill_variable_options(&vars, row, &mut fields);
} else {
fields.insert("newName".into(), json!(""));
}
}
}
fields.insert("__command".into(), json!(id));
@@ -126,6 +138,16 @@ pub fn open(app: &mut VectorcraftApp, id: &str, preselect: Option<&str>) -> bool
true
}
/// Variable Options' Name and Kind fields for the variable `name`: its name, and its kind as
/// `variable.rename` takes it (empty for a variable of unknown kind, which OK leaves as it is).
/// [`FILLED`] records which variable they were filled for.
fn fill_variable_options(vars: &Variables, name: &str, fields: &mut serde_json::Map<String, Value>) {
let kind = vars.variable(name).map(|v| v.kind).filter(|k| *k != VariableKind::Unknown).map(VariableKind::label).unwrap_or_default();
fields.insert("newName".into(), json!(name));
fields.insert("kind".into(), json!(kind));
fields.insert(FILLED.into(), json!(name));
}
/// Whether `id` opens one of these dialogs.
pub fn opens(id: &str) -> bool {
opened(id).is_some()
@@ -181,13 +203,25 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
text(ui, d, "name", 220.0);
ui.end_row();
label(ui, tl!("Kind"));
// The kinds by their translated names; the field keeps the command's own word.
let labels = kind_labels();
let shown = KINDS.iter().position(|k| *k == d.str("kind")).and_then(|i| labels.get(i)).copied().unwrap_or_default();
if let Some(kind) = widgets::dropdown_names(ui, ("variables-dd", "kind"), shown, &labels, 220.0).and_then(|i| KINDS.get(i)) {
d.fields.insert("kind".into(), json!(kind));
kind_dropdown(ui, d);
ui.end_row();
}
// Variable Options: which variable, and its name and kind; picking another variable
// starts its fields over from that one.
"variable.rename" => {
label(ui, tl!("Variable"));
dropdown(ui, d, "variable", &variable_names, 220.0);
let picked = d.str("variable");
if picked != d.str(FILLED) {
fill_variable_options(vars, &picked, &mut d.fields);
}
ui.end_row();
label(ui, tl!("Name"));
text(ui, d, "newName", 220.0);
ui.end_row();
label(ui, tl!("Kind"));
kind_dropdown(ui, d);
ui.end_row();
}
"variable.bind" => {
label(ui, tl!("Variable"));
@@ -199,15 +233,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
dropdown(ui, d, "name", &dataset_names, 220.0);
ui.end_row();
}
// A rename: which row, and its new name.
"variable.rename" | "dataset.rename" => {
let (key, label_text, names) = if d.str("__command") == "variable.rename" {
("variable", tl!("Variable").to_string(), &variable_names)
} else {
("name", tl!("Data Set").to_string(), &dataset_names)
};
label(ui, &label_text);
dropdown(ui, d, key, names, 220.0);
// Rename Data Set: which row, and its new name.
"dataset.rename" => {
label(ui, tl!("Data Set"));
dropdown(ui, d, "name", &dataset_names, 220.0);
ui.end_row();
label(ui, tl!("New Name"));
text(ui, d, "newName", 220.0);
@@ -245,6 +274,16 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
false
}
/// The Kind dropdown: the kinds by their translated names, while the `kind` field keeps the
/// command's own word. An empty field (a variable of unknown kind) shows no kind.
fn kind_dropdown(ui: &mut egui::Ui, d: &mut Dialog) {
let labels = kind_labels();
let shown = KINDS.iter().position(|k| *k == d.str("kind")).and_then(|i| labels.get(i)).copied().unwrap_or_default();
if let Some(kind) = widgets::dropdown_names(ui, ("variables-dd", "kind"), shown, &labels, 220.0).and_then(|i| KINDS.get(i)) {
d.fields.insert("kind".into(), json!(kind));
}
}
/// A text field for `key`, written back as it is typed.
fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> egui::Response {
let mut s = d.str(key);
@@ -263,8 +302,26 @@ fn set_text(d: &mut Dialog, key: &str, s: String) {
/// 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.
///
/// In Variable Options, setting the Variable field clears Name and Kind, which OK reads as the
/// variable's own name and kind, until a frame fills them from it; setting Name or Kind ties them
/// to the variable already named, so no frame fills them over.
pub(super) fn note_set(d: &mut Dialog, field: &str) {
d.fields.remove(&empty_marker(field));
if d.str("__command") == "variable.rename" {
match field {
// Null rather than removed, so `ui.dialog.set` can still set them.
"variable" => {
d.fields.insert("newName".into(), Value::Null);
d.fields.insert("kind".into(), Value::Null);
}
"newName" | "kind" => {
let row = d.str("variable");
d.fields.insert(FILLED.into(), json!(row));
}
_ => {}
}
}
}
/// A checkbox for `key`, written back as it is toggled, which screen readers call `name`.
@@ -329,7 +386,18 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let p = match cmd.as_str() {
"variable.define" => json!({"name": name, "kind": d.str("kind")}),
"variable.bind" => json!({"variable": d.str("variable")}),
"variable.rename" => json!({"name": d.str("variable"), "newName": d.str("newName")}),
// A Name the Variable field just cleared keeps the variable's name (an emptied one is sent,
// and refused), and a cleared or empty Kind (a variable of unknown kind) keeps its kind.
"variable.rename" => {
let variable = d.str("variable");
let new_name = if matches!(d.fields.get("newName"), Some(Value::Null) | None) { variable.clone() } else { d.str("newName") };
// Any other kind value goes to the command, which refuses one it doesn't know.
match d.fields.get("kind") {
None | Some(Value::Null) => json!({"name": variable, "newName": new_name}),
Some(Value::String(k)) if k.is_empty() => json!({"name": variable, "newName": new_name}),
Some(k) => json!({"name": variable, "newName": new_name, "kind": k}),
}
}
"dataset.rename" => json!({"name": name, "newName": d.str("newName")}),
"dataset.new" | "dataset.set" => {
let values = app.session.active().map(|st| values_of(d, &st.doc.variables)).unwrap_or_else(|| json!({}));
@@ -550,4 +618,55 @@ mod tests {
open(&mut app, "dataset.rename", Some("Row 1"));
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "Row 1");
}
#[test]
fn variable_options_shows_the_name_and_kind_and_changes_either() {
let mut app = app_with_variables();
let kind_of = |app: &VectorcraftApp, name: &str| app.session.active().and_then(|st| st.doc.variables.variable(name).map(|v| v.kind));
open(&mut app, "variable.rename", Some("Badge"));
{
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.str("newName"), d.str("kind")), ("Badge".to_string(), "visibility".to_string()));
}
app.ui.dialog.as_mut().unwrap().fields.insert("kind".into(), json!("text"));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(kind_of(&app, "Badge"), Some(VariableKind::Text));
// OK with nothing changed adds no undo step, and an emptied Name is refused.
let undo = app.session.active().unwrap().history.undo.len();
open(&mut app, "variable.rename", Some("Title"));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(app.session.active().unwrap().history.undo.len(), undo);
open(&mut app, "variable.rename", Some("Title"));
crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), "newName".into(), json!(""));
assert!(crate::dialogs::confirm(&mut app).is_err());
assert!(kind_of(&app, "Title").is_some(), "Title keeps its name");
// Setting the Variable field keeps that variable's own name and kind until Name or Kind is
// set, and a frame fills them from it.
open(&mut app, "variable.rename", Some("Title"));
crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), "variable".into(), json!("Badge"));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!((kind_of(&app, "Title"), kind_of(&app, "Badge")), (Some(VariableKind::Text), Some(VariableKind::Text)));
open(&mut app, "variable.rename", Some("Title"));
{
let d = app.ui.dialog.as_mut().unwrap();
crate::dialogs::set_field(d, "variable".into(), json!("Badge"));
assert!(crate::dialogs::accepts_field(d, "newName") && crate::dialogs::accepts_field(d, "kind"), "Name and Kind stay settable");
crate::dialogs::set_field(d, "newName".into(), json!("Flag"));
crate::dialogs::set_field(d, "kind".into(), json!("visibility"));
}
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!((kind_of(&app, "Flag"), kind_of(&app, "Title")), (Some(VariableKind::Visibility), Some(VariableKind::Text)));
// A frame after the Variable field changes fills Name and Kind from that variable.
open(&mut app, "variable.rename", Some("Title"));
crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), "variable".into(), json!("Flag"));
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::dialogs::show(&mut app, ui.ctx()));
out.textures_delta.clear();
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.str("newName"), d.str("kind")), ("Flag".to_string(), "visibility".to_string()));
}
}
+1 -1
View File
@@ -272,7 +272,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
if widgets::menu_item(ui, tl!("New Variable…"), !locked, false) {
crate::menus::invoke(app, "variable.define", json!({}));
}
// Variable Options renames the highlighted variable, else the first.
// Variable Options edits the name and kind of the highlighted variable, else the first.
if widgets::menu_item(ui, tl!("Variable Options…"), !vars.is_empty() && !locked, false) {
let name = highlighted.clone().or_else(|| vars.first().cloned()).unwrap_or_default();
crate::dialogs::variables::open(app, "variable.rename", Some(&name));
+6 -1
View File
@@ -3331,7 +3331,12 @@ already walks Image Trace. Short agent entry points:
`variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.makeGraphDynamic`
(`variable.bind` and `variable.makeTextDynamic` refuse type that already has another text variable and name it),
`variable.unbind`, `variable.rename`, `variable.delete`, `variable.highlight` (image values are file paths, graph
values CSV as `graph.setData`). `variable.lock {locked?}` (Lock Variables) locks or unlocks the variables;
values CSV as `graph.setData`). `variable.rename {name, newName?, kind?}` is Variable Options: it renames a
variable, changes its kind, or both, in one undo step, and adds none when the name and kind are the ones it has.
A kind change unbinds each object that can't hold the new kind (a text variable keeps type with no other text
variable, an image variable keeps image objects, a graph variable keeps graph roots, a visibility variable keeps
every object; locks and hidden layers don't unbind anything) and drops the variable's values in every data set.
That rule is VectorCraft's own. `variable.lock {locked?}` (Lock Variables) locks or unlocks the variables;
without `locked` it toggles, and the lock is saved with the document. While the variables are locked,
`variable.define`, `variable.delete`, `variable.rename`, the four `variable.make…Dynamic` commands and a
`variable.loadLibrary` that would delete or define a variable (below) fail with "variables are locked";
+1
View File
@@ -824,6 +824,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Variables: Import Data Sets… (`dataset.import`) defines the variables a file's header names that the document lacks, unbound. `#Shown` defines a visibility variable, `@Photo` defines an image variable, and a header without a prefix defines a text variable. A header that is a variable's exact name keeps naming it, and any other `%` header (graph data files) is refused.
- Variables: Load Variable Library… in the Variables panel menu (`variable.loadLibrary`) replaces the document's variables and data sets with a variable library's, read from its XML file, in one undo step: every data set is deleted, and every variable is deleted with its bindings and values, except a variable the library also declares with the same kind, which stays bound so the library's data sets change its objects (#4136); then the library's other variables are defined, unbound: text, visibility, image (linked file) and graph variables, and variables without a known `trait` as unknown. Its data sets take text values from each value's `p` elements, one paragraph each, visibility values from `true` or `false`, image values from a file's absolute path or `file:` URL, and graph values from a `data` element of series names and rows.
- Variables: Save Variable Library… in the Variables panel menu (`variable.saveLibrary`) writes the document's variables of every kind and its data sets as a variable library's XML file, which Load Variable Library reads back with the same variables, kinds, data set names and values.
- Variables: Variable Options… in the Variables panel menu, or a double click on a variable, shows the variable's name and kind, and OK changes either or both in one undo step (`variable.rename {name, newName?, kind?}`), as the [Work with variables](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/work-with-variables.html) page describes. VectorCraft's own rule for a kind change: objects that can't hold the new kind are unbound, and the variable's values in every data set are dropped.
- JSON-lines control channel with real egui pointer and keyboard injection.
- Variables: a type object holds one text variable. `variable.bind` and Make Text Dynamic refuse a second one and name the text variable the object has; a visibility variable can still share the object, and a document saved with two text variables on one object opens with both.
- Synthetic input validates complete gestures before queue mutation: at most 256 drag steps, 16 clicks and 1,024 pending events; up to 64 control requests per frame, with later requests retained for the next frame (#1709).