A ScrollFrame builds its ScrollChild before its Frames and OnLoad (#894)
A ScrollFrame built its `<ScrollChild>` after its `<Frames>` children and so after their OnLoads: under a scroll frame template, the template's scroll bar loaded before the instance's scroll child, and an OnLoad that read the other frame saw it unbuilt. In the reference, the ScrollFrame's load runs the base load, then builds the scroll child with its whole lifecycle and its own OnLoad, then stores it as the scroll child, all before the post-load hook builds `<Frames>` and fires the ScrollFrame's OnLoad. A template's scroll child is built at the template's level, ahead of the instance's, which wins. The loader now builds `<ScrollChild>` in that place, on the one path XML and `CreateFrame` share, and only for a ScrollFrame. Tests cover the order directly, through a template, and with a scroll child in both template and instance. Fixes #846 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
de17e48247
commit
46512dc5b5
@@ -710,6 +710,9 @@ impl Loader<'_> {
|
||||
self.apply_minimap(el, wrapper, dbg_name);
|
||||
// 6 · <Scripts> handlers (`0x769ef0`); OnLoad is captured to fire bottom-up below.
|
||||
let onload = self.apply_scripts(el, wrapper, dbg_name);
|
||||
// 6b · a ScrollFrame's <ScrollChild> (`0x786bc0`, after the base load `0x769820`): still
|
||||
// inside the load window, so ahead of every <Frames> 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
|
||||
// <Frames>, whose OnLoads fire before ours (`0x76a060`).
|
||||
@@ -721,26 +724,6 @@ impl Loader<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
// 7b · <ScrollChild>: its one child is built through `Instantiate 0x6ee280` like
|
||||
// `<Frames>`, 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}: <ScrollChild> 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}: <ScrollChild>: {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).
|
||||
|
||||
@@ -9,6 +9,41 @@ use super::{
|
||||
};
|
||||
|
||||
impl Loader<'_> {
|
||||
/// `<ScrollFrame>`'s `<ScrollChild>` (`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 `<Frames>` 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 `<ScrollChild>` 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}: <ScrollChild> 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}: <ScrollChild>: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `<StatusBar>` (`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) {
|
||||
|
||||
@@ -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#"<Script>Order = ""; function Log(w) Order = Order .. w .. ";" end</Script>"#;
|
||||
|
||||
/// A ScrollFrame's `LoadXML` (`0x786bc0`) runs the base load (`0x769820`), then builds its
|
||||
/// `<ScrollChild>` through `Instantiate 0x6ee280` at `0x786bf4`: the child's whole lifecycle,
|
||||
/// its own `<Frames>` and OnLoad included, is over before the post-load hook `0x76a2f0` builds
|
||||
/// the ScrollFrame's `<Frames>` (`0x76a36f`) and fires its OnLoad. `SetScrollChild` (`0x786ce0`)
|
||||
/// follows the child's return, so the child's own OnLoad sees none and `<Frames>` 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#"<Ui>{LOG}
|
||||
<ScrollFrame name="Roller">
|
||||
<Size><AbsDimension x="200" y="100"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<Frames>
|
||||
<Frame name="$parentBar">
|
||||
<Scripts><OnLoad>Log("bar:" .. tostring(this:GetParent():GetScrollChild() ~= nil))</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<ScrollChild>
|
||||
<Frame name="$parentChild">
|
||||
<Size><AbsDimension x="200" y="400"/></Size>
|
||||
<Frames>
|
||||
<Frame name="$parentInner">
|
||||
<Scripts><OnLoad>Log("inner")</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<Scripts><OnLoad>Log("child:" .. tostring(this:GetParent():GetScrollChild() ~= nil))</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</ScrollChild>
|
||||
<Scripts>
|
||||
<OnLoad>Log("scroll:" .. tostring(this:GetScrollChild() == RollerChild))</OnLoad>
|
||||
</Scripts>
|
||||
</ScrollFrame>
|
||||
</Ui>"#
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<String>("return Order").unwrap(),
|
||||
"inner;child:false;bar:true;scroll:true;",
|
||||
"the scroll child's subtree, then <Frames>, then the ScrollFrame's own OnLoad"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The same order when the `<Frames>` come from a template, which is how
|
||||
/// `UIPanelScrollFrameTemplate`'s ScrollBar meets an instance's `<ScrollChild>`: the template's
|
||||
/// `<Frames>` 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#"<Ui>{LOG}
|
||||
<ScrollFrame name="PanelScrollTemplate" virtual="true">
|
||||
<Frames>
|
||||
<Slider name="$parentScrollBar">
|
||||
<Scripts><OnLoad>Log("bar")</OnLoad></Scripts>
|
||||
</Slider>
|
||||
</Frames>
|
||||
<Scripts><OnLoad>Log("template-scroll")</OnLoad></Scripts>
|
||||
</ScrollFrame>
|
||||
<ScrollFrame name="ViaXml" inherits="PanelScrollTemplate">
|
||||
<Size><AbsDimension x="200" y="100"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
<ScrollChild>
|
||||
<Frame name="$parentChild">
|
||||
<Size><AbsDimension x="200" y="400"/></Size>
|
||||
<Scripts><OnLoad>Log("child")</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</ScrollChild>
|
||||
</ScrollFrame>
|
||||
</Ui>"#
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return Order").unwrap(),
|
||||
"child;bar;template-scroll;",
|
||||
"through inherits= in XML"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>("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::<String>("return Order").unwrap(),
|
||||
"bar;template-scroll;"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
/// A `<ScrollChild>` 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 `<Frames>`; 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#"<Ui>{LOG}
|
||||
<ScrollFrame name="StockishTemplate" virtual="true">
|
||||
<Frames>
|
||||
<Frame name="$parentBar">
|
||||
<Scripts><OnLoad>Log("bar")</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<ScrollChild>
|
||||
<Frame name="$parentFromTemplate">
|
||||
<Scripts><OnLoad>Log("from-template")</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</ScrollChild>
|
||||
</ScrollFrame>
|
||||
<ScrollFrame name="Both" inherits="StockishTemplate">
|
||||
<ScrollChild>
|
||||
<Frame name="$parentFromInstance">
|
||||
<Scripts><OnLoad>Log("from-instance")</OnLoad></Scripts>
|
||||
</Frame>
|
||||
</ScrollChild>
|
||||
</ScrollFrame>
|
||||
</Ui>"#
|
||||
),
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return Order").unwrap(),
|
||||
"from-template;from-instance;bar;"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>("return Both:GetScrollChild() == BothFromInstance")
|
||||
.unwrap());
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user