Files
figcraft/crates/engine/src/components_rebase_source_tests.rs
T
MortenHusted 6394e96005 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.
2026-10-22 13:41:56 +02:00

259 lines
12 KiB
Rust

//! Contextual import sources survive rebasing; detached and swapped subtrees use their current mains.
use super::*;
fn add(doc: &mut Document, parent: NodeId, kind: NodeKind, name: &str) -> NodeId {
doc.create(parent, kind, name, 0.0, 0.0, 20.0, 20.0).unwrap()
}
fn component(doc: &mut Document, parent: NodeId, name: &str) -> NodeId {
add(doc, parent, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, name)
}
fn mirror(doc: &mut Document, source: NodeId, parent: NodeId) -> NodeId {
let mut n = doc.node(source).unwrap().clone();
let children = std::mem::take(&mut n.children);
n.id = doc.alloc_id();
n.parent = None;
n.source = Some(source);
let id = n.id;
doc.insert(n, parent, None).unwrap();
for child in children {
mirror(doc, child, id);
}
id
}
fn instance(doc: &mut Document, main: NodeId, parent: NodeId, name: &str) -> NodeId {
let mut n = doc.node(main).unwrap().clone();
let children = std::mem::take(&mut n.children);
n.id = doc.alloc_id();
n.kind = NodeKind::Instance { component: main, props: BTreeMap::new() };
n.name = name.into();
n.parent = None;
n.source = None;
let id = n.id;
doc.insert(n, parent, None).unwrap();
for child in children {
mirror(doc, child, id);
}
id
}
struct Scene {
doc: Document,
inner: NodeId,
base: NodeId,
slot: NodeId,
context: NodeId,
root: NodeId,
nested: NodeId,
child: NodeId,
}
fn scene() -> Scene {
let mut doc = Document::new("Source context");
let page = doc.first_page().unwrap();
let inner = component(&mut doc, page, "Inner");
let base = add(&mut doc, inner, NodeKind::Rectangle, "Body");
doc.node_mut(base).unwrap().transform = Affine::translate((2.0, 0.0));
let outer = component(&mut doc, page, "Outer");
let slot = instance(&mut doc, inner, outer, "Nested");
doc.node_mut(slot).unwrap().source = Some(inner);
let context = doc.node(slot).unwrap().children[0];
doc.node_mut(context).unwrap().transform = Affine::translate((3.0, 0.0));
doc.node_mut(context).unwrap().opacity = 0.3;
let root = instance(&mut doc, outer, page, "Outer copy");
let nested = doc.node(root).unwrap().children[0];
let child = doc.node(nested).unwrap().children[0];
doc.node_mut(child).unwrap().transform = Affine::translate((2.0, 0.0));
doc.node_mut(child).unwrap().opacity = 0.2;
Scene { doc, inner, base, slot, context, root, nested, child }
}
fn both(s: &mut Scene) {
rebase_instance(&mut s.doc, s.nested);
rebase_instance(&mut s.doc, s.root);
}
#[test]
fn rebase_default_only_sources_promote_to_the_actual_outer_context() {
let mut s = scene();
s.doc.node_mut(s.nested).unwrap().source = None;
s.doc.node_mut(s.child).unwrap().source = None;
both(&mut s);
assert_eq!(s.doc.node(s.nested).unwrap().source, Some(s.slot));
assert_eq!(s.doc.node(s.child).unwrap().source, Some(s.context));
}
#[test]
fn rebase_rejects_a_context_source_from_another_nested_slot() {
let mut s = scene();
let outer = s.doc.node(s.slot).unwrap().parent.unwrap();
let sibling = instance(&mut s.doc, s.inner, outer, "Other nested slot");
s.doc.node_mut(sibling).unwrap().source = Some(s.inner);
let sibling_child = s.doc.node(sibling).unwrap().children[0];
mirror(&mut s.doc, sibling, s.root);
s.doc.node_mut(s.child).unwrap().source = Some(sibling_child);
both(&mut s);
assert_eq!(s.doc.node(s.nested).unwrap().source, Some(s.slot));
assert_eq!(s.doc.node(s.child).unwrap().source, Some(s.context));
assert_eq!(s.doc.node(sibling_child).unwrap().transform.translation().x, 2.0);
assert_eq!(s.doc.node(s.context).unwrap().transform.translation().x, 3.0);
}
#[test]
fn rebase_does_not_mark_inherited_context_values_as_local_overrides() {
let mut s = scene();
s.doc.node_mut(s.child).unwrap().opacity = 0.3;
both(&mut s);
assert_eq!(s.doc.node(s.child).unwrap().source, Some(s.context));
assert!(!s.doc.node(s.child).unwrap().overrides.contains("opacity"));
}
#[test]
fn rebase_preserves_multilevel_context_without_shortening_source_chains() {
let mut doc = Document::new("Nested source contexts");
let page = doc.first_page().unwrap();
let inner = component(&mut doc, page, "Inner");
let base = add(&mut doc, inner, NodeKind::Rectangle, "Body");
doc.node_mut(base).unwrap().transform = Affine::translate((2.0, 0.0));
let wrapper = component(&mut doc, page, "Wrapper");
let default_slot = instance(&mut doc, inner, wrapper, "Nested");
doc.node_mut(default_slot).unwrap().source = Some(inner);
let outer = component(&mut doc, page, "Outer");
let wrapper_slot = instance(&mut doc, wrapper, outer, "Wrapper slot");
doc.node_mut(wrapper_slot).unwrap().source = Some(wrapper);
let source_slot = doc.node(wrapper_slot).unwrap().children[0];
let context = doc.node(source_slot).unwrap().children[0];
doc.node_mut(context).unwrap().transform = Affine::translate((3.0, 0.0));
let root = instance(&mut doc, outer, page, "Outer copy");
let wrapper_copy = doc.node(root).unwrap().children[0];
let nested = doc.node(wrapper_copy).unwrap().children[0];
let child = doc.node(nested).unwrap().children[0];
doc.node_mut(nested).unwrap().name = "Renamed nested".into();
doc.node_mut(child).unwrap().transform = Affine::translate((2.0, 0.0));
for id in [nested, wrapper_copy, root] {
rebase_instance(&mut doc, id);
assert_eq!(doc.node(nested).unwrap().source, Some(source_slot));
assert_eq!(doc.node(child).unwrap().source, Some(context));
}
assert_eq!(doc.node(context).unwrap().source, Some(doc.node(default_slot).unwrap().children[0]));
assert_eq!(doc.node(context).unwrap().transform.translation().x, 3.0);
assert_eq!(doc.node(child).unwrap().transform.translation().x, 2.0);
}
#[test]
fn rebase_preserves_placed_frame_to_instance_and_current_main_children() {
let mut doc = Document::new("Typed nested source");
let page = doc.first_page().unwrap();
let inner = component(&mut doc, page, "Inner");
let body = add(&mut doc, inner, NodeKind::Rectangle, "Body");
let outer = component(&mut doc, page, "Outer");
let slot = add(&mut doc, outer, NodeKind::Frame, "Empty slot");
let root = instance(&mut doc, outer, page, "Outer copy");
let nested = doc.node(root).unwrap().children[0];
doc.node_mut(nested).unwrap().kind = NodeKind::Instance { component: inner, props: BTreeMap::new() };
let child = mirror(&mut doc, body, nested);
rebase_instance(&mut doc, nested);
rebase_instance(&mut doc, root);
assert_eq!(doc.node(nested).unwrap().source, Some(slot));
assert_eq!(doc.node(child).unwrap().source, Some(body));
assert!(matches!(doc.node(nested).unwrap().kind, NodeKind::Instance { component, .. } if component == inner));
}
#[test]
fn rebase_keeps_current_main_sources_and_component_delta_beneath_swap() {
let mut s = scene();
let page = s.doc.first_page().unwrap();
let replacement = component(&mut s.doc, page, "Replacement");
let body = add(&mut s.doc, replacement, NodeKind::Rectangle, "Body");
s.doc.node_mut(body).unwrap().transform = Affine::translate((7.0, 0.0));
s.doc.node_mut(s.nested).unwrap().kind = NodeKind::Instance { component: replacement, props: BTreeMap::new() };
s.doc.remove(s.child).unwrap();
s.child = mirror(&mut s.doc, body, s.nested);
s.doc.node_mut(s.child).unwrap().transform = Affine::translate((8.0, 0.0));
s.doc.node_mut(s.nested).unwrap().name = "Renamed swap".into();
rebase_instance(&mut s.doc, s.nested);
assert_eq!(s.doc.node(s.nested).unwrap().source, Some(s.slot));
assert!(s.doc.node(s.nested).unwrap().overrides.contains("component"));
rebase_instance(&mut s.doc, s.root);
assert_eq!(s.doc.node(s.nested).unwrap().source, Some(s.slot));
assert_eq!(s.doc.node(s.child).unwrap().source, Some(body));
assert!(s.doc.node(s.nested).unwrap().overrides.contains("component"));
assert_eq!(s.doc.node(s.child).unwrap().transform.translation().x, 8.0);
}
#[test]
fn rebase_detached_nested_uses_own_main_and_keeps_inherited_geometry() {
let mut s = scene();
let page = s.doc.first_page().unwrap();
let detached = mirror(&mut s.doc, s.nested, page);
let child = s.doc.node(detached).unwrap().children[0];
// The old source references still exist, but their Outer context is absent here.
s.doc.node_mut(detached).unwrap().source = Some(s.slot);
s.doc.node_mut(child).unwrap().source = Some(s.context);
s.doc.node_mut(child).unwrap().transform = Affine::translate((3.0, 0.0));
rebase_instance(&mut s.doc, detached);
assert_eq!(s.doc.node(detached).unwrap().source, Some(s.inner));
assert_eq!(s.doc.node(child).unwrap().source, Some(s.base));
assert_eq!(s.doc.node(child).unwrap().transform.translation().x, 3.0);
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();
let page = s.doc.first_page().unwrap();
let foreign = component(&mut s.doc, page, "Foreign");
let foreign_body = add(&mut s.doc, foreign, NodeKind::Rectangle, "Body");
s.doc.node_mut(s.child).unwrap().source = Some(foreign_body);
both(&mut s);
assert_ne!(s.doc.node(s.child).unwrap().source, Some(foreign_body));
assert_eq!(s.doc.node(s.child).unwrap().source, Some(s.context));
let mut s = scene();
let page = s.doc.first_page().unwrap();
let replacement = component(&mut s.doc, page, "Replacement");
let body = add(&mut s.doc, replacement, NodeKind::Rectangle, "Body");
s.doc.node_mut(s.nested).unwrap().kind = NodeKind::Instance { component: replacement, props: BTreeMap::new() };
s.doc.node_mut(s.child).unwrap().source = Some(s.context);
both(&mut s);
assert_ne!(s.doc.node(s.child).unwrap().source, Some(s.context), "the old default-main child is stale after a swap");
assert!(s.doc.node(s.child).unwrap().source.is_none() || s.doc.node(s.child).unwrap().source == Some(body));
}
#[test]
fn rebase_rejects_source_and_parent_cycles_with_bounded_lookups() {
let mut s = scene();
s.doc.node_mut(s.base).unwrap().source = Some(s.context);
both(&mut s);
assert_eq!(s.doc.node(s.child).unwrap().source, None);
let mut s = scene();
s.doc.node_mut(s.context).unwrap().parent = Some(s.context);
both(&mut s);
assert_ne!(s.doc.node(s.child).unwrap().source, Some(s.context));
let mut s = scene();
s.doc.node_mut(s.nested).unwrap().parent = Some(s.child);
s.doc.node_mut(s.child).unwrap().parent = Some(s.nested);
let sources = RebaseSources::new(&s.doc);
assert!(sources.owners(s.child).is_none());
let mut s = scene();
s.doc.node_mut(s.root).unwrap().kind = NodeKind::Instance { component: s.root, props: BTreeMap::new() };
rebase_instance(&mut s.doc, s.root);
assert_ne!(s.doc.node(s.root).unwrap().source, Some(s.root));
}