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:
Sam Sung
2026-10-09 13:44:15 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent dc63a06178
commit c2fc3cf304
18 changed files with 633 additions and 204 deletions
-70
View File
@@ -10,7 +10,6 @@ use mlua::{Function, Lua, Table, Value};
use super::{Model, ScriptValue, REG_SCRIPTS};
use crate::script::object::frame_wrapper;
use crate::widget::{ButtonState, FrameHandle};
/// `UiScript::fire_event` for engine code that holds only the Lua context.
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
/// 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 {
+1
View File
@@ -138,6 +138,7 @@ mod trainer;
mod types;
mod unit;
mod video_pairs;
pub(crate) mod visibility;
mod weapon_enchant;
mod who_sort;
mod worldmap;
+19 -16
View File
@@ -154,11 +154,12 @@ pub struct NamePlateClick {
pub button: String,
}
/// What a sync fires outside the model borrow: `OnShow`/`OnHide` for frames whose visibility
/// flipped and `OnValueChanged` for moved bars (pfUI hooks it, `nameplates.lua:393`).
/// What a sync runs outside the model borrow: the plates' `Show`/`Hide`, whose transitions fire
/// handlers, and `OnValueChanged` for moved bars (pfUI hooks it, `nameplates.lua:393`).
#[derive(Default)]
struct SyncEffects {
visibility: Vec<FrameHandle>,
/// `(plate, shown)` in the order the sync issued them.
visibility: Vec<(FrameHandle, bool)>,
values: Vec<(FrameHandle, f32)>,
}
@@ -247,7 +248,7 @@ impl UiScript {
};
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 {
super::statusbar::fire_engine_value_changed(lua, bar, value);
}
@@ -304,7 +305,7 @@ impl UiScript {
}
effects
};
super::event::fire_visibility_changes(lua, effects.visibility);
apply_visibility(lua, effects.visibility);
}
/// 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.
pub(super) fn note_click(lua: &mlua::Lua, id: u32, button: &str) {
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())
.find(|i| model.nameplates.plates[*i].last.is_none());
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.len() - 1
});
@@ -396,7 +404,7 @@ fn sync(
impl Plate {
/// 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.
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
// `WorldFrame` child.
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()`.
effects
.visibility
.extend(model.arena.set_shown(frame, false));
// Silent, since nothing can have set a handler on a frame not yet handed out.
model.arena.set_shown(frame, false);
model
.nameplates
@@ -625,9 +632,7 @@ impl Plate {
}
if last.is_none() {
effects
.visibility
.extend(model.arena.set_shown(frame, true));
effects.visibility.push((frame, true));
}
if last.as_ref().is_none_or(|l| l.alpha != state.alpha) {
model.arena.set_alpha(frame, state.alpha);
@@ -722,9 +727,7 @@ impl Plate {
return;
}
let frame = model.nameplates.plates[i].frame;
effects
.visibility
.extend(model.arena.set_shown(frame, false));
effects.visibility.push((frame, false));
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::order::Strata;
use crate::script::{event, Backdrop, Insets, Model};
use crate::script::{visibility, Backdrop, Insets, Model};
use crate::widget::FrameKind;
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
// 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`,
// since `fire_visibility_changes` reads each frame's live state for the direction.
// the reference, the hide transition runs between `reparent_begin` and `reparent_finish`, and
// the show transition after.
set_shared(
lua,
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");
model.arena.reparent_begin(h, new_parent)
};
// None: the same parent, a total no-op (`0x76ab20`), not even a layout touch.
let Some(hidden) = hidden else { return Ok(()) };
let was_visible = !hidden.is_empty();
event::fire_visibility_changes(lua, hidden);
let shown = {
let Some(was_visible) = was_visible else {
return Ok(());
};
if was_visible {
visibility::hide(lua, h);
}
{
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
// descendant FontString's measure key.
model.touch_layout_reparent(h);
model.touch_measure_all();
shown
};
event::fire_visibility_changes(lua, shown);
}
if was_visible {
visibility::show(lua, h);
}
Ok(())
},
)?;
@@ -784,11 +788,7 @@ fn strata_name(s: Strata) -> &'static str {
fn set_shown(lua: &Lua, this: &Table, shown: bool) -> mlua::Result<()> {
let h = frame_handle_of(lua, this)?;
let changed = {
let mut model = lua.app_data_mut::<Model>().expect("model");
model.arena.set_shown(h, shown)
};
event::fire_visibility_changes(lua, changed);
visibility::set_shown(lua, h, shown);
Ok(())
}
@@ -430,3 +430,37 @@ fn an_empty_scroll_child_is_reported() {
);
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();
assert_eq!(
log,
vec!["hide:RTOld", "show:RTNew", "kidshow"],
"OnHide observes the OLD parent, OnShow the new, and the refire walks the subtree"
vec!["hide:RTOld", "kidshow", "show:RTNew"],
"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!(
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
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());
}
+2 -6
View File
@@ -355,13 +355,9 @@ fn now(lua: &Lua) -> f64 {
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) {
let changed = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.arena.set_shown(h, shown)
};
event::fire_visibility_changes(lua, changed);
crate::script::visibility::set_shown(lua, h, shown);
}
/// The default line colour, gold `0xffffd200` (stored at `0xc0d3e8` by `0x528e50`). The zone
+157
View File
@@ -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());
}
}