OnShow and OnHide fire children first, and after OnLoad, as in 1.12.1 (#842)
Show and hide propagated a whole subtree first, then fired from a snapshot, parent first: a parent's `OnShow` ran before its children's, and a child hidden by an earlier handler still got its `OnShow`. A frame visible at the end of its load got no `OnShow` at all. In 1.12.1 show `0x76ae10` and hide `0x76ad50` mark the frame, walk the live child list re-checking each child's gate, and fire the frame's own handler last (`0x76aef5`, `0x76adfd`). The post-load hook fires `OnShow` after `OnLoad` for a visible frame (`0x76a3b3`), from XML and `CreateFrame` alike. Now handlers run inside a live post-order walk, and the post-load `OnShow` fires. Tests cover the order, a sibling hidden mid-walk, and one or two `OnShow`s after a load; they fail without the fix. Four existing tests' expected orders flip to the reference's. Fixes #350 Fixes #59 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dc63a06178
commit
c2fc3cf304
@@ -110,7 +110,8 @@
|
|||||||
BenillaOptionsFrame:SetScale(k)
|
BenillaOptionsFrame:SetScale(k)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The era tab width, label + 40, measured after OnShow sets the scale.
|
-- The era tab width, label + 40, measured after the window's OnShow sets the scale:
|
||||||
|
-- the tabs' own OnShows run before it, children first.
|
||||||
function BenillaOptionsTab_FitWidth(tab)
|
function BenillaOptionsTab_FitWidth(tab)
|
||||||
tab:SetWidth(getglobal(tab:GetName() .. "Text"):GetStringWidth() + 40)
|
tab:SetWidth(getglobal(tab:GetName() .. "Text"):GetStringWidth() + 40)
|
||||||
end
|
end
|
||||||
@@ -1341,9 +1342,6 @@
|
|||||||
<NormalFont inherits="GameFontHighlightSmall"/>
|
<NormalFont inherits="GameFontHighlightSmall"/>
|
||||||
<HighlightFont inherits="GameFontHighlightSmall"/>
|
<HighlightFont inherits="GameFontHighlightSmall"/>
|
||||||
<DisabledFont inherits="GameFontHighlightSmall"/>
|
<DisabledFont inherits="GameFontHighlightSmall"/>
|
||||||
<Scripts>
|
|
||||||
<OnShow>BenillaOptionsTab_FitWidth(this)</OnShow>
|
|
||||||
</Scripts>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button name="$parentAddOnsTab">
|
<Button name="$parentAddOnsTab">
|
||||||
<Size><AbsDimension x="87" y="37"/></Size>
|
<Size><AbsDimension x="87" y="37"/></Size>
|
||||||
@@ -1371,7 +1369,6 @@
|
|||||||
<!-- No addon list behind it: disabled, in the era's unselected gold. -->
|
<!-- No addon list behind it: disabled, in the era's unselected gold. -->
|
||||||
<Scripts>
|
<Scripts>
|
||||||
<OnLoad>this:Disable()</OnLoad>
|
<OnLoad>this:Disable()</OnLoad>
|
||||||
<OnShow>BenillaOptionsTab_FitWidth(this)</OnShow>
|
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -3068,6 +3065,8 @@
|
|||||||
<!-- Controls by default; selection persists and the scale re-fits, as the era's. -->
|
<!-- Controls by default; selection persists and the scale re-fits, as the era's. -->
|
||||||
<OnShow>
|
<OnShow>
|
||||||
BenillaOptionsFrame_UpdateScale()
|
BenillaOptionsFrame_UpdateScale()
|
||||||
|
BenillaOptionsTab_FitWidth(BenillaOptionsFrameGameTab)
|
||||||
|
BenillaOptionsTab_FitWidth(BenillaOptionsFrameAddOnsTab)
|
||||||
BenillaOptionsFrame_SelectCategory(this.selectedCategory or "Controls")
|
BenillaOptionsFrame_SelectCategory(this.selectedCategory or "Controls")
|
||||||
if UpdateMicroButtons then UpdateMicroButtons() end
|
if UpdateMicroButtons then UpdateMicroButtons() end
|
||||||
</OnShow>
|
</OnShow>
|
||||||
|
|||||||
@@ -492,16 +492,19 @@ fn toggle_inspect_reopens_after_a_close_and_raises_like_the_reference() {
|
|||||||
s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
|
s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
|
||||||
"the window still shows — Show() runs before OnShow raises"
|
"the window still shows — Show() runs before OnShow raises"
|
||||||
);
|
);
|
||||||
// Two raises: `InspectFrame` and `InspectPaperDollFrame` each run their own `OnShow` under
|
// Two raises: `InspectPaperDollFrame` and `InspectFrame` each run their own `OnShow` under
|
||||||
// their own pcall, the first dying at the portrait's gate, the second at `UnitLevel`'s.
|
// their own pcall, the page first, since the show cascade notifies children before their
|
||||||
|
// parent (`0x76aed5`, then `0x76aef5`): the page dies at `UnitLevel`'s gate, the window at
|
||||||
|
// the portrait's.
|
||||||
let errors = s.errors();
|
let errors = s.errors();
|
||||||
assert_eq!(errors.len(), 2, "one raise per OnShow handler: {errors:?}");
|
assert_eq!(errors.len(), 2, "one raise per OnShow handler: {errors:?}");
|
||||||
assert!(
|
assert!(
|
||||||
errors[0].contains(r#"Usage: SetPortraitTexture(texture, "unit")"#),
|
errors[0].contains(r#"Usage: UnitLevel("unit")"#),
|
||||||
"the portrait's gate, before the name line is ever reached: {errors:?}"
|
"the paper doll's own handler, dying at its first getter: {errors:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
errors[1].contains(r#"Usage: UnitLevel("unit")"#),
|
errors[1].contains(r#"Usage: SetPortraitTexture(texture, "unit")"#),
|
||||||
"and the paper doll's own handler, dying at its first getter: {errors:?}"
|
"then the window's, at the portrait's gate, before the name line is ever reached: \
|
||||||
|
{errors:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -660,6 +660,8 @@ pub(super) const BAG_UI: &[&str] = &[
|
|||||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||||
|
// `CharacterFrame.xml`'s tab template, for the hidden parent `PaperDollFrame` needs below.
|
||||||
|
r"Interface\FrameXML\CharacterFrameTemplates.xml",
|
||||||
// The dialog engine, after the `UIPanelCloseButton` it inherits.
|
// The dialog engine, after the `UIPanelCloseButton` it inherits.
|
||||||
r"Interface\FrameXML\StaticPopup.xml",
|
r"Interface\FrameXML\StaticPopup.xml",
|
||||||
// `ContainerFrameItemButton_OnClick` hides `StackSplitFrame` on every plain click
|
// `ContainerFrameItemButton_OnClick` hides `StackSplitFrame` on every plain click
|
||||||
@@ -683,7 +685,9 @@ pub(super) const BAG_UI: &[&str] = &[
|
|||||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||||
// `BagSlotButtonTemplate` inherits `PaperDollItemSlotButtonTemplate`, and its OnLoad
|
// `BagSlotButtonTemplate` inherits `PaperDollItemSlotButtonTemplate`, and its OnLoad
|
||||||
// (`PaperDollItemSlotButton_OnLoad`) gives each bag button its inventory-slot id, 20..23.
|
// (`PaperDollItemSlotButton_OnLoad`) gives each bag button its inventory-slot id, 20..23.
|
||||||
// `CharacterFrame.xml` stays out: a missing `parent=` only warns.
|
// Its hidden parent `CharacterFrame` comes first: a parentless `PaperDollFrame` would be
|
||||||
|
// visible at the end of its load and run its OnShow, which reaches for the Honor tab.
|
||||||
|
"Interface\\FrameXML\\CharacterFrame.xml",
|
||||||
"Interface\\FrameXML\\PaperDollFrame.xml",
|
"Interface\\FrameXML\\PaperDollFrame.xml",
|
||||||
// The stock bag bar, with `BagSlotButtonTemplate` and `KEYRING_CONTAINER`.
|
// The stock bag bar, with `BagSlotButtonTemplate` and `KEYRING_CONTAINER`.
|
||||||
"Interface\\FrameXML\\MainMenuBarBagButtons.xml",
|
"Interface\\FrameXML\\MainMenuBarBagButtons.xml",
|
||||||
|
|||||||
@@ -374,10 +374,9 @@ fn only_the_bids_pane_gets_its_page_without_being_opened() {
|
|||||||
assert!(s.take_errors().is_empty());
|
assert!(s.take_errors().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The reference's show cascade `0x76ae10` is post-order: a frame marks itself visible
|
/// The show cascade `0x76ae10` is post-order: a frame marks itself visible (`0x76ae7b`), walks its
|
||||||
/// (`0x76ae7b`), walks its children re-reading live links, and fires its own `OnShow` last
|
/// children re-reading live links, and fires its own `OnShow` last (`0x76aef5`), so the pane the
|
||||||
/// (`0x76aef5`). benilla's is pre-order, a gap in the engine; for this window both orders reach
|
/// XML leaves shown is notified before the window.
|
||||||
/// the same state, so only the `ShowOrder` assertions are benilla's order.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_show_cascade_notifies_the_bids_pane_once_and_keeps_its_page() {
|
fn the_show_cascade_notifies_the_bids_pane_once_and_keeps_its_page() {
|
||||||
let _data = benilla_formats::wow_data_or_skip!();
|
let _data = benilla_formats::wow_data_or_skip!();
|
||||||
@@ -408,11 +407,7 @@ fn the_show_cascade_notifies_the_bids_pane_once_and_keeps_its_page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
s.fire_event("AUCTION_HOUSE_SHOW", vec![]);
|
s.fire_event("AUCTION_HOUSE_SHOW", vec![]);
|
||||||
assert_eq!(
|
assert_eq!(order(&s), "pane,window", "children first, the window last");
|
||||||
order(&s),
|
|
||||||
"window,pane",
|
|
||||||
"OURS, and a known deviation: the reference's cascade is post-order, \"pane,window\""
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
page(&s),
|
page(&s),
|
||||||
"0",
|
"0",
|
||||||
@@ -431,7 +426,7 @@ fn the_show_cascade_notifies_the_bids_pane_once_and_keeps_its_page() {
|
|||||||
s.fire_event("AUCTION_HOUSE_SHOW", vec![]);
|
s.fire_event("AUCTION_HOUSE_SHOW", vec![]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
order(&s),
|
order(&s),
|
||||||
"window,pane,window",
|
"pane,window,window",
|
||||||
"the reopen notifies the window and not the pane it left hidden"
|
"the reopen notifies the window and not the pane it left hidden"
|
||||||
);
|
);
|
||||||
assert_eq!(page(&s), "0", "kept from the first open, not re-assigned");
|
assert_eq!(page(&s), "0", "kept from the first open, not re-assigned");
|
||||||
|
|||||||
@@ -642,7 +642,7 @@ impl Loader<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Everything a frame element does to an existing frame, from `LoadXML` attributes to its
|
/// Everything a frame element does to an existing frame, from `LoadXML` attributes to its
|
||||||
/// `OnLoad` (last, bottom-up, `0x76a060`); shared by [`Self::materialize`] and
|
/// `OnLoad` (bottom-up, `0x76a060`) and the post-load OnShow; shared by [`Self::materialize`] and
|
||||||
/// [`apply_template`]. `$parent` is `self_name` in this frame's contents and `parent_name` in
|
/// [`apply_template`]. `$parent` is `self_name` in this frame's contents and `parent_name` in
|
||||||
/// its own anchors (`0x76c5b0`).
|
/// its own anchors (`0x76c5b0`).
|
||||||
pub(super) fn decorate(
|
pub(super) fn decorate(
|
||||||
@@ -655,6 +655,9 @@ impl Loader<'_> {
|
|||||||
) {
|
) {
|
||||||
// This frame's deferred anchors drain before its OnLoad; a nested frame drains its own.
|
// This frame's deferred anchors drain before its OnLoad; a nested frame drains its own.
|
||||||
let deferred_mark = self.deferred_anchors.len();
|
let deferred_mark = self.deferred_anchors.len();
|
||||||
|
// 1c · the pre-load hook opens the frame's load window (`0x7697c2`): until the post-load
|
||||||
|
// hook, its own OnShow and OnHide fire nothing.
|
||||||
|
crate::script::visibility::set_load_window(self.lua(), wrapper, true);
|
||||||
// 2 · LoadXML attributes (`0x769820`).
|
// 2 · LoadXML attributes (`0x769820`).
|
||||||
self.apply_attrs(el, wrapper, dbg_name);
|
self.apply_attrs(el, wrapper, dbg_name);
|
||||||
// 3 · <Size> and 4 · <Anchors> (the CLayoutFrame geometry base, `0x767800`).
|
// 3 · <Size> and 4 · <Anchors> (the CLayoutFrame geometry base, `0x767800`).
|
||||||
@@ -681,7 +684,9 @@ impl Loader<'_> {
|
|||||||
// 6 · <Scripts> handlers (`0x769ef0`); OnLoad is captured to fire bottom-up below.
|
// 6 · <Scripts> handlers (`0x769ef0`); OnLoad is captured to fire bottom-up below.
|
||||||
let onload = self.apply_scripts(el, wrapper, dbg_name);
|
let onload = self.apply_scripts(el, wrapper, dbg_name);
|
||||||
|
|
||||||
// 7 · nested <Frames>, whose OnLoads fire before ours (`0x76a060`).
|
// 7 · the post-load hook (`0x76a2f0`): close the window (`0x76a2ff`), then the nested
|
||||||
|
// <Frames>, whose OnLoads fire before ours (`0x76a060`).
|
||||||
|
crate::script::visibility::set_load_window(self.lua(), wrapper, false);
|
||||||
for frames_el in children_named(el, "Frames") {
|
for frames_el in children_named(el, "Frames") {
|
||||||
for child in &frames_el.children {
|
for child in &frames_el.children {
|
||||||
let expanded = self.expand(child);
|
let expanded = self.expand(child);
|
||||||
@@ -717,6 +722,8 @@ impl Loader<'_> {
|
|||||||
if let Some(func) = onload {
|
if let Some(func) = onload {
|
||||||
self.fire_onload(wrapper, &func, dbg_name);
|
self.fire_onload(wrapper, &func, dbg_name);
|
||||||
}
|
}
|
||||||
|
// 9 · and its OnShow, if it is visible now (`0x76a3b3`).
|
||||||
|
crate::script::visibility::post_load_show(self.lua(), wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply one anchor, deferring it (when `may_defer`) while its named `relativeTo` is unbuilt.
|
/// Apply one anchor, deferring it (when `may_defer`) while its named `relativeTo` is unbuilt.
|
||||||
|
|||||||
@@ -2470,6 +2470,112 @@ mod chunk_name_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The post-load hook's OnShow (`0x76a2f0`), for a frame visible at the end of its load.
|
||||||
|
mod post_load_show_tests {
|
||||||
|
use crate::framexml;
|
||||||
|
use crate::loader::*;
|
||||||
|
use crate::script::UiScript;
|
||||||
|
|
||||||
|
fn no_files(_: &str) -> Option<Vec<u8>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(text: &str) -> framexml::ParsedDocument {
|
||||||
|
framexml::parse(text).expect("valid FrameXML")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The post-load hook `0x76a2f0` builds the `<Frames>` children (`0x76a36f`, their own hooks
|
||||||
|
/// inside), fires OnLoad (`0x76a387`), then fires OnShow (`0x76a3b3`) when the frame is visible
|
||||||
|
/// (`0x76a38c`) and has the handler (`0x76a396`): a frame visible at the end of its load gets
|
||||||
|
/// one OnShow, after its OnLoad and its children's.
|
||||||
|
#[test]
|
||||||
|
fn a_frame_visible_at_the_end_of_its_load_gets_one_onshow_after_its_onload() {
|
||||||
|
let s = UiScript::new().unwrap();
|
||||||
|
s.run("LOG = {} function Log(tag) table.insert(LOG, tag) end")
|
||||||
|
.unwrap();
|
||||||
|
let doc = parse(
|
||||||
|
r#"<Ui>
|
||||||
|
<Frame name="Born">
|
||||||
|
<Scripts>
|
||||||
|
<OnLoad>Log("load")</OnLoad>
|
||||||
|
<OnShow>Log("show")</OnShow>
|
||||||
|
</Scripts>
|
||||||
|
<Frames>
|
||||||
|
<Frame name="BornChild">
|
||||||
|
<Scripts>
|
||||||
|
<OnLoad>Log("child-load")</OnLoad>
|
||||||
|
<OnShow>Log("child-show")</OnShow>
|
||||||
|
</Scripts>
|
||||||
|
</Frame>
|
||||||
|
</Frames>
|
||||||
|
</Frame>
|
||||||
|
</Ui>"#,
|
||||||
|
);
|
||||||
|
let report = load_in(&s, &doc, "Test.xml", &no_files);
|
||||||
|
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||||
|
assert_eq!(
|
||||||
|
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||||
|
"child-load,child-show,load,show"
|
||||||
|
);
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The post-load OnShow is keyed on the frame's visibility at `0x76a38c`, and nothing records
|
||||||
|
/// an OnShow already given: a frame first shown by its own OnLoad gets that `Show()`'s OnShow
|
||||||
|
/// and then the hook's, while a frame still hidden gets none.
|
||||||
|
#[test]
|
||||||
|
fn a_frame_shown_by_its_onload_gets_two_onshows_and_a_hidden_one_none() {
|
||||||
|
let s = UiScript::new().unwrap();
|
||||||
|
s.run("LOG = {} function Log(tag) table.insert(LOG, tag) end")
|
||||||
|
.unwrap();
|
||||||
|
let doc = parse(
|
||||||
|
r#"<Ui>
|
||||||
|
<Frame name="ShownByLoad" hidden="true">
|
||||||
|
<Scripts>
|
||||||
|
<OnLoad>Log("a-load") this:Show() Log("a-load-end")</OnLoad>
|
||||||
|
<OnShow>Log("a-show")</OnShow>
|
||||||
|
</Scripts>
|
||||||
|
</Frame>
|
||||||
|
<Frame name="StaysHidden" hidden="true">
|
||||||
|
<Scripts>
|
||||||
|
<OnLoad>Log("b-load")</OnLoad>
|
||||||
|
<OnShow>Log("b-show")</OnShow>
|
||||||
|
<OnHide>Log("b-hide")</OnHide>
|
||||||
|
</Scripts>
|
||||||
|
</Frame>
|
||||||
|
</Ui>"#,
|
||||||
|
);
|
||||||
|
let report = load_in(&s, &doc, "Test.xml", &no_files);
|
||||||
|
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||||
|
assert_eq!(
|
||||||
|
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||||
|
"a-load,a-show,a-load-end,a-show,b-load"
|
||||||
|
);
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The post-load OnShow is the script alone (`0x76a3b3` calls the runner `0x702690`, not the
|
||||||
|
/// notify slot `+0x30`), so an `autoFocus` EditBox born visible runs its OnShow and still takes
|
||||||
|
/// no keyboard: the focus is the EditBox notify's (`0x77a750`), which only a transition reaches.
|
||||||
|
#[test]
|
||||||
|
fn the_post_load_onshow_runs_the_script_without_the_editbox_autofocus() {
|
||||||
|
let s = UiScript::new().unwrap();
|
||||||
|
let doc = parse(
|
||||||
|
r#"<Ui>
|
||||||
|
<EditBox name="BornBox" autoFocus="true">
|
||||||
|
<Size><AbsDimension x="100" y="20"/></Size>
|
||||||
|
<Scripts><OnShow>BOX_SHOWN = true</OnShow></Scripts>
|
||||||
|
</EditBox>
|
||||||
|
</Ui>"#,
|
||||||
|
);
|
||||||
|
let report = load_in(&s, &doc, "Test.xml", &no_files);
|
||||||
|
assert!(report.errors.is_empty(), "{:?}", report.errors);
|
||||||
|
assert!(s.eval::<bool>("return BOX_SHOWN == true").unwrap());
|
||||||
|
assert_eq!(s.focused_editbox_name(), None);
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn join_ref_resolves_relative_framexml_paths() {
|
fn join_ref_resolves_relative_framexml_paths() {
|
||||||
use super::join_ref;
|
use super::join_ref;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use mlua::{Function, Lua, Table, Value};
|
|||||||
|
|
||||||
use super::{Model, ScriptValue, REG_SCRIPTS};
|
use super::{Model, ScriptValue, REG_SCRIPTS};
|
||||||
use crate::script::object::frame_wrapper;
|
use crate::script::object::frame_wrapper;
|
||||||
use crate::widget::{ButtonState, FrameHandle};
|
|
||||||
|
|
||||||
/// `UiScript::fire_event` for engine code that holds only the Lua context.
|
/// `UiScript::fire_event` for engine code that holds only the Lua context.
|
||||||
pub(super) fn fire_global(lua: &Lua, event: &str, args: &[ScriptValue]) {
|
pub(super) fn fire_global(lua: &Lua, event: &str, args: &[ScriptValue]) {
|
||||||
@@ -108,75 +107,6 @@ pub(super) fn fire_size_changes(lua: &Lua) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fire `OnShow`/`OnHide` for the frames whose effective visibility just changed, recording
|
|
||||||
/// errors in [`Model::errors`]. A shown `toplevel` frame raises here, after the subtree has
|
|
||||||
/// propagated and before its `OnShow` (`0x76ae10` at `0x76aee0`), so a show that does not come
|
|
||||||
/// through Lua `Show` raises too.
|
|
||||||
pub(super) fn fire_visibility_changes(lua: &Lua, changed: Vec<FrameHandle>) {
|
|
||||||
// Resolve (handle, id, now-visible?) under one short borrow, then fire with no borrow held.
|
|
||||||
let items: Vec<(FrameHandle, u32, bool)> = {
|
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
|
||||||
changed
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|h| {
|
|
||||||
let vis = model.arena.frame(h)?.effective_visible;
|
|
||||||
Some((h, model.frame_id(h), vis))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
// Hiding the hovered frame, directly or by an ancestor, fires its `OnLeave` inside the hide and
|
|
||||||
// before its `OnHide` (`0x764ba0`'s tail in `0x76ad50`, the leave at `0x764cce`), clears the
|
|
||||||
// hover and the drag-arm (`+0x100`/`+0x104`) and arms the re-pick; a show arms it too
|
|
||||||
// (`0x764b8d`). This leave is how a tooltip closes with its window; the reference has no other.
|
|
||||||
let left: Option<(FrameHandle, u32)> = {
|
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
|
||||||
let hidden_hover = model
|
|
||||||
.mouseover
|
|
||||||
.filter(|&m| items.iter().any(|&(h, _, vis)| h == m && !vis));
|
|
||||||
// The leave is a virtual call (`0x764cce`, `[vtable+0x50]`): a disabled Button's own
|
|
||||||
// `0x7794e0` skips its `OnLeave`, while the hover and the drag-arm still clear.
|
|
||||||
let notified = super::button::hover_notify_runs(&model, hidden_hover);
|
|
||||||
if let Some(m) = hidden_hover {
|
|
||||||
model.mouseover = None;
|
|
||||||
if model.drag.as_ref().is_some_and(|d| d.source == m) {
|
|
||||||
model.drag = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model.hover_repick |= hidden_hover.is_some() || items.iter().any(|&(_, _, vis)| vis);
|
|
||||||
hidden_hover
|
|
||||||
.filter(|_| notified)
|
|
||||||
.map(|m| (m, model.frame_id(m)))
|
|
||||||
};
|
|
||||||
if let Some((_, oid)) = left {
|
|
||||||
if let Err(e) = fire_widget_handler(lua, oid, "OnLeave", vec![Value::Boolean(true)]) {
|
|
||||||
lua.app_data_mut::<Model>()
|
|
||||||
.expect("model")
|
|
||||||
.record_script_error(e.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (h, id, visible) in items {
|
|
||||||
if visible {
|
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
|
||||||
super::object::toplevel::raise_on_show(&mut model, h);
|
|
||||||
} else {
|
|
||||||
// `CSimpleButton`'s hide notify (`+0x34`, `0x7791e0`) un-presses it, unless disabled
|
|
||||||
// or locked, before the base notify, so a button hidden while held comes back unpushed.
|
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
|
||||||
super::button::edge(&mut model, h, ButtonState::on_hide);
|
|
||||||
}
|
|
||||||
let name = if visible { "OnShow" } else { "OnHide" };
|
|
||||||
if let Err(e) = fire(lua, id, name, None, Vec::new()) {
|
|
||||||
lua.app_data_mut::<Model>()
|
|
||||||
.expect("model")
|
|
||||||
.record_script_error(e.to_string());
|
|
||||||
}
|
|
||||||
// The EditBox show/hide overrides (`0x81c910` `+0x30`/`+0x34`) call the base notify first,
|
|
||||||
// so this runs after the `fire`: an `autoFocus` box takes a free focus, a hidden box
|
|
||||||
// releases its own.
|
|
||||||
super::editbox::visibility_focus(lua, h, visible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a handler is bound under `script`, without firing it: the keyboard walk consumes on
|
/// Whether a handler is bound under `script`, without firing it: the keyboard walk consumes on
|
||||||
/// the slot's presence (`0x76b7d0`). A failed lookup reads absent, so the gate never raises.
|
/// the slot's presence (`0x76b7d0`). A failed lookup reads absent, so the gate never raises.
|
||||||
pub(super) fn has_widget_handler(lua: &Lua, id: u32, script: &str) -> bool {
|
pub(super) fn has_widget_handler(lua: &Lua, id: u32, script: &str) -> bool {
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ mod trainer;
|
|||||||
mod types;
|
mod types;
|
||||||
mod unit;
|
mod unit;
|
||||||
mod video_pairs;
|
mod video_pairs;
|
||||||
|
pub(crate) mod visibility;
|
||||||
mod weapon_enchant;
|
mod weapon_enchant;
|
||||||
mod who_sort;
|
mod who_sort;
|
||||||
mod worldmap;
|
mod worldmap;
|
||||||
|
|||||||
@@ -154,11 +154,12 @@ pub struct NamePlateClick {
|
|||||||
pub button: String,
|
pub button: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a sync fires outside the model borrow: `OnShow`/`OnHide` for frames whose visibility
|
/// What a sync runs outside the model borrow: the plates' `Show`/`Hide`, whose transitions fire
|
||||||
/// flipped and `OnValueChanged` for moved bars (pfUI hooks it, `nameplates.lua:393`).
|
/// handlers, and `OnValueChanged` for moved bars (pfUI hooks it, `nameplates.lua:393`).
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct SyncEffects {
|
struct SyncEffects {
|
||||||
visibility: Vec<FrameHandle>,
|
/// `(plate, shown)` in the order the sync issued them.
|
||||||
|
visibility: Vec<(FrameHandle, bool)>,
|
||||||
values: Vec<(FrameHandle, f32)>,
|
values: Vec<(FrameHandle, f32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ impl UiScript {
|
|||||||
};
|
};
|
||||||
sync(&mut model, world, geometry, states)
|
sync(&mut model, world, geometry, states)
|
||||||
};
|
};
|
||||||
super::event::fire_visibility_changes(lua, effects.visibility);
|
apply_visibility(lua, effects.visibility);
|
||||||
for (bar, value) in effects.values {
|
for (bar, value) in effects.values {
|
||||||
super::statusbar::fire_engine_value_changed(lua, bar, value);
|
super::statusbar::fire_engine_value_changed(lua, bar, value);
|
||||||
}
|
}
|
||||||
@@ -304,7 +305,7 @@ impl UiScript {
|
|||||||
}
|
}
|
||||||
effects
|
effects
|
||||||
};
|
};
|
||||||
super::event::fire_visibility_changes(lua, effects.visibility);
|
apply_visibility(lua, effects.visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How many plate widgets the pool holds.
|
/// How many plate widgets the pool holds.
|
||||||
@@ -315,6 +316,13 @@ impl UiScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the plates' `Show`/`Hide` a sync issued, each a full transition with its handlers.
|
||||||
|
fn apply_visibility(lua: &mlua::Lua, verbs: Vec<(FrameHandle, bool)>) {
|
||||||
|
for (frame, shown) in verbs {
|
||||||
|
super::visibility::set_shown(lua, frame, shown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Record a click on `id` if it is a live plate; every button click comes here.
|
/// Record a click on `id` if it is a live plate; every button click comes here.
|
||||||
pub(super) fn note_click(lua: &mlua::Lua, id: u32, button: &str) {
|
pub(super) fn note_click(lua: &mlua::Lua, id: u32, button: &str) {
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||||
@@ -368,7 +376,7 @@ fn sync(
|
|||||||
let free = (0..model.nameplates.plates.len())
|
let free = (0..model.nameplates.plates.len())
|
||||||
.find(|i| model.nameplates.plates[*i].last.is_none());
|
.find(|i| model.nameplates.plates[*i].last.is_none());
|
||||||
let i = free.unwrap_or_else(|| {
|
let i = free.unwrap_or_else(|| {
|
||||||
let plate = Plate::create(model, world, &mut effects);
|
let plate = Plate::create(model, world);
|
||||||
model.nameplates.plates.push(plate);
|
model.nameplates.plates.push(plate);
|
||||||
model.nameplates.plates.len() - 1
|
model.nameplates.plates.len() - 1
|
||||||
});
|
});
|
||||||
@@ -396,7 +404,7 @@ fn sync(
|
|||||||
impl Plate {
|
impl Plate {
|
||||||
/// Build one plate: the Button, its six regions in the reference's order, and the health bar.
|
/// Build one plate: the Button, its six regions in the reference's order, and the health bar.
|
||||||
/// Written once, so a region an addon blanks stays blank.
|
/// Written once, so a region an addon blanks stays blank.
|
||||||
fn create(model: &mut Model, world: FrameHandle, effects: &mut SyncEffects) -> Plate {
|
fn create(model: &mut Model, world: FrameHandle) -> Plate {
|
||||||
// Anonymous, as the reference's name is zeroed (`0x76c50b`): plate addons reject a named
|
// Anonymous, as the reference's name is zeroed (`0x76c50b`): plate addons reject a named
|
||||||
// `WorldFrame` child.
|
// `WorldFrame` child.
|
||||||
let frame = model.arena.create(FrameKind::Button, None, Some(world));
|
let frame = model.arena.create(FrameKind::Button, None, Some(world));
|
||||||
@@ -479,9 +487,8 @@ impl Plate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Born retired and hidden: addons tell a live plate from a pooled one by `IsShown()`.
|
// Born retired and hidden: addons tell a live plate from a pooled one by `IsShown()`.
|
||||||
effects
|
// Silent, since nothing can have set a handler on a frame not yet handed out.
|
||||||
.visibility
|
model.arena.set_shown(frame, false);
|
||||||
.extend(model.arena.set_shown(frame, false));
|
|
||||||
|
|
||||||
model
|
model
|
||||||
.nameplates
|
.nameplates
|
||||||
@@ -625,9 +632,7 @@ impl Plate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if last.is_none() {
|
if last.is_none() {
|
||||||
effects
|
effects.visibility.push((frame, true));
|
||||||
.visibility
|
|
||||||
.extend(model.arena.set_shown(frame, true));
|
|
||||||
}
|
}
|
||||||
if last.as_ref().is_none_or(|l| l.alpha != state.alpha) {
|
if last.as_ref().is_none_or(|l| l.alpha != state.alpha) {
|
||||||
model.arena.set_alpha(frame, state.alpha);
|
model.arena.set_alpha(frame, state.alpha);
|
||||||
@@ -722,9 +727,7 @@ impl Plate {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let frame = model.nameplates.plates[i].frame;
|
let frame = model.nameplates.plates[i].frame;
|
||||||
effects
|
effects.visibility.push((frame, false));
|
||||||
.visibility
|
|
||||||
.extend(model.arena.set_shown(frame, false));
|
|
||||||
model.nameplates.plates[i].last = None;
|
model.nameplates.plates[i].last = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use mlua::{Lua, Table, Value};
|
|||||||
use crate::script::region_map::{set_shared, Side};
|
use crate::script::region_map::{set_shared, Side};
|
||||||
|
|
||||||
use crate::order::Strata;
|
use crate::order::Strata;
|
||||||
use crate::script::{event, Backdrop, Insets, Model};
|
use crate::script::{visibility, Backdrop, Insets, Model};
|
||||||
use crate::widget::FrameKind;
|
use crate::widget::FrameKind;
|
||||||
|
|
||||||
use super::{decode_id, draw_layer_from_str, frame_handle_of, frame_wrapper, strata_from_str};
|
use super::{decode_id, draw_layer_from_str, frame_handle_of, frame_wrapper, strata_from_str};
|
||||||
@@ -248,8 +248,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
// `SetParent` (`0x7a1550`): a frame, a name (`0x76c760`) or an explicit nil; an absent argument
|
// `SetParent` (`0x7a1550`): a frame, a name (`0x76c760`) or an explicit nil; an absent argument
|
||||||
// raises like an unknown name (`0x6f3400` gives -1), and so does a cycle (`0x87cb14`). As in
|
// raises like an unknown name (`0x6f3400` gives -1), and so does a cycle (`0x87cb14`). As in
|
||||||
// the reference, hide fires before show, between `reparent_begin` and `reparent_finish`,
|
// the reference, the hide transition runs between `reparent_begin` and `reparent_finish`, and
|
||||||
// since `fire_visibility_changes` reads each frame's live state for the direction.
|
// the show transition after.
|
||||||
set_shared(
|
set_shared(
|
||||||
lua,
|
lua,
|
||||||
m,
|
m,
|
||||||
@@ -330,24 +330,28 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let hidden = {
|
let was_visible = {
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||||
model.arena.reparent_begin(h, new_parent)
|
model.arena.reparent_begin(h, new_parent)
|
||||||
};
|
};
|
||||||
// None: the same parent, a total no-op (`0x76ab20`), not even a layout touch.
|
// None: the same parent, a total no-op (`0x76ab20`), not even a layout touch.
|
||||||
let Some(hidden) = hidden else { return Ok(()) };
|
let Some(was_visible) = was_visible else {
|
||||||
let was_visible = !hidden.is_empty();
|
return Ok(());
|
||||||
event::fire_visibility_changes(lua, hidden);
|
};
|
||||||
let shown = {
|
if was_visible {
|
||||||
|
visibility::hide(lua, h);
|
||||||
|
}
|
||||||
|
{
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||||
let shown = model.arena.reparent_finish(h, new_parent, was_visible);
|
model.arena.reparent_finish(h, new_parent);
|
||||||
// A reparent moves the subtree's effective scale, a layout input and part of every
|
// A reparent moves the subtree's effective scale, a layout input and part of every
|
||||||
// descendant FontString's measure key.
|
// descendant FontString's measure key.
|
||||||
model.touch_layout_reparent(h);
|
model.touch_layout_reparent(h);
|
||||||
model.touch_measure_all();
|
model.touch_measure_all();
|
||||||
shown
|
}
|
||||||
};
|
if was_visible {
|
||||||
event::fire_visibility_changes(lua, shown);
|
visibility::show(lua, h);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
@@ -784,11 +788,7 @@ fn strata_name(s: Strata) -> &'static str {
|
|||||||
|
|
||||||
fn set_shown(lua: &Lua, this: &Table, shown: bool) -> mlua::Result<()> {
|
fn set_shown(lua: &Lua, this: &Table, shown: bool) -> mlua::Result<()> {
|
||||||
let h = frame_handle_of(lua, this)?;
|
let h = frame_handle_of(lua, this)?;
|
||||||
let changed = {
|
visibility::set_shown(lua, h, shown);
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
|
||||||
model.arena.set_shown(h, shown)
|
|
||||||
};
|
|
||||||
event::fire_visibility_changes(lua, changed);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -430,3 +430,37 @@ fn an_empty_scroll_child_is_reported() {
|
|||||||
);
|
);
|
||||||
assert!(s.eval::<bool>("return Hollow ~= nil").unwrap());
|
assert!(s.eval::<bool>("return Hollow ~= nil").unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `CreateFrame` builds through the same instantiator as the XML, `0x6ee280` (called at
|
||||||
|
/// `0x70628b`), so its post-load hook `0x76a2f0` fires a visible frame's template OnShow once,
|
||||||
|
/// after its OnLoad (`0x76a3b3`); one created under a hidden parent gets none.
|
||||||
|
#[test]
|
||||||
|
fn a_runtime_template_fires_its_onshow_after_its_onload_when_visible() {
|
||||||
|
let s = script();
|
||||||
|
register(
|
||||||
|
&s,
|
||||||
|
r#"<Ui>
|
||||||
|
<Frame name="ShowProbeTemplate" virtual="true">
|
||||||
|
<Scripts>
|
||||||
|
<OnLoad>table.insert(LOG, this:GetName() .. ":load")</OnLoad>
|
||||||
|
<OnShow>table.insert(LOG, this:GetName() .. ":show")</OnShow>
|
||||||
|
</Scripts>
|
||||||
|
</Frame>
|
||||||
|
</Ui>"#,
|
||||||
|
);
|
||||||
|
s.run(
|
||||||
|
r#"
|
||||||
|
LOG = {}
|
||||||
|
CreateFrame("Frame", "Seen", nil, "ShowProbeTemplate")
|
||||||
|
local hidden = CreateFrame("Frame", "HiddenHost")
|
||||||
|
hidden:Hide()
|
||||||
|
CreateFrame("Frame", "Unseen", hidden, "ShowProbeTemplate")
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||||
|
"Seen:load,Seen:show,Unseen:load"
|
||||||
|
);
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,8 +92,9 @@ fn a_visible_reparent_refires_onhide_then_onshow() {
|
|||||||
let log: Vec<String> = s.eval("return log").unwrap();
|
let log: Vec<String> = s.eval("return log").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
log,
|
log,
|
||||||
vec!["hide:RTOld", "show:RTNew", "kidshow"],
|
vec!["hide:RTOld", "kidshow", "show:RTNew"],
|
||||||
"OnHide observes the OLD parent, OnShow the new, and the refire walks the subtree"
|
"OnHide observes the OLD parent, OnShow the new, and the refire walks the subtree, the \
|
||||||
|
child notified before its parent (`0x76aed5`, then `0x76aef5`)"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
s.eval::<i64>("return RTMover:GetFrameLevel()").unwrap(),
|
s.eval::<i64>("return RTMover:GetFrameLevel()").unwrap(),
|
||||||
|
|||||||
@@ -64,3 +64,151 @@ fn tick_runs_onupdate_only_when_effectively_visible() {
|
|||||||
s.tick(0.25); // visible again → fires
|
s.tick(0.25); // visible again → fires
|
||||||
assert_eq!(s.eval::<i64>("return ticks").unwrap(), 2);
|
assert_eq!(s.eval::<i64>("return ticks").unwrap(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The show cascade `0x76ae10` walks its children (`0x76aed5`) before its own notify
|
||||||
|
/// (`0x76aef5`), and the hide cascade `0x76ad50` likewise (`0x76adee`, then `0x76adfd`): both are
|
||||||
|
/// post-order, depth-first in child-link order.
|
||||||
|
#[test]
|
||||||
|
fn a_cascade_notifies_children_before_their_parent() {
|
||||||
|
let s = script();
|
||||||
|
s.run(
|
||||||
|
r#"
|
||||||
|
LOG = {}
|
||||||
|
local function log(tag) return function() table.insert(LOG, tag) end end
|
||||||
|
local p = CreateFrame("Frame", "PostP")
|
||||||
|
local c1 = CreateFrame("Frame", "PostC1", p)
|
||||||
|
local g = CreateFrame("Frame", "PostG", c1)
|
||||||
|
local c2 = CreateFrame("Frame", "PostC2", p)
|
||||||
|
for f, tag in pairs({ [p] = "p", [c1] = "c1", [g] = "g", [c2] = "c2" }) do
|
||||||
|
f:SetScript("OnShow", log(tag .. "+"))
|
||||||
|
f:SetScript("OnHide", log(tag .. "-"))
|
||||||
|
end
|
||||||
|
p:Hide()
|
||||||
|
p:Show()
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||||
|
"g-,c1-,c2-,p-,g+,c1+,c2+,p+"
|
||||||
|
);
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The child walk follows live links and each child's show re-checks its own gate when the walk
|
||||||
|
/// reaches it (`0x76ae1d`): a sibling an earlier sibling's OnShow hides gets no OnShow, and, never
|
||||||
|
/// having become visible, no OnHide either.
|
||||||
|
#[test]
|
||||||
|
fn a_sibling_hidden_by_an_earlier_siblings_onshow_gets_no_onshow() {
|
||||||
|
let s = script();
|
||||||
|
s.run(
|
||||||
|
r#"
|
||||||
|
LOG = {}
|
||||||
|
local p = CreateFrame("Frame", "LiveP")
|
||||||
|
local c1 = CreateFrame("Frame", "LiveC1", p)
|
||||||
|
local c2 = CreateFrame("Frame", "LiveC2", p)
|
||||||
|
p:Hide()
|
||||||
|
c1:SetScript("OnShow", function() table.insert(LOG, "c1+") LiveC2:Hide() end)
|
||||||
|
c2:SetScript("OnShow", function() table.insert(LOG, "c2+") end)
|
||||||
|
c2:SetScript("OnHide", function() table.insert(LOG, "c2-") end)
|
||||||
|
p:SetScript("OnShow", function() table.insert(LOG, "p+") end)
|
||||||
|
p:Show()
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||||
|
"c1+,p+"
|
||||||
|
);
|
||||||
|
assert!(!s.eval::<bool>("return LiveC2:IsVisible() == 1").unwrap());
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sibling an earlier sibling's OnHide hides runs its whole hide, OnHide included, nested in that
|
||||||
|
/// handler (`0x7758bb`); when the parent's walk reaches it, it is already invisible and the hide
|
||||||
|
/// gate (`0x76ad5b`) returns without a second OnHide. The parent's own OnHide comes last.
|
||||||
|
#[test]
|
||||||
|
fn a_sibling_hidden_by_an_earlier_siblings_onhide_is_notified_once_inside_it() {
|
||||||
|
let s = script();
|
||||||
|
s.run(
|
||||||
|
r#"
|
||||||
|
LOG = {}
|
||||||
|
local p = CreateFrame("Frame", "NestP")
|
||||||
|
local c1 = CreateFrame("Frame", "NestC1", p)
|
||||||
|
local c2 = CreateFrame("Frame", "NestC2", p)
|
||||||
|
c1:SetScript("OnHide", function()
|
||||||
|
table.insert(LOG, "c1-")
|
||||||
|
NestC2:Hide()
|
||||||
|
table.insert(LOG, "c1-end")
|
||||||
|
end)
|
||||||
|
c2:SetScript("OnHide", function() table.insert(LOG, "c2-") end)
|
||||||
|
p:SetScript("OnHide", function() table.insert(LOG, "p-") end)
|
||||||
|
p:Hide()
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||||
|
"c1-,c2-,c1-end,p-"
|
||||||
|
);
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A child's OnShow that hides the parent: the parent's hide cascade runs nested (the child's
|
||||||
|
/// OnHide, then the parent's), the next sibling then fails its parent gate (`0x76ae35`) and is
|
||||||
|
/// never shown, and the parent's own OnShow still fires last, since nothing between the walk and
|
||||||
|
/// `0x76aef5` re-checks.
|
||||||
|
#[test]
|
||||||
|
fn a_parent_hidden_by_a_childs_onshow_stops_the_walk_but_still_gets_its_onshow() {
|
||||||
|
let s = script();
|
||||||
|
s.run(
|
||||||
|
r#"
|
||||||
|
LOG = {}
|
||||||
|
local p = CreateFrame("Frame", "StopP")
|
||||||
|
local c1 = CreateFrame("Frame", "StopC1", p)
|
||||||
|
local c2 = CreateFrame("Frame", "StopC2", p)
|
||||||
|
p:Hide()
|
||||||
|
c1:SetScript("OnShow", function() table.insert(LOG, "c1+") StopP:Hide() end)
|
||||||
|
c1:SetScript("OnHide", function() table.insert(LOG, "c1-") end)
|
||||||
|
c2:SetScript("OnShow", function() table.insert(LOG, "c2+") end)
|
||||||
|
p:SetScript("OnShow", function() table.insert(LOG, "p+") end)
|
||||||
|
p:SetScript("OnHide", function() table.insert(LOG, "p-") end)
|
||||||
|
p:Show()
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.eval::<String>("return table.concat(LOG, ',')").unwrap(),
|
||||||
|
"c1+,c1-,p-,p+"
|
||||||
|
);
|
||||||
|
assert!(!s.eval::<bool>("return StopC2:IsVisible() == 1").unwrap());
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Lua `Show` writes the shown bit and dispatches the cascade unconditionally (`0x7757f1`,
|
||||||
|
/// `0x7757fb`), so a frame whose bit is already set but which is not visible (a `SetParent` from a
|
||||||
|
/// hidden parent to a shown one skips the show half, `0x76abe3`) becomes visible on `Show()`.
|
||||||
|
#[test]
|
||||||
|
fn show_on_a_shown_but_invisible_frame_makes_it_visible() {
|
||||||
|
let s = script();
|
||||||
|
s.run(
|
||||||
|
r#"
|
||||||
|
SHOWS = 0
|
||||||
|
local hidden = CreateFrame("Frame", "StaleHidden")
|
||||||
|
hidden:Hide()
|
||||||
|
local shown = CreateFrame("Frame", "StaleShown")
|
||||||
|
local f = CreateFrame("Frame", "Stale", hidden)
|
||||||
|
f:SetScript("OnShow", function() SHOWS = SHOWS + 1 end)
|
||||||
|
f:SetParent(shown)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
!s.eval::<bool>("return Stale:IsVisible() == 1").unwrap(),
|
||||||
|
"the reparent from an invisible parent runs no show half"
|
||||||
|
);
|
||||||
|
s.run("Stale:Show()").unwrap();
|
||||||
|
assert!(s.eval::<bool>("return Stale:IsVisible() == 1").unwrap());
|
||||||
|
assert_eq!(s.eval::<i64>("return SHOWS").unwrap(), 1);
|
||||||
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||||
|
}
|
||||||
|
|||||||
@@ -355,13 +355,9 @@ fn now(lua: &Lua) -> f64 {
|
|||||||
crate::script::clock::now(lua)
|
crate::script::clock::now(lua)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows or hides through the arena and fires the visibility events.
|
/// Shows or hides as the Lua verbs do, handlers included.
|
||||||
pub(super) fn set_shown(lua: &Lua, h: FrameHandle, shown: bool) {
|
pub(super) fn set_shown(lua: &Lua, h: FrameHandle, shown: bool) {
|
||||||
let changed = {
|
crate::script::visibility::set_shown(lua, h, shown);
|
||||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
|
||||||
model.arena.set_shown(h, shown)
|
|
||||||
};
|
|
||||||
event::fire_visibility_changes(lua, changed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default line colour, gold `0xffffd200` (stored at `0xc0d3e8` by `0x528e50`). The zone
|
/// The default line colour, gold `0xffffd200` (stored at `0xc0d3e8` by `0x528e50`). The zone
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
//! The show and hide transitions with their notifies (`0x76ae10`, `0x76ad50`). Each marks its
|
||||||
|
//! frame before anything runs, walks the live child list re-gating each child as the walk reaches
|
||||||
|
//! it, and fires its own notify last, so handlers run inside the walk, children before their
|
||||||
|
//! parent, and a frame an earlier handler hid is never notified.
|
||||||
|
|
||||||
|
use mlua::{Lua, Table, Value};
|
||||||
|
|
||||||
|
use super::{event, Model};
|
||||||
|
use crate::widget::{ButtonState, ChildWalk, FrameHandle};
|
||||||
|
|
||||||
|
/// The Lua `Show`/`Hide` (`0x775750`/`0x775810`): write the shown bit (`0x7757f1`/`0x7758b1`),
|
||||||
|
/// then dispatch the transition (`0x7757fb`/`0x7758bb`), which gates itself, so `Show()` on a
|
||||||
|
/// frame already shown but not visible still runs.
|
||||||
|
pub(super) fn set_shown(lua: &Lua, h: FrameHandle, shown: bool) {
|
||||||
|
match model_mut(lua).arena.frame_mut(h) {
|
||||||
|
Some(f) => f.shown = shown,
|
||||||
|
None => return,
|
||||||
|
}
|
||||||
|
if shown {
|
||||||
|
show(lua, h);
|
||||||
|
} else {
|
||||||
|
hide(lua, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The show transition (`0x76ae10`, slot `+0x88`): the frame's own step, its children's
|
||||||
|
/// transitions in link order, the toplevel raise (`0x76aeec`), then its own OnShow (`0x76aef5`).
|
||||||
|
pub(super) fn show(lua: &Lua, h: FrameHandle) {
|
||||||
|
{
|
||||||
|
let mut model = model_mut(lua);
|
||||||
|
if !model.arena.show_step(h) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Entering the mouse index schedules a hover re-pick (`0x764b8d`).
|
||||||
|
model.hover_repick = true;
|
||||||
|
}
|
||||||
|
walk_children(lua, h, show);
|
||||||
|
super::object::toplevel::raise_on_show(&mut model_mut(lua), h);
|
||||||
|
notify(lua, h, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hide transition (`0x76ad50`, slot `+0x84`): the frame's own step with its hover leave,
|
||||||
|
/// its children's transitions in link order, then its own OnHide (`0x76adfd`).
|
||||||
|
pub(super) fn hide(lua: &Lua, h: FrameHandle) {
|
||||||
|
let left = {
|
||||||
|
let mut model = model_mut(lua);
|
||||||
|
if !model.arena.hide_step(h) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
leave_if_hovered(&mut model, h)
|
||||||
|
};
|
||||||
|
if let Some(id) = left {
|
||||||
|
let fired = event::fire_widget_handler(lua, id, "OnLeave", vec![Value::Boolean(true)]);
|
||||||
|
record(lua, fired);
|
||||||
|
}
|
||||||
|
walk_children(lua, h, hide);
|
||||||
|
// `CSimpleButton`'s hide notify (`+0x34`, `0x7791e0`) un-presses it, unless disabled or
|
||||||
|
// locked, before the base notify, so a button hidden while held comes back unpushed.
|
||||||
|
super::button::edge(&mut model_mut(lua), h, ButtonState::on_hide);
|
||||||
|
notify(lua, h, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `step` on each child of `parent` as a live walk reaches it
|
||||||
|
/// ([`crate::widget::WidgetArena::next_child`]), with no borrow held across the step.
|
||||||
|
fn walk_children(lua: &Lua, parent: FrameHandle, step: fn(&Lua, FrameHandle)) {
|
||||||
|
let mut walk = ChildWalk::default();
|
||||||
|
loop {
|
||||||
|
let next = lua
|
||||||
|
.app_data_ref::<Model>()
|
||||||
|
.expect("model app_data")
|
||||||
|
.arena
|
||||||
|
.next_child(parent, &mut walk);
|
||||||
|
let Some(child) = next else { break };
|
||||||
|
step(lua, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hide's de-registration (`0x764920`, at `0x76ad9d`, before the child walk) ends a hover on
|
||||||
|
/// this frame: the removal tail (`0x764ba0`) clears the hover, schedules a re-pick (`0x764cbb`)
|
||||||
|
/// and calls the leave notify with 1 (`0x764cce`), whose base `0x76b6f0` clears the drag-arm
|
||||||
|
/// (`+0x100`/`+0x104`), so the hovered frame's OnLeave runs before its own OnHide and its
|
||||||
|
/// ancestors'.
|
||||||
|
/// The leave is a virtual call: a disabled Button's own `0x7794e0` skips its OnLeave. This leave is
|
||||||
|
/// how a tooltip closes with its window; the reference has no other. The id whose OnLeave fires.
|
||||||
|
fn leave_if_hovered(model: &mut Model, h: FrameHandle) -> Option<u32> {
|
||||||
|
if model.mouseover != Some(h) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let notified = super::button::hover_notify_runs(model, Some(h));
|
||||||
|
model.mouseover = None;
|
||||||
|
if model.drag.as_ref().is_some_and(|d| d.source == h) {
|
||||||
|
model.drag = None;
|
||||||
|
}
|
||||||
|
model.hover_repick = true;
|
||||||
|
notified.then(|| model.frame_id(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The notify slot (`+0x30` OnShow, `+0x34` OnHide): the base fires the script unless the frame is
|
||||||
|
/// in its load window (`0x76b260`/`0x76b290`), and the EditBox's overrides then take or drop the
|
||||||
|
/// focus (`0x77a750`/`0x77a780`, base first).
|
||||||
|
fn notify(lua: &Lua, h: FrameHandle, visible: bool) {
|
||||||
|
let id = {
|
||||||
|
let mut model = model_mut(lua);
|
||||||
|
match model.arena.frame(h) {
|
||||||
|
Some(f) if f.load_window => None,
|
||||||
|
Some(_) => Some(model.frame_id(h)),
|
||||||
|
None => return,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(id) = id {
|
||||||
|
let script = if visible { "OnShow" } else { "OnHide" };
|
||||||
|
let fired = event::fire_widget_handler(lua, id, script, Vec::new());
|
||||||
|
record(lua, fired);
|
||||||
|
}
|
||||||
|
super::editbox::visibility_focus(lua, h, visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open or close a frame's load window: the pre-load hook's `+0x114 = 1` (`0x7697c2`) and the
|
||||||
|
/// post-load hook's first write, `+0x114 = 0` (`0x76a2ff`).
|
||||||
|
pub(crate) fn set_load_window(lua: &Lua, wrapper: &Table, open: bool) {
|
||||||
|
let Ok(h) = super::object::frame_handle_of(lua, wrapper) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(f) = model_mut(lua).arena.frame_mut(h) {
|
||||||
|
f.load_window = open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The post-load hook's OnShow (`0x76a38c`–`0x76a3b3`), after the children and the OnLoad: the
|
||||||
|
/// script alone, through the runner and not the notify slot, so no raise and no EditBox focus,
|
||||||
|
/// when the frame is visible, has the handler and is out of its load window. It has no OnHide
|
||||||
|
/// counterpart, and it fires whatever OnShows the frame already had.
|
||||||
|
pub(crate) fn post_load_show(lua: &Lua, wrapper: &Table) {
|
||||||
|
let Ok(h) = super::object::frame_handle_of(lua, wrapper) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let id = {
|
||||||
|
let mut model = model_mut(lua);
|
||||||
|
match model.arena.frame(h) {
|
||||||
|
Some(f) if f.effective_visible && !f.load_window => model.frame_id(h),
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let fired = event::fire_widget_handler(lua, id, "OnShow", Vec::new());
|
||||||
|
record(lua, fired);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn model_mut(lua: &Lua) -> mlua::AppDataRefMut<'_, Model> {
|
||||||
|
lua.app_data_mut::<Model>().expect("model app_data")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handler error is recorded in [`Model::errors`], never propagated to the verb.
|
||||||
|
fn record(lua: &Lua, fired: mlua::Result<()>) {
|
||||||
|
if let Err(e) = fired {
|
||||||
|
model_mut(lua).record_script_error(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,10 @@ pub struct Frame {
|
|||||||
pub shown: bool,
|
pub shown: bool,
|
||||||
/// `shown` and every ancestor effectively visible (`+0xd4`).
|
/// `shown` and every ancestor effectively visible (`+0xd4`).
|
||||||
pub effective_visible: bool,
|
pub effective_visible: bool,
|
||||||
|
/// The frame's own load window (`+0x114`): set by the pre-load hook (`0x7697c2`), cleared as
|
||||||
|
/// the post-load hook starts (`0x76a2ff`). While it is set the frame's own OnShow and OnHide
|
||||||
|
/// fire nothing (`0x76b270`/`0x76b2a0`); it still transitions and walks its children.
|
||||||
|
pub load_window: bool,
|
||||||
/// `EnableMouse`: the mouse bit (bit 2) of the input mask `[frame+0xcc]`.
|
/// `EnableMouse`: the mouse bit (bit 2) of the input mask `[frame+0xcc]`.
|
||||||
pub mouse_enabled: bool,
|
pub mouse_enabled: bool,
|
||||||
/// `EnableMouseWheel`, or an `<OnMouseWheel>` script at XML load: the mask's wheel bit (3).
|
/// `EnableMouseWheel`, or an `<OnMouseWheel>` script at XML load: the mask's wheel bit (3).
|
||||||
@@ -228,6 +232,14 @@ pub struct Region {
|
|||||||
pub detached: bool,
|
pub detached: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cursor over a frame's child list for [`WidgetArena::next_child`]: the child last returned and
|
||||||
|
/// where it stood, so a walk that a handler interrupts resumes from the list as it is now.
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct ChildWalk {
|
||||||
|
at: usize,
|
||||||
|
last: Option<FrameHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
// ── The arena ────────────────────────────────────────────────────────────────────────────────
|
// ── The arena ────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// The frame arena: frames, regions, the name registry and the propagation mutators. The screen
|
/// The frame arena: frames, regions, the name registry and the propagation mutators. The screen
|
||||||
@@ -424,6 +436,7 @@ impl WidgetArena {
|
|||||||
effective_alpha,
|
effective_alpha,
|
||||||
shown,
|
shown,
|
||||||
effective_visible: shown && parent_visible,
|
effective_visible: shown && parent_visible,
|
||||||
|
load_window: false,
|
||||||
mouse_enabled: mouse_enabled_by_ctor(kind),
|
mouse_enabled: mouse_enabled_by_ctor(kind),
|
||||||
// The WorldFrame ctor takes the wheel (`0x481b1f`); the reference's ScrollFrame and
|
// The WorldFrame ctor takes the wheel (`0x481b1f`); the reference's ScrollFrame and
|
||||||
// ScrollingMessageFrame ctors do not, and a stock one takes it from `<OnMouseWheel>`.
|
// ScrollingMessageFrame ctors do not, and a stock one takes it from `<OnMouseWheel>`.
|
||||||
|
|||||||
@@ -2,51 +2,88 @@
|
|||||||
|
|
||||||
use crate::order::Strata;
|
use crate::order::Strata;
|
||||||
|
|
||||||
use super::{FrameHandle, RegionHandle, WidgetArena, SCALE_EPS};
|
use super::{ChildWalk, FrameHandle, RegionHandle, WidgetArena, SCALE_EPS};
|
||||||
|
|
||||||
impl WidgetArena {
|
impl WidgetArena {
|
||||||
// ── Visibility ───────────────────────────────────────────────────────────────────────────────
|
// ── Visibility ───────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Set `shown` and propagate effective visibility down the subtree (show `0x76ae10`, hide
|
/// The show transition's own step (`0x76ae10` up to its child walk): nothing unless the frame
|
||||||
/// `0x76ad50`), returning the frames that changed, a node before its descendants, for the
|
/// is shown (`0x76ae1d`), its parent is visible or absent (`0x76ae35`) and it is not visible
|
||||||
/// caller to fire `OnShow`/`OnHide`. The reference notifies post-order (`0x76aef5`) and
|
/// yet (`0x76ae43`); otherwise it marks itself visible (`0x76ae7b`), before any child or
|
||||||
/// re-reads live links, so a frame a handler hides mid-cascade fails its gate (`0x76ae1d`) and
|
/// handler runs, and relinks at its bucket's tail (`0x764780`). Whether it transitioned.
|
||||||
/// is never notified; this list is a snapshot that still holds it.
|
pub fn show_step(&mut self, h: FrameHandle) -> bool {
|
||||||
|
match self.frame(h) {
|
||||||
|
Some(f) if f.shown && !f.effective_visible => {}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
if !self.parent_visible(h) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.frame_mut(h).unwrap().effective_visible = true;
|
||||||
|
self.resequence_to_tail(h);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hide transition's own step (`0x76ad50` up to its child walk): nothing unless the frame
|
||||||
|
/// is visible (`0x76ad5b`); otherwise it marks itself invisible (`0x76ad93`). The shown bit is
|
||||||
|
/// not consulted. Whether it transitioned.
|
||||||
|
pub fn hide_step(&mut self, h: FrameHandle) -> bool {
|
||||||
|
match self.frame_mut(h) {
|
||||||
|
Some(f) if f.effective_visible => {
|
||||||
|
f.effective_visible = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next child of `parent` in a live walk of its child list (`0x76aec7`, `0x76ade0`). The
|
||||||
|
/// reference reads the next link from the current node after the call returns, so a child a
|
||||||
|
/// handler adds or removes later in the list is seen. A walked child that was itself moved
|
||||||
|
/// out leaves the walk at the child now in its place; the reference reads a stale link there.
|
||||||
|
pub fn next_child(&self, parent: FrameHandle, walk: &mut ChildWalk) -> Option<FrameHandle> {
|
||||||
|
let children = &self.frame(parent)?.children;
|
||||||
|
let at = match walk.last {
|
||||||
|
None => 0,
|
||||||
|
Some(last) if children.get(walk.at) == Some(&last) => walk.at + 1,
|
||||||
|
Some(last) => children
|
||||||
|
.iter()
|
||||||
|
.position(|&c| c == last)
|
||||||
|
.map_or(walk.at, |i| i + 1),
|
||||||
|
};
|
||||||
|
let next = *children.get(at)?;
|
||||||
|
walk.at = at;
|
||||||
|
walk.last = Some(next);
|
||||||
|
Some(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Lua `Show`/`Hide` (`0x7757f1`/`0x7757fb`, `0x7758b1`/`0x7758bb`) with no handler to
|
||||||
|
/// fire: write the shown bit, then run the transition, which gates itself. Returns the frames
|
||||||
|
/// that changed in the order their notifies run, children before their parent. A caller with
|
||||||
|
/// handlers walks with [`Self::show_step`], [`Self::hide_step`] and [`Self::next_child`].
|
||||||
pub fn set_shown(&mut self, h: FrameHandle, shown: bool) -> Vec<FrameHandle> {
|
pub fn set_shown(&mut self, h: FrameHandle, shown: bool) -> Vec<FrameHandle> {
|
||||||
let mut changed = Vec::new();
|
let mut changed = Vec::new();
|
||||||
match self.frame_mut(h) {
|
if let Some(f) = self.frame_mut(h) {
|
||||||
Some(f) if f.shown != shown => f.shown = shown,
|
f.shown = shown;
|
||||||
_ => return changed,
|
self.cascade_silent(h, shown, &mut changed);
|
||||||
}
|
}
|
||||||
let parent_visible = self.parent_visible(h);
|
|
||||||
self.propagate_visible(h, parent_visible, &mut changed);
|
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn propagate_visible(
|
fn cascade_silent(&mut self, h: FrameHandle, show: bool, changed: &mut Vec<FrameHandle>) {
|
||||||
&mut self,
|
let stepped = if show {
|
||||||
h: FrameHandle,
|
self.show_step(h)
|
||||||
parent_visible: bool,
|
} else {
|
||||||
changed: &mut Vec<FrameHandle>,
|
self.hide_step(h)
|
||||||
) {
|
|
||||||
let (shown, old_ev, children) = {
|
|
||||||
let f = self.frame(h).expect("live node in propagation");
|
|
||||||
(f.shown, f.effective_visible, f.children.clone())
|
|
||||||
};
|
};
|
||||||
let new_ev = shown && parent_visible;
|
if !stepped {
|
||||||
if new_ev == old_ev {
|
|
||||||
// No change here means none below: prune, as the client's recursion does.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.frame_mut(h).unwrap().effective_visible = new_ev;
|
let mut walk = ChildWalk::default();
|
||||||
if new_ev {
|
while let Some(c) = self.next_child(h, &mut walk) {
|
||||||
// Re-added at its bucket's tail, as the client does, and each descendant after it.
|
self.cascade_silent(c, show, changed);
|
||||||
self.resequence_to_tail(h);
|
|
||||||
}
|
}
|
||||||
changed.push(h);
|
changed.push(h);
|
||||||
for c in children {
|
|
||||||
self.propagate_visible(c, new_ev, changed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Strata ───────────────────────────────────────────────────────────────────────────────────
|
// ── Strata ───────────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -202,18 +239,20 @@ impl WidgetArena {
|
|||||||
|
|
||||||
// ── Reparenting ──────────────────────────────────────────────────────────────────────────────
|
// ── Reparenting ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Phase 1 of `SetParent` (`0x76ab10`): the guards and the hide half. `None` when the parent
|
/// Phase 1 of `SetParent` (`0x76ab10`): the guards. `None` when the parent is unchanged
|
||||||
/// is unchanged (`0x76ab20` skips everything), the frame is dead, or the move would cycle (the
|
/// (`0x76ab20` skips everything), the frame is dead, or the move would cycle (the Lua binding
|
||||||
/// Lua binding raises first, `0x7a177f`); otherwise the frames that lost effective visibility,
|
/// raises first, `0x7a177f`); otherwise whether the frame is visible (`0x76ab2b`), in which
|
||||||
/// in pre-order, whose `OnHide` runs under the old parent (`0x76ab41`).
|
/// case the caller runs its hide transition under the old parent (`0x76ab41`) before
|
||||||
|
/// [`Self::reparent_finish`], and its show transition after it (`0x76abfd`).
|
||||||
///
|
///
|
||||||
/// Deviation: the frame is still in the old parent's child list during `OnHide`, unlike the
|
/// Deviation: the frame is still in the old parent's child list during `OnHide`, unlike the
|
||||||
/// reference, because a handler that itself reparents must not find the lists half-spliced.
|
/// reference (`0x76ab34` unlinks first), because a handler that itself reparents must not find
|
||||||
|
/// the lists half-spliced.
|
||||||
pub fn reparent_begin(
|
pub fn reparent_begin(
|
||||||
&mut self,
|
&mut self,
|
||||||
h: FrameHandle,
|
h: FrameHandle,
|
||||||
new_parent: Option<FrameHandle>,
|
new_parent: Option<FrameHandle>,
|
||||||
) -> Option<Vec<FrameHandle>> {
|
) -> Option<bool> {
|
||||||
self.frame(h)?;
|
self.frame(h)?;
|
||||||
let new_parent = new_parent.filter(|&p| self.frame(p).is_some());
|
let new_parent = new_parent.filter(|&p| self.frame(p).is_some());
|
||||||
if let Some(np) = new_parent {
|
if let Some(np) = new_parent {
|
||||||
@@ -221,32 +260,22 @@ impl WidgetArena {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.frame(h).unwrap().parent == new_parent {
|
let f = self.frame(h).unwrap();
|
||||||
|
if f.parent == new_parent {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut hidden = Vec::new();
|
Some(f.effective_visible)
|
||||||
if self.frame(h).unwrap().effective_visible {
|
|
||||||
self.propagate_visible(h, false, &mut hidden);
|
|
||||||
}
|
|
||||||
Some(hidden)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 2 of `SetParent` (`0x76ab10`): relink, strata := the parent's (`0x76ab5a`, MEDIUM for
|
/// Phase 2 of `SetParent` (`0x76ab10`): relink, strata := the parent's (`0x76ab5a`, MEDIUM for
|
||||||
/// none), level := the parent's + 1 without propagating (`0x76ab65`, 0 for none), scale
|
/// none), level := the parent's + 1 without propagating (`0x76ab65`, 0 for none), scale
|
||||||
/// re-inherited, then the show half, returning the frames that became visible; alpha is
|
/// re-inherited; alpha is untouched. `h`'s own children keep their levels, so one can end up
|
||||||
/// untouched. `h`'s own children keep their levels, so one can end up below `h`.
|
/// below `h`. The show half is the caller's, and runs only when phase 1 found the frame
|
||||||
/// `was_visible` is whether phase 1 hid anything (the reference's `ebx` at `0x76ab2b`); the
|
/// visible (`0x76abe3`), so a hidden frame moved under a visible parent keeps a stale
|
||||||
/// show half runs only then (`0x76abfd`), so a hidden frame moved under a visible parent keeps
|
/// effective visibility until something shows it.
|
||||||
/// a stale effective visibility until something shows it.
|
pub fn reparent_finish(&mut self, h: FrameHandle, new_parent: Option<FrameHandle>) {
|
||||||
pub fn reparent_finish(
|
|
||||||
&mut self,
|
|
||||||
h: FrameHandle,
|
|
||||||
new_parent: Option<FrameHandle>,
|
|
||||||
was_visible: bool,
|
|
||||||
) -> Vec<FrameHandle> {
|
|
||||||
let mut shown = Vec::new();
|
|
||||||
if self.frame(h).is_none() {
|
if self.frame(h).is_none() {
|
||||||
return shown; // died inside an OnHide: nothing to move
|
return; // died inside an OnHide: nothing to move
|
||||||
}
|
}
|
||||||
let new_parent = new_parent.filter(|&p| self.frame(p).is_some());
|
let new_parent = new_parent.filter(|&p| self.frame(p).is_some());
|
||||||
// Relink from the current parent: an `OnHide` may have reparented it, and the reference's
|
// Relink from the current parent: an `OnHide` may have reparented it, and the reference's
|
||||||
@@ -274,25 +303,27 @@ impl WidgetArena {
|
|||||||
self.set_frame_level(h, plevel, false);
|
self.set_frame_level(h, plevel, false);
|
||||||
let ps = self.parent_effective_scale(h);
|
let ps = self.parent_effective_scale(h);
|
||||||
self.propagate_scale(h, ps);
|
self.propagate_scale(h, ps);
|
||||||
|
|
||||||
if was_visible && self.parent_visible(h) {
|
|
||||||
self.propagate_visible(h, true, &mut shown);
|
|
||||||
}
|
|
||||||
shown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Both reparent phases in one call, hide changes first, for a caller with no `OnHide` to fire.
|
/// `SetParent` whole, for a caller with no handler to fire: the frames that lost visibility,
|
||||||
|
/// then those that gained it, each half in notify order.
|
||||||
pub fn set_parent(
|
pub fn set_parent(
|
||||||
&mut self,
|
&mut self,
|
||||||
h: FrameHandle,
|
h: FrameHandle,
|
||||||
new_parent: Option<FrameHandle>,
|
new_parent: Option<FrameHandle>,
|
||||||
) -> Vec<FrameHandle> {
|
) -> Vec<FrameHandle> {
|
||||||
let Some(hidden) = self.reparent_begin(h, new_parent) else {
|
let Some(was_visible) = self.reparent_begin(h, new_parent) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let was_visible = !hidden.is_empty();
|
let mut changed = Vec::new();
|
||||||
let shown = self.reparent_finish(h, new_parent, was_visible);
|
if was_visible {
|
||||||
[hidden, shown].concat()
|
self.cascade_silent(h, false, &mut changed);
|
||||||
|
}
|
||||||
|
self.reparent_finish(h, new_parent);
|
||||||
|
if was_visible {
|
||||||
|
self.cascade_silent(h, true, &mut changed);
|
||||||
|
}
|
||||||
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Region:SetParent` for a texture or font string; `None` detaches. The client re-links in
|
/// `Region:SetParent` for a texture or font string; `None` detaches. The client re-links in
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ fn mid_tree_hide_blocks_a_shown_grandchild() {
|
|||||||
assert!(a.frame(grand).unwrap().effective_visible);
|
assert!(a.frame(grand).unwrap().effective_visible);
|
||||||
|
|
||||||
let changed = a.set_shown(child, false);
|
let changed = a.set_shown(child, false);
|
||||||
assert_eq!(changed, vec![child, grand]); // pre-order
|
// Notify order: post-order, children before their parent (`0x76adee`, then `0x76adfd`).
|
||||||
|
assert_eq!(changed, vec![grand, child]);
|
||||||
assert!(a.frame(root).unwrap().effective_visible);
|
assert!(a.frame(root).unwrap().effective_visible);
|
||||||
assert!(!a.frame(child).unwrap().effective_visible);
|
assert!(!a.frame(child).unwrap().effective_visible);
|
||||||
assert!(!a.frame(grand).unwrap().effective_visible);
|
assert!(!a.frame(grand).unwrap().effective_visible);
|
||||||
@@ -44,7 +45,7 @@ fn mid_tree_hide_blocks_a_shown_grandchild() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let changed = a.set_shown(child, true);
|
let changed = a.set_shown(child, true);
|
||||||
assert_eq!(changed, vec![child, grand]);
|
assert_eq!(changed, vec![grand, child]);
|
||||||
assert!(a.frame(grand).unwrap().effective_visible);
|
assert!(a.frame(grand).unwrap().effective_visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +59,10 @@ fn hidden_grandchild_stays_hidden_when_ancestor_reshows() {
|
|||||||
a.set_shown(grand, false);
|
a.set_shown(grand, false);
|
||||||
let changed = a.set_shown(root, false);
|
let changed = a.set_shown(root, false);
|
||||||
// grand is already invisible, so the hide does not report it.
|
// grand is already invisible, so the hide does not report it.
|
||||||
assert_eq!(changed, vec![root, child]);
|
assert_eq!(changed, vec![child, root]);
|
||||||
|
|
||||||
let changed = a.set_shown(root, true);
|
let changed = a.set_shown(root, true);
|
||||||
assert_eq!(changed, vec![root, child]);
|
assert_eq!(changed, vec![child, root]);
|
||||||
assert!(a.frame(child).unwrap().effective_visible);
|
assert!(a.frame(child).unwrap().effective_visible);
|
||||||
assert!(!a.frame(grand).unwrap().effective_visible);
|
assert!(!a.frame(grand).unwrap().effective_visible);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user