diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 7f435f33..9b425483 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -110,7 +110,8 @@ BenillaOptionsFrame:SetScale(k) 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) tab:SetWidth(getglobal(tab:GetName() .. "Text"):GetStringWidth() + 40) end @@ -1341,9 +1342,6 @@ - - BenillaOptionsTab_FitWidth(this) - @@ -3068,6 +3065,8 @@ BenillaOptionsFrame_UpdateScale() + BenillaOptionsTab_FitWidth(BenillaOptionsFrameGameTab) + BenillaOptionsTab_FitWidth(BenillaOptionsFrameAddOnsTab) BenillaOptionsFrame_SelectCategory(this.selectedCategory or "Controls") if UpdateMicroButtons then UpdateMicroButtons() end diff --git a/crates/benilla-app/src/ui_script/inspect_tests.rs b/crates/benilla-app/src/ui_script/inspect_tests.rs index 0dfc3175..dcc59312 100644 --- a/crates/benilla-app/src/ui_script/inspect_tests.rs +++ b/crates/benilla-app/src/ui_script/inspect_tests.rs @@ -492,16 +492,19 @@ fn toggle_inspect_reopens_after_a_close_and_raises_like_the_reference() { s.eval::("return InspectFrame:IsVisible()").unwrap(), "the window still shows — Show() runs before OnShow raises" ); - // Two raises: `InspectFrame` and `InspectPaperDollFrame` each run their own `OnShow` under - // their own pcall, the first dying at the portrait's gate, the second at `UnitLevel`'s. + // Two raises: `InspectPaperDollFrame` and `InspectFrame` each run their own `OnShow` under + // 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(); assert_eq!(errors.len(), 2, "one raise per OnShow handler: {errors:?}"); assert!( - errors[0].contains(r#"Usage: SetPortraitTexture(texture, "unit")"#), - "the portrait's gate, before the name line is ever reached: {errors:?}" + errors[0].contains(r#"Usage: UnitLevel("unit")"#), + "the paper doll's own handler, dying at its first getter: {errors:?}" ); assert!( - errors[1].contains(r#"Usage: UnitLevel("unit")"#), - "and the paper doll's own handler, dying at its first getter: {errors:?}" + errors[1].contains(r#"Usage: SetPortraitTexture(texture, "unit")"#), + "then the window's, at the portrait's gate, before the name line is ever reached: \ + {errors:?}" ); } diff --git a/crates/benilla-app/src/ui_script/test_ui.rs b/crates/benilla-app/src/ui_script/test_ui.rs index 00ab9176..89be697e 100644 --- a/crates/benilla-app/src/ui_script/test_ui.rs +++ b/crates/benilla-app/src/ui_script/test_ui.rs @@ -660,6 +660,8 @@ pub(super) const BAG_UI: &[&str] = &[ "Interface\\FrameXML\\UIDropDownMenu.xml", r"Interface\FrameXML\UIPanelTemplates.lua", 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. r"Interface\FrameXML\StaticPopup.xml", // `ContainerFrameItemButton_OnClick` hides `StackSplitFrame` on every plain click @@ -683,7 +685,9 @@ pub(super) const BAG_UI: &[&str] = &[ "Interface\\FrameXML\\ItemButtonTemplate.xml", // `BagSlotButtonTemplate` inherits `PaperDollItemSlotButtonTemplate`, and its OnLoad // (`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", // The stock bag bar, with `BagSlotButtonTemplate` and `KEYRING_CONTAINER`. "Interface\\FrameXML\\MainMenuBarBagButtons.xml", diff --git a/crates/benilla-app/tests/auction_frame.rs b/crates/benilla-app/tests/auction_frame.rs index c99ab5ec..2959a52d 100644 --- a/crates/benilla-app/tests/auction_frame.rs +++ b/crates/benilla-app/tests/auction_frame.rs @@ -374,10 +374,9 @@ fn only_the_bids_pane_gets_its_page_without_being_opened() { assert!(s.take_errors().is_empty()); } -/// The reference's show cascade `0x76ae10` is post-order: a frame marks itself visible -/// (`0x76ae7b`), walks its children re-reading live links, and fires its own `OnShow` last -/// (`0x76aef5`). benilla's is pre-order, a gap in the engine; for this window both orders reach -/// the same state, so only the `ShowOrder` assertions are benilla's order. +/// The show cascade `0x76ae10` is post-order: a frame marks itself visible (`0x76ae7b`), walks its +/// children re-reading live links, and fires its own `OnShow` last (`0x76aef5`), so the pane the +/// XML leaves shown is notified before the window. #[test] fn the_show_cascade_notifies_the_bids_pane_once_and_keeps_its_page() { 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![]); - assert_eq!( - order(&s), - "window,pane", - "OURS, and a known deviation: the reference's cascade is post-order, \"pane,window\"" - ); + assert_eq!(order(&s), "pane,window", "children first, the window last"); assert_eq!( page(&s), "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![]); assert_eq!( order(&s), - "window,pane,window", + "pane,window,window", "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"); diff --git a/crates/benilla-ui/src/loader/mod.rs b/crates/benilla-ui/src/loader/mod.rs index 0e6d9d6c..238d4db6 100644 --- a/crates/benilla-ui/src/loader/mod.rs +++ b/crates/benilla-ui/src/loader/mod.rs @@ -642,7 +642,7 @@ impl Loader<'_> { } /// 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 /// its own anchors (`0x76c5b0`). 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. 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`). self.apply_attrs(el, wrapper, dbg_name); // 3 · and 4 · (the CLayoutFrame geometry base, `0x767800`). @@ -681,7 +684,9 @@ impl Loader<'_> { // 6 · handlers (`0x769ef0`); OnLoad is captured to fire bottom-up below. let onload = self.apply_scripts(el, wrapper, dbg_name); - // 7 · nested , whose OnLoads fire before ours (`0x76a060`). + // 7 · the post-load hook (`0x76a2f0`): close the window (`0x76a2ff`), then the nested + // , 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 child in &frames_el.children { let expanded = self.expand(child); @@ -717,6 +722,8 @@ impl Loader<'_> { if let Some(func) = onload { 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. diff --git a/crates/benilla-ui/src/loader/tests.rs b/crates/benilla-ui/src/loader/tests.rs index a8923c88..ab2b4f72 100644 --- a/crates/benilla-ui/src/loader/tests.rs +++ b/crates/benilla-ui/src/loader/tests.rs @@ -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> { + None + } + + fn parse(text: &str) -> framexml::ParsedDocument { + framexml::parse(text).expect("valid FrameXML") + } + + /// The post-load hook `0x76a2f0` builds the `` 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#" + + + Log("load") + Log("show") + + + + + Log("child-load") + Log("child-show") + + + + + "#, + ); + let report = load_in(&s, &doc, "Test.xml", &no_files); + assert!(report.errors.is_empty(), "{:?}", report.errors); + assert_eq!( + s.eval::("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#" + + + Log("a-load") this:Show() Log("a-load-end") + Log("a-show") + + + + + Log("b-load") + Log("b-show") + Log("b-hide") + + + "#, + ); + let report = load_in(&s, &doc, "Test.xml", &no_files); + assert!(report.errors.is_empty(), "{:?}", report.errors); + assert_eq!( + s.eval::("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#" + + + BOX_SHOWN = true + + "#, + ); + let report = load_in(&s, &doc, "Test.xml", &no_files); + assert!(report.errors.is_empty(), "{:?}", report.errors); + assert!(s.eval::("return BOX_SHOWN == true").unwrap()); + assert_eq!(s.focused_editbox_name(), None); + assert!(s.errors().is_empty(), "{:?}", s.errors()); + } +} + #[test] fn join_ref_resolves_relative_framexml_paths() { use super::join_ref; diff --git a/crates/benilla-ui/src/script/event.rs b/crates/benilla-ui/src/script/event.rs index 75bc1a47..82894de3 100644 --- a/crates/benilla-ui/src/script/event.rs +++ b/crates/benilla-ui/src/script/event.rs @@ -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) { - // 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::().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::().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::() - .expect("model") - .record_script_error(e.to_string()); - } - } - for (h, id, visible) in items { - if visible { - let mut model = lua.app_data_mut::().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::().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::() - .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 { diff --git a/crates/benilla-ui/src/script/mod.rs b/crates/benilla-ui/src/script/mod.rs index 99753ab5..39128a76 100644 --- a/crates/benilla-ui/src/script/mod.rs +++ b/crates/benilla-ui/src/script/mod.rs @@ -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; diff --git a/crates/benilla-ui/src/script/nameplate.rs b/crates/benilla-ui/src/script/nameplate.rs index 680cc7aa..c1c77bd1 100644 --- a/crates/benilla-ui/src/script/nameplate.rs +++ b/crates/benilla-ui/src/script/nameplate.rs @@ -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, + /// `(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::().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; } } diff --git a/crates/benilla-ui/src/script/object/frame_state.rs b/crates/benilla-ui/src/script/object/frame_state.rs index ae8f00bc..1b8cd2ce 100644 --- a/crates/benilla-ui/src/script/object/frame_state.rs +++ b/crates/benilla-ui/src/script/object/frame_state.rs @@ -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::().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::().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::().expect("model"); - model.arena.set_shown(h, shown) - }; - event::fire_visibility_changes(lua, changed); + visibility::set_shown(lua, h, shown); Ok(()) } diff --git a/crates/benilla-ui/src/script/tests/create_frame_template.rs b/crates/benilla-ui/src/script/tests/create_frame_template.rs index 506a6d99..eedee8bb 100644 --- a/crates/benilla-ui/src/script/tests/create_frame_template.rs +++ b/crates/benilla-ui/src/script/tests/create_frame_template.rs @@ -430,3 +430,37 @@ fn an_empty_scroll_child_is_reported() { ); assert!(s.eval::("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#" + + + table.insert(LOG, this:GetName() .. ":load") + table.insert(LOG, this:GetName() .. ":show") + + + "#, + ); + 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::("return table.concat(LOG, ',')").unwrap(), + "Seen:load,Seen:show,Unseen:load" + ); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +} diff --git a/crates/benilla-ui/src/script/tests/setparent.rs b/crates/benilla-ui/src/script/tests/setparent.rs index 5cee982b..a94b6aef 100644 --- a/crates/benilla-ui/src/script/tests/setparent.rs +++ b/crates/benilla-ui/src/script/tests/setparent.rs @@ -92,8 +92,9 @@ fn a_visible_reparent_refires_onhide_then_onshow() { let log: Vec = 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::("return RTMover:GetFrameLevel()").unwrap(), diff --git a/crates/benilla-ui/src/script/tests/visibility.rs b/crates/benilla-ui/src/script/tests/visibility.rs index 78f97e9f..314c355f 100644 --- a/crates/benilla-ui/src/script/tests/visibility.rs +++ b/crates/benilla-ui/src/script/tests/visibility.rs @@ -64,3 +64,151 @@ fn tick_runs_onupdate_only_when_effectively_visible() { s.tick(0.25); // visible again → fires assert_eq!(s.eval::("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::("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::("return table.concat(LOG, ',')").unwrap(), + "c1+,p+" + ); + assert!(!s.eval::("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::("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::("return table.concat(LOG, ',')").unwrap(), + "c1+,c1-,p-,p+" + ); + assert!(!s.eval::("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::("return Stale:IsVisible() == 1").unwrap(), + "the reparent from an invisible parent runs no show half" + ); + s.run("Stale:Show()").unwrap(); + assert!(s.eval::("return Stale:IsVisible() == 1").unwrap()); + assert_eq!(s.eval::("return SHOWS").unwrap(), 1); + assert!(s.errors().is_empty(), "{:?}", s.errors()); +} diff --git a/crates/benilla-ui/src/script/tooltip/mod.rs b/crates/benilla-ui/src/script/tooltip/mod.rs index 5cdcad93..79ef2fe1 100644 --- a/crates/benilla-ui/src/script/tooltip/mod.rs +++ b/crates/benilla-ui/src/script/tooltip/mod.rs @@ -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::().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 diff --git a/crates/benilla-ui/src/script/visibility.rs b/crates/benilla-ui/src/script/visibility.rs new file mode 100644 index 00000000..01b1d96a --- /dev/null +++ b/crates/benilla-ui/src/script/visibility.rs @@ -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::() + .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 { + 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::().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()); + } +} diff --git a/crates/benilla-ui/src/widget/mod.rs b/crates/benilla-ui/src/widget/mod.rs index a0126481..eef79a28 100644 --- a/crates/benilla-ui/src/widget/mod.rs +++ b/crates/benilla-ui/src/widget/mod.rs @@ -162,6 +162,10 @@ pub struct Frame { pub shown: bool, /// `shown` and every ancestor effectively visible (`+0xd4`). 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]`. pub mouse_enabled: bool, /// `EnableMouseWheel`, or an `` script at XML load: the mask's wheel bit (3). @@ -228,6 +232,14 @@ pub struct Region { 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, +} + // ── The arena ──────────────────────────────────────────────────────────────────────────────── /// The frame arena: frames, regions, the name registry and the propagation mutators. The screen @@ -424,6 +436,7 @@ impl WidgetArena { effective_alpha, shown, effective_visible: shown && parent_visible, + load_window: false, mouse_enabled: mouse_enabled_by_ctor(kind), // The WorldFrame ctor takes the wheel (`0x481b1f`); the reference's ScrollFrame and // ScrollingMessageFrame ctors do not, and a stock one takes it from ``. diff --git a/crates/benilla-ui/src/widget/propagation.rs b/crates/benilla-ui/src/widget/propagation.rs index 9ac43f9a..38b9de98 100644 --- a/crates/benilla-ui/src/widget/propagation.rs +++ b/crates/benilla-ui/src/widget/propagation.rs @@ -2,51 +2,88 @@ use crate::order::Strata; -use super::{FrameHandle, RegionHandle, WidgetArena, SCALE_EPS}; +use super::{ChildWalk, FrameHandle, RegionHandle, WidgetArena, SCALE_EPS}; impl WidgetArena { // ── Visibility ─────────────────────────────────────────────────────────────────────────────── - /// Set `shown` and propagate effective visibility down the subtree (show `0x76ae10`, hide - /// `0x76ad50`), returning the frames that changed, a node before its descendants, for the - /// caller to fire `OnShow`/`OnHide`. The reference notifies post-order (`0x76aef5`) and - /// re-reads live links, so a frame a handler hides mid-cascade fails its gate (`0x76ae1d`) and - /// is never notified; this list is a snapshot that still holds it. + /// The show transition's own step (`0x76ae10` up to its child walk): nothing unless the frame + /// is shown (`0x76ae1d`), its parent is visible or absent (`0x76ae35`) and it is not visible + /// yet (`0x76ae43`); otherwise it marks itself visible (`0x76ae7b`), before any child or + /// handler runs, and relinks at its bucket's tail (`0x764780`). Whether it transitioned. + 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 { + 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 { let mut changed = Vec::new(); - match self.frame_mut(h) { - Some(f) if f.shown != shown => f.shown = shown, - _ => return changed, + if let Some(f) = self.frame_mut(h) { + f.shown = shown; + self.cascade_silent(h, shown, &mut changed); } - let parent_visible = self.parent_visible(h); - self.propagate_visible(h, parent_visible, &mut changed); changed } - fn propagate_visible( - &mut self, - h: FrameHandle, - parent_visible: bool, - changed: &mut Vec, - ) { - let (shown, old_ev, children) = { - let f = self.frame(h).expect("live node in propagation"); - (f.shown, f.effective_visible, f.children.clone()) + fn cascade_silent(&mut self, h: FrameHandle, show: bool, changed: &mut Vec) { + let stepped = if show { + self.show_step(h) + } else { + self.hide_step(h) }; - let new_ev = shown && parent_visible; - if new_ev == old_ev { - // No change here means none below: prune, as the client's recursion does. + if !stepped { return; } - self.frame_mut(h).unwrap().effective_visible = new_ev; - if new_ev { - // Re-added at its bucket's tail, as the client does, and each descendant after it. - self.resequence_to_tail(h); + let mut walk = ChildWalk::default(); + while let Some(c) = self.next_child(h, &mut walk) { + self.cascade_silent(c, show, changed); } changed.push(h); - for c in children { - self.propagate_visible(c, new_ev, changed); - } } // ── Strata ─────────────────────────────────────────────────────────────────────────────────── @@ -202,18 +239,20 @@ impl WidgetArena { // ── Reparenting ────────────────────────────────────────────────────────────────────────────── - /// Phase 1 of `SetParent` (`0x76ab10`): the guards and the hide half. `None` when the parent - /// is unchanged (`0x76ab20` skips everything), the frame is dead, or the move would cycle (the - /// Lua binding raises first, `0x7a177f`); otherwise the frames that lost effective visibility, - /// in pre-order, whose `OnHide` runs under the old parent (`0x76ab41`). + /// Phase 1 of `SetParent` (`0x76ab10`): the guards. `None` when the parent is unchanged + /// (`0x76ab20` skips everything), the frame is dead, or the move would cycle (the Lua binding + /// raises first, `0x7a177f`); otherwise whether the frame is visible (`0x76ab2b`), in which + /// 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 - /// 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( &mut self, h: FrameHandle, new_parent: Option, - ) -> Option> { + ) -> Option { self.frame(h)?; let new_parent = new_parent.filter(|&p| self.frame(p).is_some()); if let Some(np) = new_parent { @@ -221,32 +260,22 @@ impl WidgetArena { return None; } } - if self.frame(h).unwrap().parent == new_parent { + let f = self.frame(h).unwrap(); + if f.parent == new_parent { return None; } - let mut hidden = Vec::new(); - if self.frame(h).unwrap().effective_visible { - self.propagate_visible(h, false, &mut hidden); - } - Some(hidden) + Some(f.effective_visible) } /// 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 - /// re-inherited, then the show half, returning the frames that became visible; alpha is - /// untouched. `h`'s own children keep their levels, so one can end up below `h`. - /// `was_visible` is whether phase 1 hid anything (the reference's `ebx` at `0x76ab2b`); the - /// show half runs only then (`0x76abfd`), so a hidden frame moved under a visible parent keeps - /// a stale effective visibility until something shows it. - pub fn reparent_finish( - &mut self, - h: FrameHandle, - new_parent: Option, - was_visible: bool, - ) -> Vec { - let mut shown = Vec::new(); + /// re-inherited; alpha is untouched. `h`'s own children keep their levels, so one can end up + /// below `h`. The show half is the caller's, and runs only when phase 1 found the frame + /// visible (`0x76abe3`), so a hidden frame moved under a visible parent keeps a stale + /// effective visibility until something shows it. + pub fn reparent_finish(&mut self, h: FrameHandle, new_parent: Option) { 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()); // 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); let ps = self.parent_effective_scale(h); 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( &mut self, h: FrameHandle, new_parent: Option, ) -> Vec { - let Some(hidden) = self.reparent_begin(h, new_parent) else { + let Some(was_visible) = self.reparent_begin(h, new_parent) else { return Vec::new(); }; - let was_visible = !hidden.is_empty(); - let shown = self.reparent_finish(h, new_parent, was_visible); - [hidden, shown].concat() + let mut changed = Vec::new(); + if was_visible { + 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 diff --git a/crates/benilla-ui/src/widget/tests.rs b/crates/benilla-ui/src/widget/tests.rs index 523d2e8d..c33337a8 100644 --- a/crates/benilla-ui/src/widget/tests.rs +++ b/crates/benilla-ui/src/widget/tests.rs @@ -34,7 +34,8 @@ fn mid_tree_hide_blocks_a_shown_grandchild() { assert!(a.frame(grand).unwrap().effective_visible); 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(child).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); - assert_eq!(changed, vec![child, grand]); + assert_eq!(changed, vec![grand, child]); assert!(a.frame(grand).unwrap().effective_visible); } @@ -58,10 +59,10 @@ fn hidden_grandchild_stays_hidden_when_ancestor_reshows() { a.set_shown(grand, false); let changed = a.set_shown(root, false); // 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); - assert_eq!(changed, vec![root, child]); + assert_eq!(changed, vec![child, root]); assert!(a.frame(child).unwrap().effective_visible); assert!(!a.frame(grand).unwrap().effective_visible); }