diff --git a/crates/benilla-ui/src/loader/mod.rs b/crates/benilla-ui/src/loader/mod.rs index 2be04727..26d508c2 100644 --- a/crates/benilla-ui/src/loader/mod.rs +++ b/crates/benilla-ui/src/loader/mod.rs @@ -710,6 +710,9 @@ impl Loader<'_> { self.apply_minimap(el, wrapper, dbg_name); // 6 · handlers (`0x769ef0`); OnLoad is captured to fire bottom-up below. let onload = self.apply_scripts(el, wrapper, dbg_name); + // 6b · a ScrollFrame's (`0x786bc0`, after the base load `0x769820`): still + // inside the load window, so ahead of every child and of our own OnLoad. + self.apply_scrollframe(el, wrapper, self_name, dbg_name); // 7 · the post-load hook (`0x76a2f0`): close the window (`0x76a2ff`), then the nested // , whose OnLoads fire before ours (`0x76a060`). @@ -721,26 +724,6 @@ impl Loader<'_> { } } - // 7b · : its one child is built through `Instantiate 0x6ee280` like - // ``, then stored at `+0x318` with `+0x314 = 1`, which is `SetScrollChild` - // and gives the ScrollFrame its scroll range. An empty one errors in the reference. - for sc in children_named(el, "ScrollChild") { - let Some(child) = sc.children.first() else { - self.report.fault(format!( - "{dbg_name}: is empty — the reference errors here, and a \ - ScrollFrame with no child has no scroll range at all" - )); - continue; - }; - let expanded = self.expand(child); - let Some(child_wrapper) = self.materialize(&expanded, Some(wrapper), self_name) else { - continue; // materialize already reported why - }; - if let Err(e) = wrapper.call_method::<()>("SetScrollChild", child_wrapper) { - self.report.fault(format!("{dbg_name}: : {e}")); - } - } - // 7c · deferred anchors: every child exists now, as at the reference's layout pass. self.drain_deferred_anchors(deferred_mark); // 8 · this frame's OnLoad, now that its subtree is complete (bottom-up). diff --git a/crates/benilla-ui/src/loader/widgets.rs b/crates/benilla-ui/src/loader/widgets.rs index b7b5d38a..58b2b52b 100644 --- a/crates/benilla-ui/src/loader/widgets.rs +++ b/crates/benilla-ui/src/loader/widgets.rs @@ -9,6 +9,41 @@ use super::{ }; impl Loader<'_> { + /// ``'s `` (`LoadXML 0x786bc0`): after the base load (`0x769820`) it + /// builds the node's one child through `Instantiate 0x6ee280` (`0x786bf4`), a whole frame + /// lifecycle with the child's own `` and OnLoad, and only then stores it with + /// `SetScrollChild` (`0x786ce0`), which gives the frame its scroll range. So the child's OnLoad + /// sees no scroll child yet. A template's `` was built by its own `LoadXML` level + /// (`0x76985c`), ahead of the instance's, which therefore wins. An empty one errors in the + /// reference (`0x786c0c`). Other frame kinds do not read the element. + pub(super) fn apply_scrollframe( + &mut self, + el: &Element, + wrapper: &Table, + self_name: &str, + dbg: &str, + ) { + if !el.tag.eq_ignore_ascii_case("ScrollFrame") { + return; + } + for sc in children_named(el, "ScrollChild") { + let Some(child) = sc.children.first() else { + self.report.fault(format!( + "{dbg}: is empty — the reference errors here, and a \ + ScrollFrame with no child has no scroll range at all" + )); + continue; + }; + let expanded = self.expand(child); + let Some(child_wrapper) = self.materialize(&expanded, Some(wrapper), self_name) else { + continue; // materialize already reported why + }; + if let Err(e) = wrapper.call_method::<()>("SetScrollChild", child_wrapper) { + self.report.fault(format!("{dbg}: : {e}")); + } + } + } + /// `` (`0x782ef0`): a reversed `minValue`/`maxValue` pair is swapped, as /// `SetMinMaxValues` also does; `drawLayer` is the bar texture's layer, ARTWORK by default. pub(super) fn apply_statusbar(&mut self, el: &Element, wrapper: &Table, dbg: &str) { diff --git a/crates/benilla-ui/src/script/tests/create_frame_template.rs b/crates/benilla-ui/src/script/tests/create_frame_template.rs index 498b75b4..d22c4a69 100644 --- a/crates/benilla-ui/src/script/tests/create_frame_template.rs +++ b/crates/benilla-ui/src/script/tests/create_frame_template.rs @@ -502,3 +502,153 @@ fn a_raising_template_onload_reaches_the_error_handler() { "CreateFrame returns the frame after its OnLoad raised" ); } + +/// The load order the tests below read: a Lua `Order` string the OnLoads append to. +const LOG: &str = r#""#; + +/// A ScrollFrame's `LoadXML` (`0x786bc0`) runs the base load (`0x769820`), then builds its +/// `` through `Instantiate 0x6ee280` at `0x786bf4`: the child's whole lifecycle, +/// its own `` and OnLoad included, is over before the post-load hook `0x76a2f0` builds +/// the ScrollFrame's `` (`0x76a36f`) and fires its OnLoad. `SetScrollChild` (`0x786ce0`) +/// follows the child's return, so the child's own OnLoad sees none and `` sees it. +#[test] +fn a_scroll_child_is_built_before_the_frames_and_the_scroll_frames_onload() { + let mut s = script(); + s.set_screen_size(1024.0, 768.0); + register( + &s, + &format!( + r#"{LOG} + + + + + + Log("bar:" .. tostring(this:GetParent():GetScrollChild() ~= nil)) + + + + + + + + Log("inner") + + + Log("child:" .. tostring(this:GetParent():GetScrollChild() ~= nil)) + + + + Log("scroll:" .. tostring(this:GetScrollChild() == RollerChild)) + + + "# + ), + ); + + assert_eq!( + s.eval::("return Order").unwrap(), + "inner;child:false;bar:true;scroll:true;", + "the scroll child's subtree, then , then the ScrollFrame's own OnLoad" + ); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +} + +/// The same order when the `` come from a template, which is how +/// `UIPanelScrollFrameTemplate`'s ScrollBar meets an instance's ``: the template's +/// `` are built by the post-load hook, after every `LoadXML` level (`0x76a060`). +#[test] +fn a_templates_frames_load_after_the_instances_scroll_child() { + let mut s = script(); + s.set_screen_size(1024.0, 768.0); + register( + &s, + &format!( + r#"{LOG} + + + + Log("bar") + + + Log("template-scroll") + + + + + + + + Log("child") + + + + "# + ), + ); + assert_eq!( + s.eval::("return Order").unwrap(), + "child;bar;template-scroll;", + "through inherits= in XML" + ); + assert!(s + .eval::("return ViaXml:GetScrollChild() == ViaXmlChild") + .unwrap()); + + // The runtime door builds through the same instantiator. + s.run( + r#"Order = "" + ViaLua = CreateFrame("ScrollFrame", "ViaLua", nil, "PanelScrollTemplate") + local c = CreateFrame("Frame", "ViaLuaChild", ViaLua) + ViaLua:SetScrollChild(c)"#, + ) + .unwrap(); + assert_eq!( + s.eval::("return Order").unwrap(), + "bar;template-scroll;" + ); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +} + +/// A `` the template declares is built by the template's own `LoadXML` level +/// (`0x76985c` re-enters the virtual `LoadXML`, which is `0x786bc0` for a ScrollFrame), so it is +/// also ahead of ``; an instance's own follows it and wins `SetScrollChild`. +#[test] +fn a_template_scroll_child_loads_ahead_of_the_frames_and_the_instances_wins() { + let mut s = script(); + s.set_screen_size(1024.0, 768.0); + register( + &s, + &format!( + r#"{LOG} + + + + Log("bar") + + + + + Log("from-template") + + + + + + + Log("from-instance") + + + + "# + ), + ); + assert_eq!( + s.eval::("return Order").unwrap(), + "from-template;from-instance;bar;" + ); + assert!(s + .eval::("return Both:GetScrollChild() == BothFromInstance") + .unwrap()); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +}