M3: a swapped nested instance keeps its outer main's overrides

A Button main's override on its icon instance's glyph (a red fill) was
lost when an instance swapped the icon: the outer rebuild re-copies the
glyph from the main's layer with no overrides of its own, so the nested
rebuild matched it by name and carried nothing. Before that rebuild, each
old layer of the swapped instance now takes the overrides held along the
source chain it had before this rebuild, through layers inside instances
in the mains that place the swapped instance. Geometry, slot content and
groups the layer overrides itself are left out. Matched by name, the
values carry onto the new main's layer as the instance's own: inherited
at the swap, frozen after, and back to the icon main's value on reset.
An icon main's own overrides stay with it.
This commit is contained in:
MortenHusted
2026-10-22 11:26:04 +02:00
parent a86897dc91
commit c141dee771
3 changed files with 249 additions and 3 deletions
+70 -2
View File
@@ -830,9 +830,10 @@ fn sync_instance_depth(doc: &mut Document, inst: NodeId, depth: usize, spare: BT
}
// Nested instances showing another main than the main's copy (swapped): rebuild from it,
// with the old layers below them this rebuild didn't reuse.
let unused: BTreeMap<NodeId, std::sync::Arc<Node>> = old.into_iter().filter(|(i, _)| !used.contains(i) && !doc.contains(*i)).collect();
let unused: BTreeMap<NodeId, std::sync::Arc<Node>> =
old.iter().filter(|(i, _)| !used.contains(*i) && !doc.contains(**i)).map(|(i, x)| (*i, x.clone())).collect();
for n in nested {
let below: BTreeMap<NodeId, std::sync::Arc<Node>> = unused
let mut below: BTreeMap<NodeId, std::sync::Arc<Node>> = unused
.iter()
.filter(|(_, x)| {
let mut cur = x.parent;
@@ -847,10 +848,77 @@ fn sync_instance_depth(doc: &mut Document, inst: NodeId, depth: usize, spare: BT
})
.map(|(i, x)| (*i, x.clone()))
.collect();
take_placing_overrides(doc, n, &old, &mut below);
sync_instance_depth(doc, n, depth + 1, below);
}
}
/// Before swapped nested instance `n` is rebuilt from its new main: each of its old layers (the
/// copies just made of the outer main's, and `below`) takes the overrides held along the source
/// chain it had before this rebuild (`before`), through layers inside instances in the mains that
/// place `n`. Matched by name, they carry onto the new main's layers as the instance's own, so a
/// swap takes what the outer main set once: later rebuilds find layers mirroring the new main.
/// Geometry and slot content follow the new main; groups the layer overrides itself keep its value.
fn take_placing_overrides(
doc: &mut Document,
n: NodeId,
before: &BTreeMap<NodeId, std::sync::Arc<Node>>,
below: &mut BTreeMap<NodeId, std::sync::Arc<Node>>,
) {
let placing = placing_mains(doc, n);
for id in doc.subtree(n).into_iter().filter(|i| *i != n) {
let held = before.get(&id).map(|b| held_along(doc, b, &placing)).unwrap_or_default();
if let Some(layer) = doc.node_mut(id) {
take(layer, &held);
}
}
for layer in below.values_mut() {
let held = held_along(doc, layer, &placing);
if !held.is_empty() {
take(std::sync::Arc::make_mut(layer), &held);
}
}
}
/// The mains placing nested instance `n`: those of the layers it mirrors, outward.
fn placing_mains(doc: &Document, n: NodeId) -> BTreeSet<NodeId> {
let mut out = BTreeSet::new();
let mut cur = doc.node(n).and_then(|x| x.source);
for _ in 0..=MAX_DEPTH {
let Some(s) = cur.and_then(|c| doc.node(c)).filter(|s| !matches!(s.kind, NodeKind::Component { .. })) else { break };
out.extend(owning_component(doc, s.id));
cur = s.source;
}
out
}
/// The overrides but geometry and slot content held along `layer`'s source chain while it runs
/// through layers inside instances in `placing` mains, each with the nearest layer holding it.
fn held_along(doc: &Document, layer: &Node, placing: &BTreeSet<NodeId>) -> BTreeMap<String, std::sync::Arc<Node>> {
let mut out = BTreeMap::new();
let mut cur = layer.source;
for _ in 0..=MAX_DEPTH {
let Some(s) = cur.and_then(|c| doc.node_arc(c)) else { break };
if !inside_instance(doc, s.id) || !owning_component(doc, s.id).is_some_and(|m| placing.contains(&m)) {
break;
}
for g in s.overrides.iter().filter(|g| !matches!(g.as_str(), "size" | "position" | SLOT)) {
out.entry(g.clone()).or_insert_with(|| s.clone());
}
cur = s.source;
}
out
}
/// Give `layer` the `held` groups it has no override of its own for.
fn take(layer: &mut Node, held: &BTreeMap<String, std::sync::Arc<Node>>) {
for (g, from) in held {
if layer.overrides.insert(g.clone()) {
copy_group(from, layer, g);
}
}
}
/// Whether main components `a` and `b` are two variants of one set.
fn sibling_variants(doc: &Document, a: NodeId, b: NodeId) -> bool {
a != b && variant_set(doc, a).is_some_and(|s| variant_set(doc, b) == Some(s))
+176
View File
@@ -198,3 +198,179 @@ fn copied_instances_keep_their_overrides() {
assert!(node(&s, t).source.is_none() && node(&s, t).overrides.is_empty());
healthy(&s);
}
/// `button()` with the main's copy of the icon's Shape filled red, and an instance-swap property
/// `Icon` on the nested icon: (icon, shape, button, nested).
fn button_with_icon_override(s: &mut Session) -> (NodeId, NodeId, NodeId, NodeId) {
let (icon_main, shape, button, nested) = button(s);
let main_shape = copy_of(s, nested, shape);
ex(s, "node.set", json!({"id": main_shape, "props": {"fill": "#ff0000"}}));
ex(s, "component.addProperty", json!({"id": button, "name": "Icon", "type": "instanceSwap", "default": icon_main, "layer": nested}));
(icon_main, shape, button, nested)
}
/// Swap instance `i`'s icon to `to` by its instance-swap property `Icon`.
fn swap_icon(s: &mut Session, i: NodeId, to: NodeId) {
ex(s, "instance.setProperties", json!({"ids": [i], "properties": {"Icon": to}}));
healthy(s);
}
#[test]
fn a_main_override_carries_onto_a_swapped_nested_instance() {
// Documented: overrides survive a swap when layer names match, and a swapped icon takes the
// colour the main component set (forum thread with staff replies). Rows: by the instance-swap
// property and by instance.swap, onto a matching and an unmatched name. The main's size
// override is geometry and stays out; the instance's own override wins over the main's.
let mut s = Session::new();
let (_icon, shape, button, nested) = button_with_icon_override(&mut s);
let main_shape = copy_of(&s, nested, shape);
ex(&mut s, "node.set", json!({"id": main_shape, "props": {"width": 10}}));
let other = icon(&mut s, 200.0, "Other", "Shape");
let unmatched = icon(&mut s, 300.0, "Unmatched", "Glyph");
for (by_property, to, want) in [(true, other, "FF0000"), (false, other, "FF0000"), (true, unmatched, "111111"), (false, unmatched, "111111")] {
let i = idv(&ex(&mut s, "instance.create", json!({"component": button, "x": 300, "y": 400})));
if by_property {
swap_icon(&mut s, i, to);
} else {
let inested = copy_of(&s, i, nested);
ex(&mut s, "instance.swap", json!({"ids": [inested], "component": to}));
healthy(&s);
}
let glyph = kids(&s, copy_of(&s, i, nested))[0];
assert_eq!(fill(&s, glyph), want, "by property {by_property}, to {to}");
assert_eq!(node(&s, glyph).width, 20.0, "the main's size override stays out");
}
let i = idv(&ex(&mut s, "instance.create", json!({"component": button, "x": 300, "y": 600})));
let glyph = kids(&s, copy_of(&s, i, nested))[0];
ex(&mut s, "node.set", json!({"id": glyph, "props": {"fill": "#0000ff"}}));
swap_icon(&mut s, i, other);
assert_eq!(fill(&s, kids(&s, copy_of(&s, i, nested))[0]), "0000FF", "the instance's own override wins");
}
#[test]
fn a_main_override_carries_onto_a_swap_one_level_deeper() {
// As above for a Button instance inside a Toolbar main, swapped in a Toolbar instance.
let mut s = Session::new();
let (_icon, _shape, button, nested) = button_with_icon_override(&mut s);
let other = icon(&mut s, 200.0, "Other", "Shape");
let bar = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 600, "width": 300, "height": 60, "name": "Toolbar"})));
let inner = idv(&ex(&mut s, "instance.create", json!({"component": button, "x": 10, "y": 10, "parent": bar})));
let toolbar = idv(&ex(&mut s, "component.create", json!({"ids": [bar]})));
let t = idv(&ex(&mut s, "instance.create", json!({"component": toolbar, "x": 0, "y": 800})));
let icon_in_t = copy_of(&s, t, copy_of(&s, inner, nested));
ex(&mut s, "instance.swap", json!({"ids": [icon_in_t], "component": other}));
healthy(&s);
assert_eq!(fill(&s, kids(&s, icon_in_t)[0]), "FF0000");
}
#[test]
fn a_swapped_nested_instance_keeps_the_main_override_it_took() {
// Documented (staff reply): later edits of the main's icon colour don't reach instances that
// swapped, and do reach the rest. Undo and redo follow; resetting the override shows the icon
// main's own fill.
let mut s = Session::new();
let (_icon, shape, button, nested) = button_with_icon_override(&mut s);
let other = icon(&mut s, 200.0, "Other", "Shape");
let swapped = idv(&ex(&mut s, "instance.create", json!({"component": button, "x": 300, "y": 400})));
let kept = idv(&ex(&mut s, "instance.create", json!({"component": button, "x": 300, "y": 500})));
swap_icon(&mut s, swapped, other);
let glyph = |s: &Session, i: NodeId| fill(s, kids(s, copy_of(s, i, nested))[0]);
let main_shape = copy_of(&s, nested, shape);
ex(&mut s, "node.set", json!({"id": main_shape, "props": {"fill": "#00ff00"}}));
healthy(&s);
assert_eq!((glyph(&s, swapped), glyph(&s, kept)), ("FF0000".into(), "00FF00".into()));
s.undo().unwrap();
assert_eq!((glyph(&s, swapped), glyph(&s, kept)), ("FF0000".into(), "FF0000".into()));
s.redo().unwrap();
assert_eq!((glyph(&s, swapped), glyph(&s, kept)), ("FF0000".into(), "00FF00".into()));
let swapped_glyph = kids(&s, copy_of(&s, swapped, nested))[0];
ex(&mut s, "component.resetOverride", json!({"id": swapped_glyph, "group": "fills"}));
healthy(&s);
assert_eq!(glyph(&s, swapped), "111111");
}
#[test]
fn a_swapped_in_icon_keeps_its_own_overrides_to_itself() {
// Docs silent; pinned: what an icon main overrides inside itself stays that main's. After a
// swap to it, its edits still reach the instance; a second swap doesn't carry them on.
let mut s = Session::new();
let (_icon, _shape, button, nested) = button_with_icon_override(&mut s);
let dot = icon(&mut s, 200.0, "Dot", "Shape");
let with_dot = |s: &mut Session, x: f64, name: &str, fill: Option<&str>| {
let f = idv(&ex(s, "frame.create", json!({"x": x, "y": 300, "width": 24, "height": 24, "name": name})));
let d = idv(&ex(s, "instance.create", json!({"component": dot, "x": 0, "y": 0, "parent": f})));
if let Some(c) = fill {
let shape = kids(s, d)[0];
ex(s, "node.set", json!({"id": shape, "props": {"fill": c}}));
}
(idv(&ex(s, "component.create", json!({"ids": [f]}))), d)
};
let (other2, dot2) = with_dot(&mut s, 300.0, "Other2", Some("#0000ff"));
let (other3, _) = with_dot(&mut s, 400.0, "Other3", None);
let i = idv(&ex(&mut s, "instance.create", json!({"component": button, "x": 300, "y": 400})));
swap_icon(&mut s, i, other2);
let shape_in = |s: &Session| kids(s, kids(s, copy_of(s, i, nested))[0])[0];
assert_eq!(fill(&s, shape_in(&s)), "0000FF");
let shape2 = kids(&s, dot2)[0];
ex(&mut s, "node.set", json!({"id": shape2, "props": {"fill": "#00ffff"}}));
healthy(&s);
assert_eq!(fill(&s, shape_in(&s)), "00FFFF", "Other2's own edit reaches it");
swap_icon(&mut s, i, other3);
assert_eq!(fill(&s, shape_in(&s)), "111111", "Other2's override doesn't carry onto Other3");
}
#[test]
fn a_swapped_out_icons_own_overrides_stay_with_it() {
// Docs silent; pinned: what the Button's default icon main overrides inside itself isn't the
// Button main's to carry. Icon A colours its Base's Shape green; swapping to icon B, also built
// on Base, shows Base's own fill.
let mut s = Session::new();
let base = icon(&mut s, 0.0, "Base", "Shape");
let on_base = |s: &mut Session, x: f64, name: &str| {
let f = idv(&ex(s, "frame.create", json!({"x": x, "y": 500, "width": 24, "height": 24, "name": name})));
let b = idv(&ex(s, "instance.create", json!({"component": base, "x": 0, "y": 0, "parent": f})));
(idv(&ex(s, "component.create", json!({"ids": [f]}))), b)
};
let (a, a_base) = on_base(&mut s, 100.0, "A");
let (b, _) = on_base(&mut s, 200.0, "B");
let a_shape = kids(&s, a_base)[0];
ex(&mut s, "node.set", json!({"id": a_shape, "props": {"fill": "#00ff00"}}));
let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 700, "width": 40, "height": 40, "name": "Button"})));
let nested = idv(&ex(&mut s, "instance.create", json!({"component": a, "x": 4, "y": 4, "parent": f})));
let button = idv(&ex(&mut s, "component.create", json!({"ids": [f]})));
ex(&mut s, "component.addProperty", json!({"id": button, "name": "Icon", "type": "instanceSwap", "default": a, "layer": nested}));
let i = idv(&ex(&mut s, "instance.create", json!({"component": button, "x": 0, "y": 800})));
swap_icon(&mut s, i, b);
let shape = kids(&s, kids(&s, copy_of(&s, i, nested))[0])[0];
assert_eq!(fill(&s, shape), "111111");
assert!(node(&s, shape).overrides.is_empty());
}
#[test]
fn a_slot_the_main_filled_shows_the_swapped_in_mains_content() {
// Docs silent on slot content across a swap; pinned: a Chip the Panel main put in its card's
// slot doesn't carry onto the swapped-in card, which shows its own default content.
let mut s = Session::new();
let card = |s: &mut Session, x: f64, name: &str| {
let f = idv(&ex(s, "frame.create", json!({"x": x, "y": 0, "width": 100, "height": 60, "name": name})));
let content = idv(&ex(s, "frame.create", json!({"x": 0, "y": 0, "width": 100, "height": 60, "parent": f, "name": "Content"})));
ex(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 40, "height": 20, "parent": content, "props": {"name": format!("{name} default")}}));
let c = idv(&ex(s, "component.create", json!({"ids": [f]})));
ex(s, "component.makeSlot", json!({"id": content}));
c
};
let (a, b) = (card(&mut s, 0.0, "A"), card(&mut s, 200.0, "B"));
let chip = icon(&mut s, 400.0, "Chip", "Dot");
let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 400, "width": 200, "height": 100, "name": "Panel"})));
let nested = idv(&ex(&mut s, "instance.create", json!({"component": a, "x": 0, "y": 0, "parent": f})));
let panel = idv(&ex(&mut s, "component.create", json!({"ids": [f]})));
ex(&mut s, "instance.slotInsert", json!({"id": nested, "slot": "Content", "component": chip, "replace": true}));
ex(&mut s, "component.addProperty", json!({"id": panel, "name": "Card", "type": "instanceSwap", "default": a, "layer": nested}));
let i = idv(&ex(&mut s, "instance.create", json!({"component": panel, "x": 0, "y": 700})));
ex(&mut s, "instance.setProperties", json!({"ids": [i], "properties": {"Card": b}}));
healthy(&s);
let content = kids(&s, copy_of(&s, i, nested))[0];
let names: Vec<String> = kids(&s, content).into_iter().map(|k| node(&s, k).name.clone()).collect();
assert_eq!(names, ["B default"]);
assert!(!node(&s, content).overrides.contains(SLOT));
}
+3 -1
View File
@@ -720,7 +720,9 @@ most when it can't be reached).
- `instance.swap {ids?, component}` shows another component in the instances (overrides on layers
with matching names stay; property values with the same names too). Nested instances can be
swapped on their own (an override of the instance they're in).
swapped on their own (an override of the instance they're in); a swapped nested instance keeps
what its outer main overrode on matching layers as its own, so later edits of those in the main
don't reach it.
- `instance.swapAt {component, x, y}` swaps the instance at a page point: of the instances under
it (innermost first), the first showing a variant of the same component set, else the innermost
(never one inside the dropped component); only text layers keep their overrides. The Assets