Kind updates run in the frame's walk slot; OnUpdateModel in draw order (#897)

The tooltip fade, a model's animation clock and its OnAnimFinished ran after the whole OnUpdate walk, and OnUpdateModel fired in creation order. A tooltip hidden through its parent also kept its owner and fade.

In the reference, the walk makes one call per frame: its OnUpdate, then its kind's engine update (a tooltip's fade step, a model pane's clock and completion test). OnUpdateModel fires later, from the paint, in draw order. A tooltip's own hide and show run from any caller, its parent's cascade included: the hide un-owns and clears it; the show needs an owner.

The walk now runs a per-kind update in each frame's slot, OnUpdateModel fires per stratum in draw order after the hover re-pick, and show and hide dispatch to a kind's override. CharacterModelBase's override is left for #890.

Tests pin each order and a parent's hide and show.

Fixes #861
Fixes #863
Fixes #864

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-11 22:20:26 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 46512dc5b5
commit 07d8c8d4cf
15 changed files with 674 additions and 249 deletions
+3 -2
View File
@@ -408,8 +408,9 @@ const SCRIPT_KINDS: [&str; 39] = [
"OnLoad",
"OnEvent",
"OnUpdate",
// The model pane's two, fired by the tick's model pass (`tick_model_panes`): `OnUpdateModel`
// at the top of a visible pane's paint, `OnAnimFinished` when the armed sequence completes.
// The model pane's two: `OnUpdateModel` from its paint in the draw pass
// (`paint_model_panes`), `OnAnimFinished` from its clock advance in the update walk
// (`advance_pane`) when the armed sequence completes.
"OnUpdateModel",
"OnAnimFinished",
"OnShow",
+2 -2
View File
@@ -655,8 +655,8 @@ impl crate::script::UiScript {
};
let path = m.path.as_deref()?;
let facts = model.model_facts.get(&model_key(path))?;
// A pane whose camera is still pending paints nothing, not even its
// `OnUpdateModel` (`0x76d5f0`).
// A pane whose camera is still pending draws nothing (`0x76d5f0`); its
// `OnUpdateModel` still fires, from the paint hook ahead of that gate (`0x76d5ea`).
if m.camera_pending.is_some() {
return None;
}
@@ -0,0 +1,246 @@
//! The engine's own per-kind updates, each run in its frame's slot of the update walk (`0x765650`,
//! `vtable+0x38` at `0x765710`) right after that frame's `OnUpdate`: GameTooltip's fade
//! (`0x530b20`) and a Model pane's clock and completion (`0x76d7f0`). `OnUpdateModel` fires later,
//! from the draw pass (`0x7657d0`), in draw order.
use crate::script::UiScript;
use crate::widget::{ModelFileFacts, SequenceFacts};
use super::common::script;
/// `LOG` joined by spaces, then emptied.
fn take_log(s: &UiScript) -> String {
s.eval::<String>("local l = table.concat(LOG, ' ') LOG = {} return l")
.unwrap()
}
/// `UI-Cooldown-Indicator.m2`'s two clamped 1000 ms sequences.
const COOLDOWN_FILE: &str = r"Interface\Cooldown\UI-Cooldown-Indicator.mdx";
fn cooldown_facts() -> ModelFileFacts {
ModelFileFacts {
sequences: [(0, 1000), (1, 1000)]
.into_iter()
.map(|(anim_id, duration_ms)| SequenceFacts {
anim_id,
duration_ms,
looping: false,
})
.collect(),
bbox: ([0.0; 3], [0.0; 3]),
cameras: 0,
}
}
fn lua_path(path: &str) -> String {
path.replace('\\', "\\\\")
}
/// A fading tooltip, owned and with a line, whose `OnHide` logs `hide`.
const FADING_TIP: &str = r#"
LOG = {}
local root = CreateFrame("Frame", "FadeRoot")
local owner = CreateFrame("Button", "FadeOwner", root)
owner:SetPoint("CENTER", 0, 0); owner:SetWidth(10); owner:SetHeight(10)
local tt = CreateFrame("GameTooltip", "FadeTip", root)
tt:SetFrameStrata("HIGH")
tt:SetOwner(owner, "ANCHOR_RIGHT")
tt:SetText("Fading")
tt:SetScript("OnHide", function() table.insert(LOG, "hide") end)
"#;
/// The fade is the tooltip's own update (`0x530b9c`, after its `OnUpdate` at `0x530b2d`): a frame
/// walked after it reads this frame's alpha, and on the frame the fade ends the hide (`0x530be9`)
/// and its `OnHide` come before that frame's `OnUpdate`.
#[test]
fn a_frame_walked_after_the_tooltip_sees_this_frames_fade() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(FADING_TIP).unwrap();
s.run(
r#"
local w = CreateFrame("Frame", "FadeWatch")
w:SetFrameStrata("TOOLTIP")
w:SetScript("OnUpdate", function()
if FadeTip:IsShown() then
table.insert(LOG, string.format("shown@%.1f", FadeTip:GetAlpha()))
else
table.insert(LOG, "hidden")
end
end)
FadeTip:FadeOut()
"#,
)
.unwrap();
take_log(&s);
s.tick(0.3);
assert_eq!(take_log(&s), "shown@0.4", "this frame's ramp, not the last");
s.tick(0.3);
assert_eq!(
take_log(&s),
"hide hidden",
"the hide lands in the tooltip's slot"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A pane's update (`0x76d7f0`) runs its `OnUpdate`, then advances its clock, whose completion
/// test drains `OnAnimFinished` in the same call (`0x7074d8`, `0x7075af`): in the pane's place,
/// before frames walked after it and before the draw pass fires `OnUpdateModel`.
#[test]
fn on_anim_finished_fires_in_the_panes_walk_slot() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
s.run(&format!(
r#"
LOG = {{}}
local p = CreateFrame("Model", "SlotPane")
p:SetFrameStrata("LOW")
p:SetModel("{}")
p:SetSequence(1)
p:SetScript("OnUpdate", function() table.insert(LOG, "pane") end)
p:SetScript("OnAnimFinished", function() table.insert(LOG, "finished") end)
p:SetScript("OnUpdateModel", function() table.insert(LOG, "model") end)
local h = CreateFrame("Frame", "SlotHigh")
h:SetFrameStrata("HIGH")
h:SetScript("OnUpdate", function() table.insert(LOG, "high") end)
"#,
lua_path(COOLDOWN_FILE)
))
.unwrap();
take_log(&s);
s.tick(0.6);
assert_eq!(take_log(&s), "pane high model");
s.tick(0.6); // 1200 ms ≥ the 1000 ms sequence
assert_eq!(take_log(&s), "pane finished high model");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// One virtual call runs both halves (`0x76d7fa`, then `0x76d843`): a pane that hides itself in
/// its `OnUpdate` still advances this frame, and a completion its advance reaches still fires.
/// A tooltip shows nothing here: its hide disarms the fade it would step (`0x530002`).
#[test]
fn a_pane_hidden_by_its_own_on_update_still_advances() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
s.run(&format!(
r#"
LOG = {{}}
local p = CreateFrame("Model", "SelfHide")
p:SetModel("{}")
p:SetSequence(1)
p:SetScript("OnAnimFinished", function() table.insert(LOG, "finished") end)
"#,
lua_path(COOLDOWN_FILE)
))
.unwrap();
s.tick(0.6);
s.run(r#"SelfHide:SetScript("OnUpdate", function() this:Hide() end)"#)
.unwrap();
take_log(&s);
s.tick(0.6);
assert!(!s.frame_visible("SelfHide"));
assert_eq!(super::model_clock::pane(&s, "SelfHide").clock_ms, 1200);
assert_eq!(take_log(&s), "finished");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A zero advance returns before the completion test and the drain (`0x7074b7`): a sequence
/// already past its end fires nothing until a frame advances the clock.
#[test]
fn a_zero_advance_tests_no_completion() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
s.run(&format!(
r#"
LOG = {{}}
local p = CreateFrame("Model", "ZeroDt")
p:SetModel("{}")
p:SetSequenceTime(1, 1000)
p:SetScript("OnAnimFinished", function() table.insert(LOG, "finished") end)
"#,
lua_path(COOLDOWN_FILE)
))
.unwrap();
take_log(&s);
s.tick(0.0009); // trunc(0.9 ms) = 0
assert_eq!(super::model_clock::pane(&s, "ZeroDt").clock_ms, 0);
assert_eq!(take_log(&s), "", "no advance, no test");
s.tick(0.016);
assert_eq!(take_log(&s), "finished");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// Completion is judged against the arm the previous paint left: an `OnUpdateModel` that scrubs
/// the sequence back each paint does not hide the completion the walk already reported.
#[test]
fn completion_is_read_before_the_paint_re_arms() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
s.run(&format!(
r#"
LOG = {{}}
local p = CreateFrame("Model", "Scrub")
p:SetModel("{}")
p:SetSequence(1)
p:SetScript("OnAnimFinished", function() table.insert(LOG, "finished") end)
"#,
lua_path(COOLDOWN_FILE)
))
.unwrap();
s.tick(0.6);
s.run(r#"Scrub:SetScript("OnUpdateModel", function() this:SetSequenceTime(1, 0) end)"#)
.unwrap();
take_log(&s);
s.tick(0.6); // the arm from tick one ends at 1000 ms; this tick's clock reads 1200
assert_eq!(take_log(&s), "finished");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// `OnUpdateModel` fires from the paint (`0x76d1bc`), in the draw pass's order: stratum from 0
/// (`0x7657ff`), level from 0 (`0x7658c0`), a level's frames first linked first (`0x765920`).
#[test]
fn on_update_model_fires_in_draw_order() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_model_facts(COOLDOWN_FILE, cooldown_facts());
s.run(&format!(
r#"
LOG = {{}}
function Pane(name, tag, strata, level)
local p = CreateFrame("Model", name)
p:SetFrameStrata(strata)
if level then p:SetFrameLevel(level) end
p:SetModel("{}")
p:SetScript("OnUpdateModel", function() table.insert(LOG, tag) end)
return p
end
Pane("PaneDialog", "dialog", "DIALOG")
Pane("PaneLow", "low", "LOW")
Pane("PaneLevel9", "level9", "MEDIUM", 9)
Pane("PaneLevel2", "level2", "MEDIUM", 2)
"#,
lua_path(COOLDOWN_FILE)
))
.unwrap();
take_log(&s);
s.tick(0.016);
assert_eq!(take_log(&s), "low level2 level9 dialog");
// Within a level the latest link draws last: a pane shown again goes after its peers.
s.run(
r#"
Pane("LinkA", "a", "BACKGROUND", 3)
Pane("LinkB", "b", "BACKGROUND", 3)
LinkA:Hide() LinkA:Show()
"#,
)
.unwrap();
take_log(&s);
s.tick(0.016);
assert_eq!(take_log(&s), "b a low level2 level9 dialog");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
@@ -13,6 +13,7 @@ mod create_frame_template;
mod cvar_bindings;
mod dispatch_bench;
mod end_to_end;
mod engine_updates;
mod events;
mod font_object;
mod frame_api;
@@ -235,7 +235,7 @@ fn the_cooldown_machine_runs_on_the_two_handlers() {
cd:SetScript("OnAnimFinished", CooldownFrame_OnAnimFinished)
cd:Hide()
fired = {{}}
cd:SetScript("OnAnimFinished", function() table.insert(fired, "finished") CooldownFrame_OnAnimFinished() end)
cd:SetScript("OnAnimFinished", function() table.insert(fired, this.stopping) CooldownFrame_OnAnimFinished() end)
CooldownFrame_SetTimer(cd, GetTime(), 2, 1)
"#,
COOLDOWN_FILE.replace('\\', "\\\\")
@@ -244,25 +244,32 @@ fn the_cooldown_machine_runs_on_the_two_handlers() {
assert!(s.frame_visible("CD"));
assert_eq!(play_head(&s, "CD", COOLDOWN_FILE), Some((0, 0)));
let fired = |s: &UiScript| s.eval::<String>("return table.concat(fired, ' ')").unwrap();
s.tick(0.5);
assert_eq!(play_head(&s, "CD", COOLDOWN_FILE), Some((0, 250)));
// Each scrub is an arm, and the walk reads the one the last paint left: between paints this
// scene runs at twice the scrub's rate, so 250 + 1000 ≥ 1000 completes sequence 0. The stock
// `stopping` guard ignores it.
s.tick(1.0);
assert_eq!(fired(&s), "0");
assert_eq!(play_head(&s, "CD", COOLDOWN_FILE), Some((0, 750)));
// A scrub is an anchor, not a freeze: between paints the clock runs on from it.
assert_eq!(pane(&s, "CD").armed.map(|a| a.anchor_ms), Some(1500 - 750));
// 750 + 600 ≥ 1000 completes sequence 0 again, before this paint moves to sequence 1.
s.tick(0.6);
assert_eq!(fired(&s), "0 0");
assert_eq!(play_head(&s, "CD", COOLDOWN_FILE), Some((1, 0)));
assert!(s.frame_visible("CD"));
assert_eq!(s.eval::<i64>("return cd.stopping").unwrap(), 1);
s.tick(0.5);
assert_eq!(play_head(&s, "CD", COOLDOWN_FILE), Some((1, 500)));
assert!(s.eval::<bool>("return table.getn(fired) == 0").unwrap());
assert_eq!(fired(&s), "0 0");
s.tick(0.6);
assert_eq!(
s.eval::<i64>("return table.getn(fired)").unwrap(),
1,
fired(&s),
"0 0 1",
"OnAnimFinished on the clamped sequence's natural completion"
);
assert!(
@@ -270,12 +277,12 @@ fn the_cooldown_machine_runs_on_the_two_handlers() {
"…which the stock handler turns into Hide()"
);
s.tick(1.0);
assert_eq!(s.eval::<i64>("return table.getn(fired)").unwrap(), 1);
assert_eq!(fired(&s), "0 0 1");
assert_eq!(play_head(&s, "CD", COOLDOWN_FILE), Some((1, 1000)));
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// `OnUpdateModel` runs before the completion is read, so re-arming each paint completes nothing.
/// A paint that re-arms each frame completes nothing: each walk reads the arm the last paint left.
#[test]
fn on_update_model_fires_per_visible_paint_with_this() {
let mut s = script();
@@ -212,3 +212,80 @@ fn show_on_a_shown_but_invisible_frame_makes_it_visible() {
assert_eq!(s.eval::<i64>("return SHOWS").unwrap(), 1);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A tooltip, owned with a line and fading, under `TipRoot`; its handlers log to `LOG`.
const OWNED_TIP: &str = r#"
LOG = {}
local root = CreateFrame("Frame", "TipRoot")
local owner = CreateFrame("Button", "TipOwner", root)
owner:SetPoint("CENTER", 0, 0); owner:SetWidth(10); owner:SetHeight(10)
local tt = CreateFrame("GameTooltip", "CascadeTip", root)
tt:SetOwner(owner, "ANCHOR_RIGHT")
tt:SetText("Owned")
tt:SetScript("OnTooltipCleared", function() table.insert(LOG, "cleared") end)
tt:SetScript("OnHide", function() table.insert(LOG, "hide") end)
tt:SetScript("OnShow", function() table.insert(LOG, "show") end)
"#;
fn take_log(s: &crate::script::UiScript) -> String {
s.eval::<String>("local l = table.concat(LOG, ' ') LOG = {} return l")
.unwrap()
}
/// A parent's hide reaches the tooltip through its own hide slot (`0x76adee` → `0x530a60`): the
/// SetOwner core with no owner clears it, fires `OnTooltipCleared` (`0x530263`), restores full
/// alpha, disarms the fade and drops the owner, and only then does the base hide fire `OnHide`.
/// The shown bit is the Lua `Hide`'s to clear, so it stays set.
#[test]
fn a_parents_hide_un_owns_a_fading_tooltip() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(OWNED_TIP).unwrap();
s.run("CascadeTip:FadeOut()").unwrap();
s.tick(0.3);
take_log(&s);
s.run("TipRoot:Hide()").unwrap();
assert_eq!(take_log(&s), "cleared hide");
s.run(
r#"
assert(not CascadeTip:IsOwned(TipOwner), "un-owned by its parent's hide")
assert(CascadeTip:NumLines() == 0, "cleared")
assert(CascadeTip:GetAlpha() == 1, "full alpha, fade disarmed")
assert(CascadeTip:IsShown(), "the shown bit is untouched")
"#,
)
.unwrap();
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A parent's show reaches the tooltip through its own show slot (`0x76aed5` → `0x530a80`): one
/// with an owner and a line shows, one without clears its shown bit and hides itself through its
/// own hide, so it stays hidden. So a parent hidden and shown again (Alt-Z) drops the tooltip.
#[test]
fn a_parents_show_brings_back_only_an_owned_tooltip() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(OWNED_TIP).unwrap();
s.run("TipRoot:Hide()").unwrap();
take_log(&s);
s.run("TipRoot:Show()").unwrap();
assert_eq!(take_log(&s), "cleared", "its own hide again; no OnShow");
assert!(!s.frame_visible("CascadeTip"));
s.run(r#"assert(not CascadeTip:IsShown(), "the show cleared the shown bit")"#)
.unwrap();
// Owned again while the parent is hidden: the parent's show brings it back.
s.run(
r#"
TipRoot:Hide()
CascadeTip:SetOwner(TipOwner, "ANCHOR_RIGHT")
CascadeTip:SetText("Again")
"#,
)
.unwrap();
take_log(&s);
s.run("TipRoot:Show()").unwrap();
assert_eq!(take_log(&s), "show");
assert!(s.frame_visible("CascadeTip"));
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
+98 -99
View File
@@ -5,7 +5,7 @@ use mlua::Lua;
use crate::widget::FrameHandle;
use super::{event, tooltip, Model, ScriptValue, UiScript};
use super::{event, Model, ScriptValue, UiScript};
impl UiScript {
/// Queue an event for the start of the next tick, behind everything queued earlier and before
@@ -70,10 +70,12 @@ pub(crate) fn fire_event_into(lua: &Lua, event: &str, args: Vec<ScriptValue>) {
}
impl super::UiScript {
/// Advance a frame: the `GetTime()` clock and queued events, then the update walk
/// (`0x765650`), each linked frame's update in strata, level and link order: its
/// `OnUpdate(self, elapsed)` (`0x704f10`), and for an EditBox its flush and caret blink after
/// it; then the engine's own fades, model panes and hover.
/// Advance a frame: the `GetTime()` clock and queued events, then the pump `0x765650` and the
/// draw pass `0x7657d0`, in that order (`0x764340`, `0x764347`). The pump is the update walk,
/// each linked frame's update in strata, level and link order: its `OnUpdate(self, elapsed)`
/// (`0x704f10`) and its kind's own update after it (an EditBox's flush, a tooltip's fade, a
/// model pane's clock); then the hover re-pick at its tail (`0x7657be`). The draw pass fires
/// `OnUpdateModel`.
pub fn tick(&mut self, elapsed: f32) {
self.model_mut().now += f64::from(elapsed);
// Events queued since the last tick fire before this tick's OnUpdate.
@@ -82,7 +84,6 @@ impl super::UiScript {
self.fire_event(&event, args);
}
self.update_walk(elapsed);
self.tick_model_panes(elapsed);
// The engine's line fades, apart from any OnUpdate script: ScrollingMessageFrame's
// (`0x788460`) and MessageFrame's (`0x786200`), which also keeps only the lines that fit
// its height, so its rows are read from the resolved rect before the mutable walk.
@@ -116,8 +117,6 @@ impl super::UiScript {
}
}
drop(model);
// Tooltip fades: `FadeOut`'s ramp and the hide at its end.
tooltip::tick_fades(&self.lua);
// A frame hid or showed under a still cursor: re-run the hover walk at the saved position,
// as the reference's pump tail does (`0x765650` → `0x7660d0`). The hidden frame's OnLeave
// already fired, so only the new winner's `OnEnter` fires.
@@ -130,115 +129,115 @@ impl super::UiScript {
if let Some((x, y)) = repick {
self.mouse_move(x, y);
}
self.paint_model_panes();
// `WOW_UI_HANDLERS=<secs>`: last, so its report covers everything this tick fired.
self.report_handler_profile(elapsed);
}
}
impl UiScript {
/// The model panes' per-frame pass, for visible panes, in the reference's order: the pane's
/// `OnUpdate` (`0x76d7f0`) advances its clock by `trunc(elapsed · 1000)` ms, the paint
/// (`0x76d1a0`) fires `OnUpdateModel`, and the animate fires `OnAnimFinished` (`0x76cdc0`) once
/// per arm when the armed sequence completes naturally, a loop's first pass included
/// (`0x719370`). A hidden pane's clock stands still. Completion is read after `OnUpdateModel`,
/// whose handler may re-arm.
fn tick_model_panes(&mut self, elapsed: f32) {
/// A model pane's update after its `OnUpdate` (`0x76d7f0`): its scene clock advances by
/// `trunc(elapsed · 1000)` ms, no `+0.5` (`0x76d843`–`0x76d863`, `0x7074b0`), and the advance
/// tests the armed sequence (`0x7074d8` → `0x719370`) and drains the completions in the same
/// call (`0x7075af`), so `OnAnimFinished` (`0x76cdc0`) fires here, once per arm, against the
/// arm the last paint left. A zero advance tests nothing (`0x7074b7`).
pub(super) fn advance_pane(&mut self, h: FrameHandle, elapsed: f32) {
let dt_ms = (elapsed * 1000.0).trunc().max(0.0) as u64;
let update_ids: Vec<u32> = {
let finished = {
let mut model = self.model_mut();
let ticked: Vec<FrameHandle> = model.arena.ticked_kinds().to_vec();
for h in ticked {
let Some(f) = model.arena.frame_mut(h) else {
continue;
};
if !f.effective_visible {
continue;
}
if let crate::widget::KindState::Model(m) = &mut f.kind_state {
m.clock_ms += dt_ms;
}
}
// Kept by `SetScript`; a destroyed frame's handle compacts out on its first miss.
if model
.on_update_model_frames
.iter()
.any(|&h| model.arena.frame(h).is_none())
{
let arena = &model.arena;
let live: Vec<FrameHandle> = model
.on_update_model_frames
.iter()
.copied()
.filter(|&h| arena.frame(h).is_some())
.collect();
model.on_update_model_frames = live;
}
// Only visible Model panes with a file set, resident or streaming: the paint that fires
// the handler (`0x76d1bc`) is gated on `[widget+0x318] ≠ 0` (`0x76d24c`). On any other
// kind the script is inert.
let frames: Vec<FrameHandle> = model
.on_update_model_frames
.iter()
.copied()
.filter(|&h| {
model.arena.frame(h).is_some_and(|f| {
f.effective_visible
&& matches!(&f.kind_state, crate::widget::KindState::Model(m) if m.path.is_some())
})
})
.collect();
let mut ids: Vec<u32> = frames.into_iter().map(|h| model.frame_id(h)).collect();
ids.sort_unstable(); // creation order
ids
};
for id in update_ids {
if let Err(e) = event::fire_widget_handler(&self.lua, id, "OnUpdateModel", Vec::new()) {
self.push_error(e);
}
}
let finished_ids: Vec<u32> = {
let mut model = self.model_mut();
let ticked: Vec<FrameHandle> = model.arena.ticked_kinds().to_vec();
let mut due: Vec<FrameHandle> = Vec::new();
for h in ticked {
let Some(f) = model.arena.frame(h) else {
continue;
};
if !f.effective_visible {
continue;
}
let crate::widget::KindState::Model(m) = &f.kind_state else {
continue;
};
let Some(path) = m.path.as_deref() else {
continue;
};
let Some(facts) = model.model_facts.get(&crate::widget::model_key(path)) else {
continue;
};
if m.completion_due(facts) {
due.push(h);
}
}
let mut ids = Vec::with_capacity(due.len());
for h in due {
if let Some(crate::widget::KindState::Model(m)) =
model.arena.frame_mut(h).map(|f| &mut f.kind_state)
{
let model = &mut *model;
let Some(crate::widget::KindState::Model(m)) =
model.arena.frame_mut(h).map(|f| &mut f.kind_state)
else {
return;
};
m.clock_ms += dt_ms;
let facts = m
.path
.as_deref()
.and_then(|p| model.model_facts.get(&crate::widget::model_key(p)));
match facts {
Some(facts) if dt_ms > 0 && m.completion_due(facts) => {
if let Some(a) = &mut m.armed {
a.finished = true;
}
Some(model.frame_id(h))
}
ids.push(model.frame_id(h));
_ => None,
}
ids.sort_unstable();
ids
};
for id in finished_ids {
if let Some(id) = finished {
if let Err(e) = event::fire_widget_handler(&self.lua, id, "OnAnimFinished", Vec::new())
{
self.push_error(e);
}
}
}
/// The draw pass's `OnUpdateModel` (`0x7657d0`): each pane's paint fires it (`0x76d1bc`) from
/// the render callback its layer emit adds to its level's ARTWORK batch (`0x76d160`). Strata
/// go from 0 (`0x7657ff`); a stratum's batches are emitted before any is drained (`0x765896`,
/// then `0x7658e2`), level 0 up, a level's frames first linked first (`0x765920`). So the
/// panes are read per stratum when its turn comes, and a pane an earlier handler of that
/// stratum hid still paints this frame; only the callback's own gate, a model file set
/// (`0x76d24c`), is read at the call.
fn paint_model_panes(&mut self) {
let mut model = self.model_mut();
if model.on_update_model_frames.is_empty() {
return;
}
// Kept by `SetScript`; a destroyed frame's handle compacts out on its first miss.
if model
.on_update_model_frames
.iter()
.any(|&h| model.arena.frame(h).is_none())
{
let arena = &model.arena;
let live: Vec<FrameHandle> = model
.on_update_model_frames
.iter()
.copied()
.filter(|&h| arena.frame(h).is_some())
.collect();
model.on_update_model_frames = live;
}
drop(model);
for stratum in crate::order::Strata::ALL {
let panes: Vec<FrameHandle> = {
let model = self.model_ref();
let mut panes: Vec<(u16, u64, FrameHandle)> = model
.on_update_model_frames
.iter()
.filter_map(|&h| {
let f = model.arena.frame(h)?;
(f.strata == stratum && f.effective_visible && has_model_file(f))
.then_some((f.level, u64::from(f.insertion_seq), h))
})
.collect();
panes.sort_unstable_by_key(|&(level, seq, _)| (level, seq));
panes.into_iter().map(|(_, _, h)| h).collect()
};
for h in panes {
let id = {
let mut model = self.model_mut();
match model.arena.frame(h) {
Some(f) if has_model_file(f) => model.frame_id(h),
_ => continue,
}
};
if let Err(e) =
event::fire_widget_handler(&self.lua, id, "OnUpdateModel", Vec::new())
{
self.push_error(e);
}
}
}
}
}
/// A Model-family pane with a file set, resident or streaming (`[widget+0x318] ≠ 0`), which its
/// layer emit and its render callback both require (`0x76d24c`); on any other kind
/// `OnUpdateModel` is inert.
fn has_model_file(f: &crate::widget::Frame) -> bool {
matches!(&f.kind_state, crate::widget::KindState::Model(m) if m.path.is_some())
}
+86 -63
View File
@@ -15,6 +15,7 @@ use mlua::{Lua, Table, Value};
use super::event;
use super::object::{frame_handle_of, publish_global};
use super::region::region_wrapper;
use super::visibility;
use super::{FontObject, Model, RegionData};
use crate::layout::{Anchor, Point};
use crate::order::DrawLayer;
@@ -319,7 +320,8 @@ pub(super) fn fire_cleared(lua: &Lua, h: FrameHandle) {
/// A content `Set*`'s tail: shown with lines, hidden without. The reference leaves an uncached
/// tooltip cleared and collapsed to nothing; this plate keeps its declared size, so it hides. It
/// hides without [`hide_tooltip`] to keep the owner, which the hover loop's `IsOwned` needs.
/// takes the base transitions past the tooltip's own ([`hide_override`] would un-own it), to keep
/// the owner, which the hover loop's `IsOwned` needs.
pub(super) fn show_or_hide_empty(lua: &Lua, h: FrameHandle) {
let lines = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
@@ -328,14 +330,67 @@ pub(super) fn show_or_hide_empty(lua: &Lua, h: FrameHandle) {
Err(_) => 0,
}
};
set_shown(lua, h, lines > 0);
visibility::set_shown_bit(lua, h, lines > 0);
if lines > 0 {
visibility::base_show(lua, h);
} else {
visibility::base_hide(lua, h);
}
}
/// The tooltip's show (`0x530a80`, slot `+0x88`), however the show comes: with an owner and a line
/// (`0x530a83`–`0x530a95`) it restores full alpha, disarms the fade (`0x530aa3`) and runs the base
/// show; else it clears its shown bit and hides through its own hide (`0x530abe`, `0x530ac8`).
/// So a parent's show brings back only a tooltip that still has an owner.
pub(in crate::script) fn show_override(lua: &Lua, h: FrameHandle) {
let live = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
let live = tip_mut(&mut model, h).is_ok_and(|t| t.owner.is_some() && t.num_lines > 0);
if live {
full_alpha(&mut model, h);
}
live
};
if live {
visibility::base_show(lua, h);
} else {
visibility::set_shown_bit(lua, h, false);
visibility::hide(lua, h);
}
}
/// The tooltip's hide (`0x530a60`, slot `+0x84`), however the hide comes, its parent's included:
/// the SetOwner core with no owner (`0x52ffe0`), then the base hide. The core clears the content
/// and fires `OnTooltipCleared` (`0x530050`, whose last act is the fire at `0x530263`), then
/// restores full alpha (`0x52fff4`), disarms the fade (`0x530002`) and drops the owner
/// (`0x53000c`); the mode it leaves at 7 (`0x530012`) is not kept (see `GetAnchorType`). It runs
/// whether or not the tooltip is shown: only the base hide gates on visibility (`0x76ad5b`).
pub(in crate::script) fn hide_override(lua: &Lua, h: FrameHandle) {
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
if tip_mut(&mut model, h).is_err() {
drop(model);
visibility::base_hide(lua, h);
return;
}
clear_content(&mut model, h);
}
fire_cleared(lua, h);
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
full_alpha(&mut model, h);
if let Ok(t) = tip_mut(&mut model, h) {
t.owner = None;
}
}
visibility::base_hide(lua, h);
}
/// New content cancels a running fade at full alpha; with no fade running, an alpha the addon set
/// stays, since only the paths in [`full_alpha`] reset it.
fn cancel_fade(model: &mut Model, h: FrameHandle) {
if let Ok(t) = tip_mut(model, h) {
if t.fade_start.take().is_some() {
if t.fade_left.take().is_some() {
model.arena.set_alpha(h, 1.0);
}
}
@@ -345,16 +400,11 @@ fn cancel_fade(model: &mut Model, h: FrameHandle) {
/// Show (`0x530a80`) and its self-hide (`0x530a60`, the core with no owner) reach it too.
fn full_alpha(model: &mut Model, h: FrameHandle) {
if let Ok(t) = tip_mut(model, h) {
t.fade_start = None;
t.fade_left = None;
}
model.arena.set_alpha(h, 1.0);
}
/// The engine's `GetTime` clock, which [`super::UiScript::tick`] advances.
fn now(lua: &Lua) -> f64 {
crate::script::clock::now(lua)
}
/// Shows or hides as the Lua verbs do, handlers included.
pub(super) fn set_shown(lua: &Lua, h: FrameHandle, shown: bool) {
crate::script::visibility::set_shown(lua, h, shown);
@@ -519,20 +569,6 @@ pub(super) fn append_line(
mod verbs;
pub(super) use verbs::install;
/// The full hide shared by `Hide`, a `Show` with no owner or no lines, and the end of a fade.
pub(super) fn hide_tooltip(lua: &Lua, h: FrameHandle) {
{
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
full_alpha(&mut model, h);
if let Ok(t) = tip_mut(&mut model, h) {
t.owner = None;
}
clear_content(&mut model, h);
}
set_shown(lua, h, false);
fire_cleared(lua, h);
}
/// A line cell's measured extent; `None` for a hidden or unmeasured cell, which adds no size and
/// no gap, so a fresh tooltip keeps its declared size until its lines are measured.
type Cell = Option<(f32, f32)>;
@@ -669,47 +705,34 @@ pub(super) fn layout_tooltips(model: &mut Model) {
}
}
/// Advances every fading tooltip: alpha ramps from 1 to 0 over [`TOOLTIP_FADE_SECS`], then the
/// full hide.
pub(super) fn tick_fades(lua: &Lua) {
let t = now(lua);
let mut ramping: Vec<(FrameHandle, f32)> = Vec::new();
let mut finished: Vec<FrameHandle> = Vec::new();
{
let model = lua.app_data_ref::<Model>().expect("model app_data");
// Roster-gated as in `layout_tooltips`, since `hide_tooltip` below writes layout.
for &h in model.arena.tooltip_kinds() {
if !model.frame_to_id.contains_key(&h) {
continue;
}
let Some(frame) = model.arena.frame(h) else {
continue;
};
let KindState::Tooltip(tip) = &frame.kind_state else {
continue;
};
let Some(start) = tip.fade_start else {
continue;
};
if !frame.shown {
finished.push(h); // hidden mid-fade by other means: just tidy the state
continue;
}
let a = 1.0 - ((t - start) / TOOLTIP_FADE_SECS) as f32;
if a <= 0.0 {
finished.push(h);
} else {
ramping.push((h, a));
}
}
}
{
/// The tooltip's own update after its `OnUpdate` (`0x530b20`, from `0x530b9c`): an armed fade
/// counts down by the elapsed time (`0x530baa`–`0x530bc2`) and ramps the alpha; at zero the full
/// hide (`0x530bd8` full alpha, `0x530be9` the hide slot), whose `OnHide` runs here, in the
/// tooltip's place in the walk. Nothing gates it on shown, owner or lines.
pub(in crate::script) fn update(lua: &Lua, h: FrameHandle, elapsed: f32) {
let done = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
for (h, a) in ramping {
model.arena.set_alpha(h, a);
// Roster-gated as in `layout_tooltips`, since the hide writes layout.
if !model.frame_to_id.contains_key(&h) {
return;
}
}
for h in finished {
hide_tooltip(lua, h);
let Ok(tip) = tip_mut(&mut model, h) else {
return;
};
let Some(left) = tip.fade_left else {
return;
};
let left = left - elapsed;
if left <= 0.0 {
true
} else {
tip.fade_left = Some(left);
model.arena.set_alpha(h, left / TOOLTIP_FADE_SECS);
false
}
};
// The end: shown bit cleared, then the hide slot, as the Lua `Hide` does.
if done {
set_shown(lua, h, false);
}
}
+5 -38
View File
@@ -7,12 +7,11 @@ use crate::script::binding_abi::optional_string;
use crate::script::object::frame_handle_of;
use crate::script::region::region_handle_of;
use crate::script::Model;
use crate::widget::{KindState, TooltipAnchor, TOOLTIP_LINE_GAP, TOOLTIP_PAD};
use crate::widget::{KindState, TooltipAnchor, TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD};
use super::{
append_line, bool_arg, clear_content, fire_cleared, full_alpha, hide_tooltip, now,
parse_line_color, parse_line_tail, set_shown, text_of, tip_mut, with_tip, write_cell,
REG_TOOLTIP_METHODS,
append_line, bool_arg, clear_content, fire_cleared, full_alpha, parse_line_color,
parse_line_tail, set_shown, text_of, tip_mut, with_tip, write_cell, REG_TOOLTIP_METHODS,
};
pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
@@ -322,45 +321,13 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
m.set(
"FadeOut",
lua.create_function(|lua, this: Table| {
let t = now(lua);
with_tip(lua, &this, |tip| {
if tip.fade_start.is_none() {
tip.fade_start = Some(t);
if tip.fade_left.is_none() {
tip.fade_left = Some(TOOLTIP_FADE_SECS);
}
})
})?,
)?;
// Show and Hide shadow the shared pair. Show (`0x530a80`, the override the generic Show at
// `0x7a3350` calls) shows only a plate with an owner and at least one line, checked at Show
// time only; otherwise it takes the full hide (`0x530a60`): un-owned, cleared,
// OnTooltipCleared fired. Hide is the full hide.
m.set(
"Show",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
let live = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
full_alpha(&mut model, h);
tip_mut(&mut model, h)
.map(|t| t.owner.is_some() && t.num_lines > 0)
.unwrap_or(false)
};
match live {
true => set_shown(lua, h, true),
false => hide_tooltip(lua, h),
}
Ok(())
})?,
)?;
m.set(
"Hide",
lua.create_function(|lua, this: Table| {
let h = frame_handle_of(lua, &this)?;
hide_tooltip(lua, h);
Ok(())
})?,
)?;
// The content methods: items, spells and auras, talents, units.
crate::script::tooltip_item::install_methods(lua, &m)?;
crate::script::tooltip_spell::install_methods(lua, &m)?;
@@ -218,7 +218,7 @@ fn answer_wait(lua: &Lua, h: FrameHandle, spell_id: u32, alt_caster: bool) -> ml
else {
return Ok(());
};
let fading = tip_mut(&mut model, h)?.fade_start.is_some();
let fading = tip_mut(&mut model, h)?.fade_left.is_some();
if fading || drawn_lines(&mut model, h)? != wait.drawn {
model.spell_tooltip_waits.remove(&h);
return Ok(());
+6 -7
View File
@@ -12,7 +12,7 @@ use super::unit::{is_civilian_kill, level_reads_unknown, unknownobject};
use super::{KindState, Model, UnitState};
use crate::layout::{Anchor, Point};
use crate::strings::{fill, Arg};
use crate::widget::FrameHandle;
use crate::widget::{FrameHandle, TOOLTIP_FADE_SECS};
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
const GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0];
@@ -303,7 +303,7 @@ impl super::UiScript {
let mut model = self.model_mut();
if let Some(f) = model.arena.frame_mut(h) {
if let KindState::Tooltip(t) = &mut f.kind_state {
if t.fade_start.take().is_some() {
if t.fade_left.take().is_some() {
model.arena.set_alpha(h, 1.0);
}
}
@@ -404,7 +404,7 @@ impl super::UiScript {
let mut model = self.model_mut();
if let Some(f) = model.arena.frame_mut(h) {
if let KindState::Tooltip(t) = &mut f.kind_state {
if t.fade_start.take().is_some() {
if t.fade_left.take().is_some() {
model.arena.set_alpha(h, 1.0);
}
}
@@ -469,7 +469,7 @@ impl super::UiScript {
let mut model = self.model_mut();
if let Some(f) = model.arena.frame_mut(h) {
if let KindState::Tooltip(t) = &mut f.kind_state {
if t.fade_start.take().is_some() {
if t.fade_left.take().is_some() {
model.arena.set_alpha(h, 1.0);
}
}
@@ -525,15 +525,14 @@ impl super::UiScript {
/// On hover loss, arm the world tooltip's timed fade (the reference's unit plate fades rather
/// than hiding); a no-op unless a world-owned plate is shown.
pub fn world_tooltip_fade(&mut self) {
let now = self.now();
let mut model = self.model_mut();
let Some(h) = model.arena.lookup("GameTooltip") else {
return;
};
if let Some(f) = model.arena.frame_mut(h) {
if let KindState::Tooltip(t) = &mut f.kind_state {
if t.world_owned && f.shown && t.fade_start.is_none() {
t.fade_start = Some(now);
if t.world_owned && f.shown && t.fade_left.is_none() {
t.fade_left = Some(TOOLTIP_FADE_SECS);
}
}
}
+29 -11
View File
@@ -10,12 +10,16 @@
//! ends (`0x76489b`, spliced at `0x76573e`) and so waits a tick, as does one linked into a level
//! already walked; one linked into a level still ahead is updated there, after that level's
//! older links.
//!
//! A frame's turn is its kind's update (`vtable+0x38`, `0x765710`): the `OnUpdate`, then, for a
//! kind with a [`KindUpdate`], that kind's engine work, so whatever it fires runs in the frame's
//! place, ahead of every frame walked after it.
use std::collections::{BTreeMap, HashMap, HashSet};
use crate::widget::{FrameHandle, FrameKind};
use crate::widget::{FrameHandle, KindUpdate};
use super::{editbox, event, UiScript};
use super::{editbox, event, tooltip, UiScript};
/// A place in the walk: stratum, level, then within the level the frames linked before the walk
/// began (0, by link stamp) ahead of those linked during it (1, by the order they linked).
@@ -23,8 +27,7 @@ type Place = (u8, u16, u8, u64);
impl UiScript {
/// Run the walk: each frame's `OnUpdate(self, elapsed)` (`0x76b2c0`, reading the handler when
/// its turn comes), and for an EditBox its flush after it (`0x77a79a`, then `0x77a7a1`), so the
/// events an edit raised fire in the box's place.
/// its turn comes), then its kind's own update ([`Self::kind_update`]).
pub(super) fn update_walk(&mut self, elapsed: f32) {
let mut ahead: BTreeMap<Place, FrameHandle> = BTreeMap::new();
let (renumbers, mut joins, mut seen) = {
@@ -44,12 +47,12 @@ impl UiScript {
.collect();
model.on_update_frames = live;
}
// Only frames with an `OnUpdate` or an EditBox's flush do anything when walked.
// Only frames with an `OnUpdate` or a kind's own update do anything when walked.
let mut seen = HashSet::new();
for &h in model
.on_update_frames
.iter()
.chain(model.arena.editbox_kinds())
.chain(model.arena.walked_kinds())
{
if let Some(f) = model.arena.frame(h).filter(|f| f.effective_visible) {
let place = (f.strata as u8, f.level, 0, u64::from(f.insertion_seq));
@@ -64,7 +67,7 @@ impl UiScript {
let mut relinked: HashMap<FrameHandle, u64> = HashMap::new();
let mut links = 0u64;
while let Some((place, h)) = ahead.pop_first() {
let (on_update, editbox) = {
let (on_update, kind_update) = {
let mut model = self.model_mut();
let Some(f) = model.arena.frame(h) else {
continue;
@@ -75,21 +78,23 @@ impl UiScript {
0 => !relinked.contains_key(&h),
_ => relinked.get(&h) == Some(&place.3),
};
let editbox = f.kind == FrameKind::EditBox;
// Fixed before the `OnUpdate` runs: one virtual call does both, so a frame that
// hides itself in its `OnUpdate` still takes its kind's update.
let kind_update = f.kind.kind_update().filter(|_| here);
let on_update = here
&& model
.scripts
.get(&h)
.is_some_and(|s| s.contains("OnUpdate"));
(on_update.then(|| model.frame_id(h)), here && editbox)
(on_update.then(|| model.frame_id(h)), kind_update)
};
if let Some(id) = on_update {
if let Err(e) = event::fire_update_handler(&self.lua, id, elapsed) {
self.push_error(e);
}
}
if editbox {
editbox::update(&self.lua, h, elapsed);
if let Some(k) = kind_update {
self.kind_update(k, h, elapsed);
}
let mut model = self.model_mut();
for linked in model.arena.take_linked() {
@@ -128,4 +133,17 @@ impl UiScript {
}
self.model_mut().arena.close_link_log();
}
/// A kind's engine work after its `OnUpdate`, in its place in the walk.
fn kind_update(&mut self, kind: KindUpdate, h: FrameHandle, elapsed: f32) {
match kind {
// The flush (`0x77a7a1`), so the events an edit raised fire in the box's place, then
// the caret blink.
KindUpdate::EditBox => editbox::update(&self.lua, h, elapsed),
// The fade (`0x530b9c`), whose end hides the tooltip in its place.
KindUpdate::Tooltip => tooltip::update(&self.lua, h, elapsed),
// The scene clock (`0x76d843`), whose completions fire `OnAnimFinished` here.
KindUpdate::Model => self.advance_pane(h, elapsed),
}
}
}
+46 -6
View File
@@ -2,11 +2,16 @@
//! 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.
//!
//! Both are virtual (slots `+0x88` and `+0x84`), and every caller reaches them through the slot:
//! the Lua `Show`/`Hide`, the parent's child walk (`0x76aed5`, `0x76adee`) and `SetParent`
//! (`0x76abfd`, `0x76ab41`). [`show`] and [`hide`] are that dispatch, so a kind's override runs
//! the same however the transition comes.
use mlua::{Lua, Table, Value};
use super::{event, Model};
use crate::widget::{ButtonState, ChildWalk, FrameHandle};
use crate::widget::{ButtonState, ChildWalk, FrameHandle, FrameKind};
/// 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
@@ -23,9 +28,44 @@ pub(super) fn set_shown(lua: &Lua, h: FrameHandle, shown: bool) {
}
}
/// 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`).
/// The show slot (`+0x88`): the kind's override, else the base [`base_show`]. GameTooltip's is
/// `0x530a80`. CharacterModelBase's (`0x505d00`, which rebuilds the doll from its unit or file)
/// is not modelled.
pub(super) fn show(lua: &Lua, h: FrameHandle) {
match kind(lua, h) {
Some(FrameKind::GameTooltip) => super::tooltip::show_override(lua, h),
_ => base_show(lua, h),
}
}
/// The hide slot (`+0x84`): the kind's override, else the base [`base_hide`]. GameTooltip's is
/// `0x530a60`. CharacterModelBase's (`0x505ce0`, which releases the doll's model) is not
/// modelled.
pub(super) fn hide(lua: &Lua, h: FrameHandle) {
match kind(lua, h) {
Some(FrameKind::GameTooltip) => super::tooltip::hide_override(lua, h),
_ => base_hide(lua, h),
}
}
fn kind(lua: &Lua, h: FrameHandle) -> Option<FrameKind> {
lua.app_data_ref::<Model>()
.expect("model app_data")
.arena
.frame(h)
.map(|f| f.kind)
}
/// The shown bit alone (`[+0xd0]`), which an override writes before it re-dispatches.
pub(super) fn set_shown_bit(lua: &Lua, h: FrameHandle, shown: bool) {
if let Some(f) = model_mut(lua).arena.frame_mut(h) {
f.shown = shown;
}
}
/// The base show transition (`0x76ae10`): the frame's own step, its children's transitions in
/// link order, the toplevel raise (`0x76aeec`), then its own OnShow (`0x76aef5`).
pub(super) fn base_show(lua: &Lua, h: FrameHandle) {
{
let mut model = model_mut(lua);
if !model.arena.show_step(h) {
@@ -45,9 +85,9 @@ pub(super) fn show(lua: &Lua, h: FrameHandle) {
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) {
/// The base hide transition (`0x76ad50`): the frame's own step with its hover leave, its
/// children's transitions in link order, then its own OnHide (`0x76adfd`).
pub(super) fn base_hide(lua: &Lua, h: FrameHandle) {
let left = {
let mut model = model_mut(lua);
if !model.arena.hide_step(h) {
+36 -3
View File
@@ -49,6 +49,37 @@ pub enum FrameKind {
Minimap,
}
/// A kind's own per-frame update: the `vtable+0x38` override the update walk calls in the frame's
/// place (`0x765710`), which fires the frame's `OnUpdate` (`0x76b2c0`) first and then does the
/// kind's engine work.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KindUpdate {
/// `CSimpleEditBox`'s `0x77a790`: the flush, then the caret blink.
EditBox,
/// `CGameTooltip`'s `0x530b20`: the fade.
Tooltip,
/// `CSimpleModel`'s `0x76d7f0`, in the slot of every model kind's vtable (`0x81c608`,
/// `0x8084f8`, `0x8083a8`, `0x808260`): the scene clock and the completions it reports.
Model,
}
impl FrameKind {
/// The kind's own update, run in the walk after the frame's `OnUpdate`. The reference
/// overrides the slot on more kinds; benilla runs the message frames' fades (`0x786200`,
/// `0x788460`) after the walk, and models none of the rest there.
pub fn kind_update(self) -> Option<KindUpdate> {
match self {
FrameKind::EditBox => Some(KindUpdate::EditBox),
FrameKind::GameTooltip => Some(KindUpdate::Tooltip),
FrameKind::Model
| FrameKind::PlayerModel
| FrameKind::DressUpModel
| FrameKind::TabardModel => Some(KindUpdate::Model),
_ => None,
}
}
}
/// The kind of a [`Region`] leaf, the client's `CScriptRegion`-derived non-frame objects.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RegionKind {
@@ -187,8 +218,10 @@ pub struct TooltipState {
pub anchor: TooltipAnchor,
/// `SetMinimumWidth`: a floor on the auto-sized width, cleared with the content.
pub min_width: f32,
/// `FadeOut()`'s start on the `GetTime` clock; any fresh content or `Show` cancels the fade.
pub fade_start: Option<f64>,
/// `FadeOut()`'s countdown in seconds (`[+0x3b4]` armed, `[+0x3b8]` left): the tooltip's own
/// update counts it down by its elapsed, so it stands still while the tooltip is not walked.
/// Any fresh content or `Show` cancels it.
pub fade_left: Option<f32>,
/// The guid of the unit this tooltip shows (`+0x368`/`+0x36c`, stored at `0x52a070`), whose
/// health watcher (`0x467e70`, registered at `0x52a0a8`) drives the bar; a `set_unit` push of
/// that unit, under any token, re-drives it.
@@ -212,7 +245,7 @@ pub const TOOLTIP_LINE_GAP: f32 = 2.0;
/// The least double-line column gap, the stock `TextRightN` offset; the reference's is untraced.
pub const TOOLTIP_DOUBLE_GAP: f32 = 40.0;
/// `FadeOut`'s ramp length in seconds; the reference's constant is untraced.
pub const TOOLTIP_FADE_SECS: f64 = 0.5;
pub const TOOLTIP_FADE_SECS: f32 = 0.5;
/// A wrap-flagged line's width, pinned when it is appended; the reference's column is untraced.
pub const TOOLTIP_WRAP_WIDTH: f32 = 260.0;
+25 -11
View File
@@ -120,12 +120,13 @@ mod kinds;
pub use kinds::{
model_key, slider_fraction, slider_grab, slider_set_value, ArmedSequence, ButtonFont,
ButtonState, ButtonVisualState, ColorSelectState, EditAction, EditBoxState, EditOutcome,
EditUnit, FrameKind, InsertMode, KindState, MessageFrameState, MessageLine, MinimapState,
ModelFileFacts, ModelFog, ModelLight, ModelPlayHead, ModelState, RegionKind, ScrollFrameState,
ScrollingMessageState, SequenceFacts, SliderState, StatusBarState, TooltipAnchor, TooltipState,
MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_MASK, MINIMAP_DEFAULT_PLAYER_MODEL,
MINIMAP_DEFAULT_ZOOM, MINIMAP_ENGINE_CHILDREN, MINIMAP_ZOOM_LEVELS, TOOLTIP_DOUBLE_GAP,
TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD, TOOLTIP_WRAP_WIDTH,
EditUnit, FrameKind, InsertMode, KindState, KindUpdate, MessageFrameState, MessageLine,
MinimapState, ModelFileFacts, ModelFog, ModelLight, ModelPlayHead, ModelState, RegionKind,
ScrollFrameState, ScrollingMessageState, SequenceFacts, SliderState, StatusBarState,
TooltipAnchor, TooltipState, MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_MASK,
MINIMAP_DEFAULT_PLAYER_MODEL, MINIMAP_DEFAULT_ZOOM, MINIMAP_ENGINE_CHILDREN,
MINIMAP_ZOOM_LEVELS, TOOLTIP_DOUBLE_GAP, TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD,
TOOLTIP_WRAP_WIDTH,
};
// ── Frame and Region nodes ───────────────────────────────────────────────────────────────────
@@ -265,15 +266,18 @@ pub struct WidgetArena {
pub(crate) order_cache: crate::order::OrderCache,
/// Minimap frames ever created, so the state feed notices a new one without a walk.
minimap_created: u64,
/// The live frames the host ticks (message fades, model scene clocks), so the tick skips the
/// rest of the arena. Added at creation, as kinds never change; `destroy` removes its own.
/// The live message frames and model panes, so the host's passes over them (message fades,
/// model paint) skip the rest of the arena. Added at creation, as kinds never change;
/// `destroy` removes its own.
ticked_kinds: Vec<FrameHandle>,
/// The live GameTooltips, kept the same way, for the layout pre-pass, fade tick and unit push.
/// The live frames whose kind has a [`KindUpdate`], kept the same way: the update walk reaches
/// each one shown, with an `OnUpdate` or without.
walked_kinds: Vec<FrameHandle>,
/// The live GameTooltips, kept the same way, for the layout pre-pass and unit push.
tooltip_kinds: Vec<FrameHandle>,
/// The live Minimaps, kept the same way, for the containment, zoom and arrow-facing feeds.
minimap_kinds: Vec<FrameHandle>,
/// The live EditBoxes, kept the same way, for the resize re-seat (`reseat_resized`) and the
/// tick's walk, which flushes every shown box.
/// The live EditBoxes, kept the same way, for the resize re-seat (`reseat_resized`).
editbox_kinds: Vec<FrameHandle>,
}
@@ -318,6 +322,7 @@ impl WidgetArena {
minimap_created: 0,
order_cache: Default::default(),
ticked_kinds: Vec::new(),
walked_kinds: Vec::new(),
tooltip_kinds: Vec::new(),
minimap_kinds: Vec::new(),
editbox_kinds: Vec::new(),
@@ -334,6 +339,11 @@ impl WidgetArena {
&self.ticked_kinds
}
/// The frames whose kind has an update of its own in the walk ([`FrameKind::kind_update`]).
pub fn walked_kinds(&self) -> &[FrameHandle] {
&self.walked_kinds
}
pub fn tooltip_kinds(&self) -> &[FrameHandle] {
&self.tooltip_kinds
}
@@ -558,6 +568,9 @@ impl WidgetArena {
if ticked {
self.ticked_kinds.push(handle);
}
if kind.kind_update().is_some() {
self.walked_kinds.push(handle);
}
if matches!(kind, FrameKind::GameTooltip) {
self.tooltip_kinds.push(handle);
}
@@ -627,6 +640,7 @@ impl WidgetArena {
let name = frame.name.clone();
// Each recursive call removes its own handle, so the registries hold no dead frame.
self.ticked_kinds.retain(|&t| t != h);
self.walked_kinds.retain(|&t| t != h);
self.tooltip_kinds.retain(|&t| t != h);
self.minimap_kinds.retain(|&t| t != h);
self.editbox_kinds.retain(|&t| t != h);