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:
co-authored by
Claude Opus 5.5
parent
46512dc5b5
commit
07d8c8d4cf
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user