mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 03:46:50 -06:00
M3: a variable mode set on a layer inside an instance is an override
A variable mode chosen on a layer inside an instance (the icon inside a Close button, recoloured through an icon colour collection) wasn't an override group. A full rebuild of the instance starts each layer over from its main and carries only override groups, so the mode fell back to the main's. Library import always rebuilds, as do main edits such as a new auto layout direction, so a modal's recoloured close icon came into other files in its default colour. "modes" is now an override group: setting a mode inside an instance records it, rebuilds carry it, rebasing a nested instance finds it, the component panel lists it, and resetting it brings back the main's mode. A top-level instance's own modes stay its own and record no override, like its resizing.
This commit is contained in:
@@ -165,7 +165,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"component.resetOverride",
|
||||
"Reset override",
|
||||
"{id?, group? (fills|strokes|effects|opacity|visible|name|corners|characters|textStyle|padding|gap|alignment|grids|exports|interactions|size|position|component|props|slot; all when omitted)}",
|
||||
"{id?, group? (fills|strokes|effects|opacity|visible|name|corners|characters|textStyle|padding|gap|alignment|grids|exports|interactions|size|position|modes|component|props|slot; all when omitted)}",
|
||||
true,
|
||||
reset_override
|
||||
),
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
//! `characters`, `textStyle` (font, size, spacing, resizing, …), `padding`, `gap` and `alignment`
|
||||
//! (of auto layout: its direction and other settings are the main's), `grids`, `exports`,
|
||||
//! `interactions`, `size` and `resizing` (hug, fill or fixed, with min and max sizes) of layers
|
||||
//! inside an instance, `component` (a swapped nested instance), `props` (property values of a
|
||||
//! nested instance) and `slot` (a slot holding the instance's own content). An instance's own
|
||||
//! export settings, interactions and resizing are its own, like its place. `position`
|
||||
//! inside an instance, `modes` (the variable modes a layer inside an instance chooses),
|
||||
//! `component` (a swapped nested instance), `props` (property values of a nested instance) and
|
||||
//! `slot` (a slot holding the instance's own content). An instance's own export settings,
|
||||
//! interactions, resizing and variable modes are its own, like its place. `position`
|
||||
//! overrides of layers inside an instance are still read and kept, but new ones are refused
|
||||
//! ([`check_override_edit`]): those layers take their place and constraints from the main.
|
||||
//!
|
||||
@@ -60,6 +61,7 @@ pub const OVERRIDE_GROUPS: &[&str] = &[
|
||||
"size",
|
||||
"resizing",
|
||||
"position",
|
||||
"modes",
|
||||
"component",
|
||||
"props",
|
||||
SLOT,
|
||||
@@ -415,6 +417,7 @@ pub(crate) fn copy_group(from: &Node, to: &mut Node, g: &str) {
|
||||
(b.sizing_h, b.sizing_v, b.min_w, b.max_w, b.min_h, b.max_h) = (a.sizing_h, a.sizing_v, a.min_w, a.max_w, a.min_h, a.max_h);
|
||||
}
|
||||
"position" => to.transform = from.transform,
|
||||
"modes" => to.var_modes = from.var_modes.clone(),
|
||||
"component" => {
|
||||
if let (NodeKind::Instance { component: a, .. }, NodeKind::Instance { component: b, .. }) = (&from.kind, &mut to.kind) {
|
||||
*b = *a;
|
||||
@@ -1049,6 +1052,9 @@ pub fn diff_groups(a: &Node, b: &Node) -> Vec<&'static str> {
|
||||
if a.interactions != b.interactions {
|
||||
out.push("interactions");
|
||||
}
|
||||
if a.var_modes != b.var_modes {
|
||||
out.push("modes");
|
||||
}
|
||||
if let (NodeKind::Instance { component: x, props: px }, NodeKind::Instance { component: y, props: py }) = (&a.kind, &b.kind) {
|
||||
if x != y {
|
||||
out.push("component");
|
||||
@@ -1146,10 +1152,10 @@ pub fn sync_after_edit(doc: &mut Document, before: &Document, changed: &[NodeId]
|
||||
}
|
||||
let raw = diff_groups(b, n);
|
||||
let mut groups = raw.clone();
|
||||
// An instance's own place, resizing, export settings and interactions are its own, not
|
||||
// overrides.
|
||||
// An instance's own place, resizing, export settings, interactions and variable modes are
|
||||
// its own, not overrides.
|
||||
if top_root {
|
||||
groups.retain(|g| !matches!(*g, "component" | "props" | "position" | "resizing" | "exports" | "interactions"));
|
||||
groups.retain(|g| !matches!(*g, "component" | "props" | "position" | "resizing" | "exports" | "interactions" | "modes"));
|
||||
}
|
||||
if groups.iter().any(|g| matches!(*g, "size" | "position")) {
|
||||
groups.retain(|g| !matches!(*g, "size" | "position") || !geometry_derived(doc, before, id, g));
|
||||
@@ -1969,7 +1975,7 @@ pub fn rebase_instance(doc: &mut Document, inst: NodeId) {
|
||||
let mut groups: Vec<String> = value_groups(m, n)
|
||||
.into_iter()
|
||||
.filter(|g| !matches!(g.as_str(), "name" | "exports" | "interactions") && !from_variables.contains(&g.as_str()))
|
||||
.filter(|g| placed_source.is_some() || g != "resizing")
|
||||
.filter(|g| placed_source.is_some() || !matches!(g.as_str(), "resizing" | "modes"))
|
||||
.collect();
|
||||
let named = if placed_source.is_some() { n.name != m.name } else { n.name != default_instance_name(doc, component) };
|
||||
if named {
|
||||
@@ -1993,8 +1999,11 @@ pub fn rebase_instance(doc: &mut Document, inst: NodeId) {
|
||||
}
|
||||
if let Some(n) = doc.node_mut(inst) {
|
||||
n.source = root_base;
|
||||
// A top-level instance's resizing is its own (a nested one's overrides its main layer's).
|
||||
n.overrides.retain(|g| !matches!(g.as_str(), "component" | "props" | "position") && (placed_source.is_some() || g != "resizing"));
|
||||
// A top-level instance's resizing and modes are its own (a nested one's override its main
|
||||
// layer's).
|
||||
n.overrides.retain(|g| {
|
||||
!matches!(g.as_str(), "component" | "props" | "position") && (placed_source.is_some() || !matches!(g.as_str(), "resizing" | "modes"))
|
||||
});
|
||||
n.overrides.extend(root_groups);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,20 @@ fn rebase_detached_nested_uses_own_main_and_keeps_inherited_geometry() {
|
||||
assert_eq!(s.doc.node(s.base).unwrap().transform.translation().x, 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebase_marks_a_nested_roots_own_mode_but_not_a_top_level_instances() {
|
||||
let mut s = scene();
|
||||
let modes = BTreeMap::from([("Tone".to_string(), "B".to_string())]);
|
||||
s.doc.node_mut(s.nested).unwrap().var_modes = modes.clone();
|
||||
both(&mut s);
|
||||
assert!(s.doc.node(s.nested).unwrap().overrides.contains("modes"), "its slot in Outer sets no mode");
|
||||
let page = s.doc.first_page().unwrap();
|
||||
let detached = mirror(&mut s.doc, s.nested, page);
|
||||
rebase_instance(&mut s.doc, detached);
|
||||
let n = s.doc.node(detached).unwrap();
|
||||
assert_eq!((&n.var_modes, n.overrides.contains("modes")), (&modes, false), "a top-level instance's modes are its own");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebase_does_not_preserve_existing_foreign_or_stale_sources() {
|
||||
let mut s = scene();
|
||||
|
||||
@@ -126,3 +126,52 @@ fn detaching_keeps_the_mode_it_took_from_its_main() {
|
||||
assert_eq!(fill(&s, i), "0000FF", "the detached frame keeps mode B");
|
||||
assert_eq!(s.doc().unwrap().node(i).unwrap().var_modes.len(), 1);
|
||||
}
|
||||
|
||||
/// A "Button" holding an instance of a swatch "Icon" on mode A, and a "Modal" holding a Button
|
||||
/// instance whose icon copy was set to mode B: (the Button main, its icon layer, the copy in Modal).
|
||||
fn modal(s: &mut Session) -> (NodeId, NodeId, NodeId) {
|
||||
tone(s);
|
||||
let icon = swatch(s, "Icon", None);
|
||||
let frame = id(run(s, "frame.create", json!({"x": 100, "y": 0, "width": 60, "height": 40, "name": "Button"})));
|
||||
let button_icon = id(run(s, "instance.create", json!({"component": icon, "x": 10, "y": 10, "parent": frame})));
|
||||
set_mode(s, button_icon, json!("A"));
|
||||
let button = id(run(s, "component.create", json!({"ids": [frame]})));
|
||||
let outer = id(run(s, "frame.create", json!({"x": 300, "y": 0, "width": 200, "height": 100, "name": "Modal"})));
|
||||
let close = id(run(s, "instance.create", json!({"component": button, "x": 10, "y": 10, "parent": outer})));
|
||||
let nested = s.doc().unwrap().node(close).unwrap().children[0];
|
||||
set_mode(s, nested, json!("B"));
|
||||
run(s, "component.create", json!({"ids": [outer]}));
|
||||
(button, button_icon, nested)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mode_set_on_a_layer_inside_an_instance_is_an_override() {
|
||||
let mut s = Session::new();
|
||||
let (button, button_icon, nested) = modal(&mut s);
|
||||
assert_eq!(fill(&s, nested), "0000FF");
|
||||
// An edit the copies can't take as they are rebuilds the Button instance.
|
||||
run(&mut s, "node.set", json!({"ids": [button], "layout": "horizontal"}));
|
||||
assert_eq!(fill(&s, nested), "0000FF", "a rebuild keeps the mode set on the copy");
|
||||
// The main's layer choosing another mode doesn't reach it either.
|
||||
set_mode(&mut s, button_icon, json!("B"));
|
||||
set_mode(&mut s, button_icon, Value::Null);
|
||||
assert_eq!(fill(&s, nested), "0000FF");
|
||||
run(&mut s, "component.resetOverride", json!({"id": nested, "group": "modes"}));
|
||||
assert_eq!(fill(&s, nested), "FF0000", "reset: the main's layer is on Auto (mode A)");
|
||||
assert!(s.doc().unwrap().node(nested).unwrap().var_modes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mode_set_on_an_icon_inside_a_placed_instance_survives_main_edits() {
|
||||
let mut s = Session::new();
|
||||
let (button, _, _) = modal(&mut s);
|
||||
// A Button placed on the page, its icon recoloured through the icon's mode.
|
||||
let placed = instance(&mut s, button);
|
||||
let icon = s.doc().unwrap().node(placed).unwrap().children[0];
|
||||
set_mode(&mut s, icon, json!("B"));
|
||||
assert!(s.doc().unwrap().node(icon).unwrap().overrides.contains("modes"), "it shows as an override");
|
||||
run(&mut s, "node.set", json!({"ids": [button], "layout": "vertical"}));
|
||||
assert_eq!(fill(&s, icon), "0000FF", "a rebuild of the Button keeps it");
|
||||
run(&mut s, "component.resetInstance", json!({"ids": [placed]}));
|
||||
assert_eq!(fill(&s, icon), "FF0000", "reset all brings back the main's mode");
|
||||
}
|
||||
|
||||
@@ -344,3 +344,39 @@ fn library_style_bindings_ignore_unrelated_local_variables() {
|
||||
healthy(&s);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modes_set_inside_nested_instances_come_along() {
|
||||
let d = dir("nested-modes");
|
||||
let lib_path = path_str(&d.join("kit.figcraft"));
|
||||
let mut lib = Session::new();
|
||||
theme(&mut lib);
|
||||
let f = idv(&ex(&mut lib, "frame.create", json!({"x": 0, "y": 0, "width": 20, "height": 20, "name": "Icon"})));
|
||||
ex(&mut lib, "variable.bind", json!({"ids": [f], "field": "fill", "variable": "primary"}));
|
||||
let icon = idv(&ex(&mut lib, "component.create", json!({"ids": [f]})));
|
||||
let b = idv(&ex(&mut lib, "frame.create", json!({"x": 100, "y": 0, "width": 60, "height": 40, "name": "Button"})));
|
||||
ex(&mut lib, "instance.create", json!({"component": icon, "x": 10, "y": 10, "parent": b}));
|
||||
let button = idv(&ex(&mut lib, "component.create", json!({"ids": [b]})));
|
||||
let m = idv(&ex(&mut lib, "frame.create", json!({"x": 300, "y": 0, "width": 200, "height": 100, "name": "Modal"})));
|
||||
let close = idv(&ex(&mut lib, "instance.create", json!({"component": button, "x": 10, "y": 10, "parent": m})));
|
||||
let nested = lib.doc().unwrap().node(close).unwrap().children[0];
|
||||
ex(&mut lib, "variable.setMode", json!({"ids": [nested], "collection": "Theme", "mode": "Dark"}));
|
||||
ex(&mut lib, "component.create", json!({"ids": [m]}));
|
||||
let lid = publish_to(&mut lib, &lib_path);
|
||||
|
||||
let mut s = Session::new();
|
||||
ex(&mut s, "library.add", json!({"path": lib_path}));
|
||||
let inst = idv(&ex(&mut s, "library.place", json!({"library": lid, "component": "Modal", "x": 0, "y": 0})));
|
||||
let copy = |s: &Session| {
|
||||
let doc = s.doc().unwrap();
|
||||
doc.node(doc.node(inst).unwrap().children[0]).unwrap().children[0]
|
||||
};
|
||||
assert_eq!(fill(&s, copy(&s)), "FFFFFF", "the Dark mode set on the nested icon comes along");
|
||||
// An update that rebuilds the Button copies keeps it too.
|
||||
ex(&mut lib, "node.set", json!({"ids": [button], "layout": "horizontal"}));
|
||||
republish(&mut lib);
|
||||
ex(&mut s, "library.acceptUpdates", json!({}));
|
||||
assert_eq!(fill(&s, copy(&s)), "FFFFFF");
|
||||
healthy(&s);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
@@ -521,6 +521,7 @@ fn group_label(g: &str) -> String {
|
||||
"grids" => tr("Layout grids"),
|
||||
"exports" => tr("Export settings"),
|
||||
"interactions" => tr("Interactions"),
|
||||
"modes" => tr("Variable modes"),
|
||||
"size" => tr("Size"),
|
||||
"resizing" => tr("Resizing"),
|
||||
"position" => tr("Position"),
|
||||
|
||||
Reference in New Issue
Block a user