From 7644de9f810c6570e8d87d7bb54478fb0fc1730a Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Sat, 14 Nov 2026 02:36:36 +0600 Subject: [PATCH 1/3] Variables: renaming a group to a path with / nests it (#2353) Renaming a group in the Local variables window replaced every slash in the typed name with a hyphen, so "Palette/Orange" gave a flat Palette-Orange group and there was no way to move a group under a new parent. The typed path now replaces the group's last part, so a slash nests the group as in Figma: Orange/100 becomes Palette/Orange/100 and the sidebar shows Orange under Palette. Each part is trimmed and empty parts are dropped; a name that comes out empty or unchanged does nothing. The renames go deepest name first, so a group moved into its own new subgroup (Orange to Orange/Warm) never collides with a variable that is still to move. A rename onto a variable name that stays is refused by variable.update and the batch rolls back; it stays one undo step. --- crates/ui-egui/src/dialogs/variables_panel.rs | 82 ++++++++++++++++--- crates/ui-egui/src/variables_select_tests.rs | 79 ++++++++++++++++++ docs/components-parity.md | 3 +- 3 files changed, 152 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/dialogs/variables_panel.rs b/crates/ui-egui/src/dialogs/variables_panel.rs index 067d54ea9..21889fe29 100644 --- a/crates/ui-egui/src/dialogs/variables_panel.rs +++ b/crates/ui-egui/src/dialogs/variables_panel.rs @@ -594,23 +594,40 @@ fn group_rows(app: &mut FigApp, ui: &mut Ui, view: &View, coll: &Collection, st: } } -/// Rename the last part of group `g` of a collection (its variables `members`) to `new`: every -/// variable under it moves along (one undo step). -fn rename_group(app: &mut FigApp, members: &[&Variable], g: &str, new: &str, st: &mut Editor) { - let new = new.trim().replace('/', "-"); +/// What renaming group `g` (of the variables `members`) to `new` does: the group's new path and +/// each moved variable's (id, new name). `new` replaces the group's last part; a `/` in it nests +/// the group, as in Figma ("Palette/Orange" puts it in a group Palette, made if there is none). +/// Each part is trimmed and empty parts are dropped. The renames come deepest name first, so a +/// variable moving into the group's own new path ("Orange" to "Orange/Warm") never takes a name +/// another moved variable still holds. None when nothing changes. +fn group_renames(members: &[&Variable], g: &str, new: &str) -> Option<(String, Vec<(String, String)>)> { + let new = new.split('/').map(str::trim).filter(|p| !p.is_empty()).collect::>().join("/"); if new.is_empty() { - return; + return None; } let renamed = match g.rsplit_once('/') { Some((parent, _)) => format!("{parent}/{new}"), None => new, }; + if renamed == g { + return None; + } + let prefix = format!("{g}/"); + let mut moved: Vec<(&Variable, &str)> = members.iter().filter_map(|v| Some((*v, v.name.strip_prefix(&prefix)?))).collect(); + // A longer name can only be one a shorter one moves to: rename those out of the way first. + moved.sort_by_key(|(_, rest)| std::cmp::Reverse(rest.len())); + Some((renamed.clone(), moved.into_iter().map(|(v, rest)| (v.id.clone(), format!("{renamed}/{rest}"))).collect())) +} + +/// Rename group `g` of a collection (its variables `members`) to `new` (see [`group_renames`]): +/// every variable under it moves along, in one undo step. +fn rename_group(app: &mut FigApp, members: &[&Variable], g: &str, new: &str, st: &mut Editor) { + let Some((renamed, moves)) = group_renames(members, g, new) else { + return; + }; + let steps: Vec = + moves.iter().map(|(id, name)| json!({"command": "variable.update", "params": {"variable": id, "name": name}})).collect(); let prefix = format!("{g}/"); - let steps: Vec = members - .iter() - .filter(|v| v.name.starts_with(&prefix)) - .map(|v| json!({"command": "variable.update", "params": {"variable": v.id, "name": format!("{renamed}/{}", &v.name[prefix.len()..])}})) - .collect(); if app.run("command.batch", json!({"label": "Rename group", "commands": steps})).is_some() && st.group.as_deref().is_some_and(|s| s == g || s.starts_with(&prefix)) { @@ -1308,7 +1325,7 @@ fn bulk_details(app: &mut FigApp, ctx: &Context, vars: &Variables, st: &mut Edit #[cfg(test)] mod tests { - use super::{groups, tri}; + use super::{group_renames, groups, tri}; #[test] fn tri_is_checked_empty_or_mixed() { @@ -1330,4 +1347,47 @@ mod tests { assert_eq!(groups(names.into_iter()), ["color", "color/brand", "space"]); assert!(groups(["a", "b"].into_iter()).is_empty()); } + + /// The renames of group `g` among `names` (numbers in one collection) typed as `new`, by name. + fn renames(names: &[&str], g: &str, new: &str) -> Option<(String, Vec<(String, String)>)> { + let mut s = figcraft_engine::Session::new(); + s.execute("variable.createCollection", &serde_json::json!({"name": "Theme"})).unwrap(); + for n in names { + s.execute("variable.create", &serde_json::json!({"collection": "Theme", "name": n, "type": "number", "value": 1})).unwrap(); + } + let vars = &s.doc().unwrap().variables.variables; + let members: Vec<_> = vars.iter().collect(); + let (renamed, moves) = group_renames(&members, g, new)?; + let by_name = |id: &str| vars.iter().find(|v| v.id == id).unwrap().name.clone(); + Some((renamed, moves.into_iter().map(|(id, n)| (by_name(&id), n)).collect())) + } + + #[test] + fn a_slash_in_a_group_rename_nests_the_group_keeping_its_parent() { + let pair = |a: &str, b: &str| (a.to_string(), b.to_string()); + assert_eq!( + renames(&["Orange/100", "Orange/200", "Blue/100"], "Orange", "Palette/Orange"), + Some(("Palette/Orange".into(), vec![pair("Orange/100", "Palette/Orange/100"), pair("Orange/200", "Palette/Orange/200")])) + ); + assert_eq!( + renames(&["a/b/x"], "a/b", " / Warm // c / "), + Some(("a/Warm/c".into(), vec![pair("a/b/x", "a/Warm/c/x")])), + "parts are trimmed, empty parts dropped, and the parent stays" + ); + } + + #[test] + fn a_group_rename_to_nothing_or_to_itself_does_nothing() { + for new in ["", " ", "/", " / // ", "Orange", " Orange ", "/Orange/"] { + assert_eq!(renames(&["Orange/100"], "Orange", new), None, "{new:?}"); + } + assert_eq!(renames(&["a/b/x"], "a/b", "b/"), None); + } + + #[test] + fn a_group_moved_into_its_own_subgroup_renames_the_deeper_names_first() { + let (_, moves) = renames(&["Orange/100", "Orange/Warm/100"], "Orange", "Orange/Warm").unwrap(); + let order: Vec<&str> = moves.iter().map(|(old, _)| old.as_str()).collect(); + assert_eq!(order, ["Orange/Warm/100", "Orange/100"]); + } } diff --git a/crates/ui-egui/src/variables_select_tests.rs b/crates/ui-egui/src/variables_select_tests.rs index 3b32dc49d..c2b699eb8 100644 --- a/crates/ui-egui/src/variables_select_tests.rs +++ b/crates/ui-egui/src/variables_select_tests.rs @@ -332,6 +332,85 @@ fn a_second_duplicate_group_takes_the_next_free_name() { assert_eq!(n.len(), 6); } +/// A collection with the variables `names` (numbers). +fn named_window(names: &[&str]) -> Ui { + let mut s = Session::new(); + s.execute("variable.createCollection", &json!({"name": "Theme"})).unwrap(); + for n in names { + s.execute("variable.create", &json!({"collection": "Theme", "name": n, "type": "number", "value": 1})).unwrap(); + } + let mut u = Ui::new(s); + u.app.ui.variables = Some(Default::default()); + for _ in 0..10 { + u.frame(vec![]); + } + u +} + +/// Right-click the group row `name`, choose Rename group, type `to` and press Enter. +fn rename_group(u: &mut Ui, name: &str, to: &str) { + let p = row(u, name); + click(u, p, PointerButton::Secondary, Modifiers::NONE); + let menu = flat(&u.frame_output(vec![]).shapes); + click(u, text_center(&menu, "Rename group"), PointerButton::Primary, Modifiers::NONE); + u.frame(vec![Event::Text(to.into())]); + for pressed in [true, false] { + u.frame(vec![Event::Key { key: Key::Enter, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }]); + } + for _ in 0..5 { + u.frame(vec![]); + } +} + +fn sorted_names(u: &Ui) -> Vec { + let mut n = names(u); + n.sort(); + n +} + +#[test] +fn renaming_a_group_with_a_slash_nests_it_in_a_new_parent_group_in_one_undo_step() { + let mut u = named_window(&["Orange/100", "Orange/200", "Blue/100"]); + let before = u.app.session.undo_labels(100).len(); + rename_group(&mut u, "Orange", "Palette/Orange"); + assert!(u.app.ui.last_error.is_none(), "{:?}", u.app.ui.last_error); + assert_eq!(sorted_names(&u), ["Blue/100", "Palette/Orange/100", "Palette/Orange/200"]); + assert_eq!(u.app.session.undo_labels(100).len(), before + 1, "one undo step"); + assert_eq!(u.app.session.undo_labels(1), ["Rename group"]); + // The sidebar lists Palette with Orange indented under it. + let shapes = flat(&u.frame_output(vec![]).shapes); + let (parent, child) = (text_center(&shapes, "Palette"), text_center(&shapes, "Orange")); + assert!(child.y > parent.y && child.x > parent.x, "Orange is indented under Palette: {parent:?} {child:?}"); + assert!(!has_text(&shapes, "Palette-Orange")); + u.app.session.undo().unwrap(); + assert_eq!(sorted_names(&u), ["Blue/100", "Orange/100", "Orange/200"]); +} + +#[test] +fn a_nested_groups_new_path_stays_in_its_parent_and_drops_empty_parts() { + let mut u = named_window(&["Palette/Orange/100", "Palette/Orange/200"]); + rename_group(&mut u, "Orange", " /Warm // Orange / "); + assert!(u.app.ui.last_error.is_none(), "{:?}", u.app.ui.last_error); + assert_eq!(sorted_names(&u), ["Palette/Warm/Orange/100", "Palette/Warm/Orange/200"]); +} + +#[test] +fn renaming_a_group_into_its_own_new_subgroup_moves_the_deeper_names_first() { + // Orange/100 takes Orange/Warm/100, which itself moves on to Orange/Warm/Warm/100. + let mut u = named_window(&["Orange/100", "Orange/Warm/100"]); + rename_group(&mut u, "Orange", "Orange/Warm"); + assert!(u.app.ui.last_error.is_none(), "{:?}", u.app.ui.last_error); + assert_eq!(sorted_names(&u), ["Orange/Warm/100", "Orange/Warm/Warm/100"]); +} + +#[test] +fn a_group_rename_onto_a_variable_name_that_stays_changes_nothing() { + let mut u = named_window(&["Orange/100", "Orange/200", "Palette/Orange/100"]); + rename_group(&mut u, "Orange", "Palette/Orange"); + assert!(u.app.ui.last_error.as_deref().is_some_and(|e| e.contains("Palette/Orange/100")), "{:?}", u.app.ui.last_error); + assert_eq!(sorted_names(&u), ["Orange/100", "Orange/200", "Palette/Orange/100"]); +} + fn open_edit_variables(u: &mut Ui) -> Vec { let menu = select_three_and_open_menu(u); assert!(has_text(&menu, "Edit variables"), "the bulk menu offers Edit variables"); diff --git a/docs/components-parity.md b/docs/components-parity.md index 3d23800b0..8b7219b81 100644 --- a/docs/components-parity.md +++ b/docs/components-parity.md @@ -1,6 +1,6 @@ # Components, variants, styles and variables parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-12 · **Change:** minor (a group's context menu in the Local variables window has Duplicate group, a copy named after it plus " 2" holding a copy of every variable, in one undo step (#2084); pages take a variable mode: with nothing selected the Design panel's Page section shows a Variable modes section, Auto or a mode of each collection that has more than one, as the help center says ("Apply variable mode" in the Page section of the right sidebar) (#2215); a text style binds a number variable to its weight (#2174); an instance lists only the properties its variant's layers use and long property names are cut to one line (#2022); with several layers selected the Design panel header's component button offers Create multiple components (#2088); the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); text styles carry paragraph spacing and indent, the list style and the OpenType features, as the Supported in text styles table of Create and apply text styles says (#1829); a layer's effect binding takes only a field its effect has; Go to main component on an instance of a library component opens the library file in a tab and selects the main there; glass effects take variables; Delete on a layer inside an instance hides it as an override instead of being refused, as the help center says: "Figma will only toggle the layer's visibility instead of removing it"; library analytics list how many layers and files use each style and variable; SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; a typed or stored variable, collection or mode id names only the object with that id (#1613); colour variables can be calculated from other colours per mode — `{alpha}` replaces a colour's alpha, `{mix}` blends two — and follow them (#1280); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (renaming a variable group to a path with `/` nests it under a parent group instead of turning the slash into a hyphen (#2353); a group's context menu in the Local variables window has Duplicate group, a copy named after it plus " 2" holding a copy of every variable, in one undo step (#2084); pages take a variable mode: with nothing selected the Design panel's Page section shows a Variable modes section, Auto or a mode of each collection that has more than one, as the help center says ("Apply variable mode" in the Page section of the right sidebar) (#2215); a text style binds a number variable to its weight (#2174); an instance lists only the properties its variant's layers use and long property names are cut to one line (#2022); with several layers selected the Design panel header's component button offers Create multiple components (#2088); the Local variables window selects several variables (click, Shift+click for a range, Cmd/Ctrl+click to toggle, Escape to clear) and delete, move to a group and scope act on the selected local variables in one undo step (#2002); text styles carry paragraph spacing and indent, the list style and the OpenType features, as the Supported in text styles table of Create and apply text styles says (#1829); a layer's effect binding takes only a field its effect has; Go to main component on an instance of a library component opens the library file in a tab and selects the main there; glass effects take variables; Delete on a layer inside an instance hides it as an override instead of being refused, as the help center says: "Figma will only toggle the layer's visibility instead of removing it"; library analytics list how many layers and files use each style and variable; SwiftUI and Compose code names a bound variable by its iOS or Android code syntax; library analytics count insertions and show insertions and detachments over the past 30 days, 90 days or year; a collection of this file can be extended (Extend collection on its row, `variable.extendCollection`), its extension following its edits at once, and extended collections extend again in the file; a typed or stored variable, collection or mode id names only the object with that id (#1613); colour variables can be calculated from other colours per mode — `{alpha}` replaces a colour's alpha, `{mix}` blends two — and follow them (#1280); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; opening a file contacts no team library server it names until you check it (#1097); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); components can't contain their own instances, directly or through others (#1084); the instance-swap menus have a search field (#1338); an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7) Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300 @@ -81,6 +81,7 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`. | Date | Change | Summary | |---|---|---| +| 2026-11-14 | patch | M3, #2353: renaming a group in the Local variables window to a path with `/` nests it, as in Figma: `Orange` renamed to `Palette/Orange` moves its variables to `Palette/Orange/100`… and the sidebar shows Orange indented under a new Palette group, where the slash used to become a hyphen (`Palette-Orange`). The typed path replaces the group's last part (a nested group stays in its parent), each part is trimmed and empty parts are dropped; a rename onto a variable name that stays is refused with nothing changed, and it is one undo step. Tests: `variables_select_tests::renaming_a_group_with_a_slash_nests_it_in_a_new_parent_group_in_one_undo_step` and the group rename tests beside it, `variables_panel::tests`. Grade unchanged. | | 2026-11-07 | minor | M3, #2084: a group's context menu in the Local variables window has Duplicate group (after Rename group, before Delete group). It adds a group named after the original plus " 2" (the next free number when taken: " 3"), whose variables are copies of the originals with the same leaf names, values in every mode, description, scopes, hidden-from-publishing and code syntax, each sent as `variable.duplicate` plus a rename in one `command.batch` labelled Duplicate group, so one undo step. Measured in the incumbent with one mode; an alias to a variable of the same group keeps pointing at the original. Ungroup is not built. Tests: `variables_select_tests` (headless UI). Grade unchanged | | 2026-11-07 | minor | M3, #2082: Edit variables in the context menu of a selection of several local variables opens the reference's compact editor with two tabs: Details holds only Hide from publishing, Scope the scopes every selected variable's type has (All supported properties and one box per scope; a selection that shares none says so). Each box is checked when all selected variables have it, empty when none, and shows a dash when they differ; a click sets it on all (or clears it when checked on all), each change one `command.batch`, so one undo step. Names, descriptions and code syntax stay single-variable, as in the reference. The Scope: menu entries stay. A tri-state `widgets::checkbox_tri` draws the dash. Tests: `variables_select_tests` (headless UI). Grade unchanged | | 2026-11-12 | minor | M7, #2215: pages take a variable mode: with nothing selected the Design panel's Page section shows a Variable modes section, Auto or a mode of each collection that has more than one, as the help center says ("Apply variable mode" in the Page section of the right sidebar) (#2215). The same section the frames' Variable modes use (`variable.setMode` without layers sets the page's mode), one undo step per pick. Binding the page background to a variable is not in the reference and is not offered | From 70aeeee941a20cf232c883fd2369da5fd8c1cddb Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Sat, 14 Nov 2026 04:05:13 +0600 Subject: [PATCH 2/3] Variables: test that a refused group rename undoes the moves already made (#2353) The collision test's two moves had names of the same length, so the first one already collided and nothing was left to undo. With the existing name on the second move, the first moves, the second collides, and the test checks that the batch puts the first one back. --- crates/ui-egui/src/variables_select_tests.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/variables_select_tests.rs b/crates/ui-egui/src/variables_select_tests.rs index c2b699eb8..2d5bd3342 100644 --- a/crates/ui-egui/src/variables_select_tests.rs +++ b/crates/ui-egui/src/variables_select_tests.rs @@ -405,10 +405,11 @@ fn renaming_a_group_into_its_own_new_subgroup_moves_the_deeper_names_first() { #[test] fn a_group_rename_onto_a_variable_name_that_stays_changes_nothing() { - let mut u = named_window(&["Orange/100", "Orange/200", "Palette/Orange/100"]); + // Orange/100 moves first; Orange/200 then collides, and the move already made is undone. + let mut u = named_window(&["Orange/100", "Orange/200", "Palette/Orange/200"]); rename_group(&mut u, "Orange", "Palette/Orange"); - assert!(u.app.ui.last_error.as_deref().is_some_and(|e| e.contains("Palette/Orange/100")), "{:?}", u.app.ui.last_error); - assert_eq!(sorted_names(&u), ["Orange/100", "Orange/200", "Palette/Orange/100"]); + assert!(u.app.ui.last_error.as_deref().is_some_and(|e| e.contains("Palette/Orange/200")), "{:?}", u.app.ui.last_error); + assert_eq!(sorted_names(&u), ["Orange/100", "Orange/200", "Palette/Orange/200"]); } fn open_edit_variables(u: &mut Ui) -> Vec { From 92d0ffdc62828a02dadacc6bd0817af436a806df Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Sat, 14 Nov 2026 04:31:08 +0600 Subject: [PATCH 3/3] Variables: fix the move order in the group rename collision test (#2353) Both moves had names of the same length, so which one ran first came from the order of the variables. The first one now has a longer name, which the longest-first order always moves first, so the second one always collides after a move that has to be undone. --- crates/ui-egui/src/variables_select_tests.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/variables_select_tests.rs b/crates/ui-egui/src/variables_select_tests.rs index 2d5bd3342..bcc41463d 100644 --- a/crates/ui-egui/src/variables_select_tests.rs +++ b/crates/ui-egui/src/variables_select_tests.rs @@ -405,11 +405,12 @@ fn renaming_a_group_into_its_own_new_subgroup_moves_the_deeper_names_first() { #[test] fn a_group_rename_onto_a_variable_name_that_stays_changes_nothing() { - // Orange/100 moves first; Orange/200 then collides, and the move already made is undone. - let mut u = named_window(&["Orange/100", "Orange/200", "Palette/Orange/200"]); + // The longer name moves first, whatever the order of the variables: Orange/1000 moves, then + // Orange/200 collides, and the move already made is undone. + let mut u = named_window(&["Orange/1000", "Orange/200", "Palette/Orange/200"]); rename_group(&mut u, "Orange", "Palette/Orange"); assert!(u.app.ui.last_error.as_deref().is_some_and(|e| e.contains("Palette/Orange/200")), "{:?}", u.app.ui.last_error); - assert_eq!(sorted_names(&u), ["Orange/100", "Orange/200", "Palette/Orange/200"]); + assert_eq!(sorted_names(&u), ["Orange/1000", "Orange/200", "Palette/Orange/200"]); } fn open_edit_variables(u: &mut Ui) -> Vec {