diff --git a/crates/benilla-ui/src/framexml.rs b/crates/benilla-ui/src/framexml.rs index 0e1cdfe1..6a993d63 100644 --- a/crates/benilla-ui/src/framexml.rs +++ b/crates/benilla-ui/src/framexml.rs @@ -315,8 +315,9 @@ fn element_from_node(node: roxmltree::Node) -> Element { /// attributes override or extend the template's (case-insensitively). The reference merges /// nothing: `LoadXML` re-enters itself on the one object against each template, the most distant /// first, then the instance (`0x76985c`; `` repeat the walk, `0x76a060`), and reads `name` -/// and `id` from the instance alone (`PreLoadXML 0x769770`). Here `name` and `virtual` merge like -/// any attribute. A cycle is skipped with a warning. +/// and `id` from the instance alone (`PreLoadXML 0x769770`). Here every attribute but `name` and +/// `id` merges, `virtual` among them, so an unnamed child of a named template stays unnamed. A +/// cycle is skipped with a warning. pub fn expand( element: &Element, templates: &HashMap<&str, &Element>, @@ -392,7 +393,15 @@ fn expand_inner( /// [`expand`]'s merge; `over`'s tag wins, and its body when it has one. fn merge(base: &Element, over: &Element) -> Element { - let mut attrs = base.attrs.clone(); + // `name` and `id` belong to the instance alone (`PreLoadXML 0x769770`): an unnamed child that + // inherits a *named* virtual template stays unnamed, so its regions expand `$parent` against + // the real parent frame rather than the template's name. Every other attribute merges. + let mut attrs: Vec<(String, String)> = base + .attrs + .iter() + .filter(|(k, _)| !k.eq_ignore_ascii_case("name") && !k.eq_ignore_ascii_case("id")) + .cloned() + .collect(); for (k, v) in &over.attrs { if let Some(slot) = attrs.iter_mut().find(|(ek, _)| ek.eq_ignore_ascii_case(k)) { slot.1 = v.clone(); diff --git a/crates/benilla-ui/src/loader/tests.rs b/crates/benilla-ui/src/loader/tests.rs index dd5bcd1c..b50dedd7 100644 --- a/crates/benilla-ui/src/loader/tests.rs +++ b/crates/benilla-ui/src/loader/tests.rs @@ -2445,6 +2445,42 @@ mod chunk_name_tests { "the reference's own no-argument contract must reach the same frame" ); } + + /// An unnamed child that inherits a *named* virtual template stays unnamed: the reference + /// reads `name` from the instance alone (`PreLoadXML 0x769770`), so the child's regions expand + /// `$parent` against the real parent frame. ItemRack ships this shape — an unnamed + /// `` under `ItemRackInv0` names its texture + /// `ItemRackInv0Queue`, the global `Rack.OnEvent` looks up (ItemRack.lua:4015). + #[test] + fn an_unnamed_instance_does_not_take_its_template_name() { + let s = UiScript::new().unwrap(); + let doc = parse( + r#" + + + + + + + + + + + + "#, + ); + let report = load(&s, &doc, &no_files); + assert!(report.errors.is_empty(), "{:?}", report.errors); + assert!( + s.eval::("return ItemRackInv0Queue ~= nil").unwrap(), + "the unnamed child's texture must be named from the real parent, not the template: {:?}", + report.warnings + ); + assert!( + s.eval::("return ItemRackQueueTemplate == nil").unwrap(), + "a virtual template must not leak a global named after it" + ); + } } #[test]