- a comment that justifies code by an engine difference is a claim with an expiry date, and the tree had ~96 expired ones across `assets/ui`, `ui_script/` and `benilla-ui` — a hygiene pass, no behaviour change: dangling `Benilla*` names with zero definitions (`BenillaMoney_*` at thirteen sites after 1937/1962 converged the coin cluster onto the stock `SmallMoneyFrameTemplate`; `BenillaGossipRow_Resize`, `BenillaErrorsFrame_AddMessage`, the digit feed 1285 deleted, `BenillaMerchantItem_OnClick` and three more), "the shipped `assets/ui/<X>.xml`" for thirty-odd files the directory no longer holds — it holds nine — each site now naming the chain file that actually loads, `UiPanels.xml` in the present tense after 1988 took the panel manager and the dialog engine stock, and expired self-deprecation: SimpleHTML "a widget kind we do not have" (1337 built it), "benilla has no console" (1948 bound `ConsoleExec`), `Model`'s "seven verbs not published yet" and "`ClearFog` is a guess" (all 23 are published, the carve sits 500 lines below, `UNBUILT` has been `[&str; 0]` since 2027), `frame_flag_gate`'s doc describing four groups of accepted mouse divergences over a `KNOWN` list that is empty (the empty array and its enforcement stay), five comments saying key delivery is not gated on `keyboard_enabled` where `script::keyboard`'s walk filters on exactly that field (1319), `SetRaidTarget` called a verb this house lacks beside an assert that its intent IS queued (1820), two questlog tests deferring to a screenshot for `GetPoint`/`IsVisible` the regions already answer, `benilla.toc`'s "trade still needs four engine verbs" (all four bound, 1966) and its ping `Model`s "this engine draws no pixels for" (2013 renders `MinimapPing.mdx` itself), and `rig_anim`'s `set_paused` naming a booth caller it does not have; and the code the prose was propping up goes with it: `GameMenuFrame.xml`'s six `<OnLoad>` label setters, because `text=` on a `<Button>` is a global-string lookup at the loader's attribute step (0995, built 61 seconds before the file was written) — the buttons carry `text="LOGOUT"`/`"EXIT_GAME"`/`"MACROS"`/`"RETURN_TO_GAME"`/`"MAIN_MENU"` as the reference's own do; `ScriptLogFrame`'s five authored close-button lines, now `inherits="UIPanelCloseButton"` off the chain's `UIPanelTemplates.xml` seated 757 lines above it (1846); and the editbox's 22-line "gap" doc claiming only the self-acquire `autoFocus` path is built and its `false` default unverified — path 1 is 1686's `visibility_focus`, and the default is `true`, byte-read off `flags = 1` at `0x779a29`/`0x779a2e`, so the test named `autofocus_does_not_focus_on_show…` is renamed for what it actually pins (2044) - the `text=` divergence never existed: `resolve_text`'s doc had claimed since 0068 that falling back to the raw attribute on a GlobalStrings miss was "one deliberate divergence" from a reference that draws the empty string, and read at the byte it is the reference's own behaviour — `FrameScript_GetText 0x703bf0` does return a pre-seeded empty string (`0x882748`) when the global is not a string, but that return never reaches a label, because all three `text=` readers image-wide test it and substitute the raw attribute: `Button::LoadXML` at `0x778c07` (`0x778c31 mov eax,esi`), `CSimpleFontString::LoadXML` at `0x771006` byte-identically, and `0x7292a6`, which pushes `edi` on the empty arm; so the loader changes nothing, the two test comments that had called `PetLevelText`'s "Level level race class" placeholder and `CLOSE` our divergence now name the reference's arm (a real 1.12 client draws them too), and what IS ours is the extra warning on a key-shaped miss (0983 → 0991), a strict addition; beside it, `GetEffectiveScale`'s root factor is 1 not because "benilla has no `uiScale` CVar" — 1804 registered one, default 0.9 — but because the dial is applied at the raster seam, `768/uiScale` UI units of virtual-screen height rather than a scale on `UIParent`, so every coordinate the VM hands Lua is already in those units and the reference's `GetCursorPosition()/GetEffectiveScale()` transcriptions convert correctly with a root of 1 (2044) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
194 lines
9.0 KiB
Rust
194 lines
9.0 KiB
Rust
//! The errors/info frame — stock `Interface\FrameXML\UIErrorsFrame.xml`, a real
|
|
//! `<MessageFrame>` — driven engine-only: the yellow `UI_INFO_MESSAGE` toast (the quest
|
|
//! objective-progress popup's surface), the red `UI_ERROR_MESSAGE` line, insertMode-TOP stacking,
|
|
//! and the hold+fade expiry.
|
|
//!
|
|
//! Every assertion here reads the **drawn quads**, not Lua state. It used to read
|
|
//! `UIErrorsFrameLine1:GetText()` — the hand-rolled version's three stacked FontStrings — and those
|
|
//! are gone with it: the widget draws its own message bands now, so what a player sees is the only
|
|
//! thing left to test, which is also the thing worth testing.
|
|
|
|
use benilla_ui::script::{ExtractedQuad, QuadContent, ScriptValue, UiScript};
|
|
|
|
use super::test_ui::load_ui as load_xml;
|
|
|
|
/// The toast lines as drawn, **top row first**, each with its colour+alpha and its band bottom.
|
|
fn toast_lines(s: &mut UiScript) -> Vec<(String, [f32; 4], f32)> {
|
|
s.resolve();
|
|
let mut v: Vec<(String, [f32; 4], f32)> = s
|
|
.extract()
|
|
.iter()
|
|
.filter_map(|q| match (&q.content, q.rect) {
|
|
(
|
|
QuadContent::Text {
|
|
text: Some(t),
|
|
color: Some(c),
|
|
..
|
|
},
|
|
Some(r),
|
|
) if !t.is_empty() => Some((t.clone(), *c, r.bottom)),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
v.sort_by(|a, b| b.2.total_cmp(&a.2));
|
|
v
|
|
}
|
|
|
|
#[test]
|
|
fn info_and_error_messages_stack_hold_and_expire() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
|
|
|
|
// Empty at load. The frame itself is shown — a MessageFrame with nothing to say simply draws
|
|
// nothing, where the hand-rolled version had to `Hide()` itself.
|
|
assert!(toast_lines(&mut s).is_empty());
|
|
|
|
// A quest progress toast: yellow (ref UIErrorsFrame.lua:12), on the top line.
|
|
s.fire_event(
|
|
"UI_INFO_MESSAGE",
|
|
vec![ScriptValue::Str("Tough Wolf Meat: 2/8".into())],
|
|
);
|
|
assert!(s.errors().is_empty(), "info errors: {:?}", s.errors());
|
|
let lines = toast_lines(&mut s);
|
|
assert_eq!(lines.len(), 1);
|
|
assert_eq!(lines[0].0, "Tough Wolf Meat: 2/8");
|
|
assert_eq!(lines[0].1, [1.0, 1.0, 0.0, 1.0], "UI_INFO_MESSAGE yellow");
|
|
|
|
// A second message lands ON TOP (insertMode="TOP"), red for UI_ERROR_MESSAGE (lua:14), pushing
|
|
// the toast to the row below — the whole point of the attribute.
|
|
s.fire_event(
|
|
"UI_ERROR_MESSAGE",
|
|
vec![ScriptValue::Str("You are too far away!".into())],
|
|
);
|
|
let lines = toast_lines(&mut s);
|
|
assert_eq!(
|
|
lines.iter().map(|l| l.0.as_str()).collect::<Vec<_>>(),
|
|
["You are too far away!", "Tough Wolf Meat: 2/8"]
|
|
);
|
|
// 0.1 comes back as 26/255: `AddMessage` byte-quantizes every channel round-half-up
|
|
// (`ftol(v*255 + 0.5)`), so the colour a message draws with is never quite the float handed in.
|
|
assert_eq!(
|
|
lines[0].1,
|
|
[1.0, 26.0 / 255.0, 26.0 / 255.0, 1.0],
|
|
"UI_ERROR_MESSAGE red, byte-quantized"
|
|
);
|
|
|
|
// Hold 5 s (the ref's displayDuration="5"), then the class ctor's 3 s ramp, then gone. The
|
|
// phase check is per-tick, so the tick that spends the last of the hold still draws full.
|
|
s.tick(4.0);
|
|
assert_eq!(toast_lines(&mut s).len(), 2);
|
|
s.tick(2.0); // the hold is spent on this tick; the ramp starts on the next
|
|
assert_eq!(toast_lines(&mut s).len(), 2);
|
|
s.tick(1.5); // half the ramp — still drawing, now dimmer
|
|
let mid = toast_lines(&mut s);
|
|
assert_eq!(mid.len(), 2);
|
|
assert!(
|
|
mid[0].1[3] < 1.0 && mid[0].1[3] > 0.0,
|
|
"mid-ramp alpha: {:?}",
|
|
mid[0].1
|
|
);
|
|
s.tick(2.0); // ramp done — retired, and this class frees the line rather than blanking it
|
|
assert!(toast_lines(&mut s).is_empty());
|
|
assert!(s.errors().is_empty(), "expiry errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// **The toast must outrank an open panel window.** `UIErrorsFrame` is 512x60 at TOP (0,-122)
|
|
/// and every left-slot panel is 384-wide at TOPLEFT (0,-104), so they overlap. The reference puts
|
|
/// this frame in HIGH (`UIErrorsFrame.xml` l.4) and benilla had dropped it, leaving it in the
|
|
/// default MEDIUM alongside the panels.
|
|
///
|
|
/// That is not a tie: a panel carries nested child frames (level 1+) while the toast's content
|
|
/// belongs to the errors frame itself (level 0), and level outranks insertion in the draw key. So
|
|
/// the toast lost to any open panel — invisible in exactly the state you most want to read it. Same
|
|
/// defect as the party frame's in decision 0597, one stratum up.
|
|
#[test]
|
|
fn an_error_toast_draws_over_an_open_panel_window() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
|
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
|
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
|
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
|
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
|
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
|
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
|
|
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
|
|
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // QuestLogDetail's reward money row (1928)
|
|
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
|
|
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
|
|
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
|
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
|
|
|
// A left-slot panel open, and the toast raised after it — the order that must not decide.
|
|
s.eval::<()>("ShowUIPanel(QuestLogFrame)").unwrap();
|
|
s.fire_event(
|
|
"UI_ERROR_MESSAGE",
|
|
vec![ScriptValue::Str("Out of range.".into())],
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
assert!(s.eval::<bool>("return QuestLogFrame:IsVisible()").unwrap());
|
|
|
|
s.resolve();
|
|
let quads = s.extract();
|
|
const STRATUM_SHIFT: u32 = 60;
|
|
let toast = quads
|
|
.iter()
|
|
.filter_map(|q| match &q.content {
|
|
QuadContent::Text { text: Some(t), .. } if t == "Out of range." => Some(q.z),
|
|
_ => None,
|
|
})
|
|
.min()
|
|
.expect("the toast text must draw");
|
|
let panel_ceiling = quads.iter().map(|q| q.z).filter(|z| *z < toast).count();
|
|
assert!(
|
|
panel_ceiling > 0,
|
|
"sanity: the panel must be drawing something at all"
|
|
);
|
|
// Everything the quest log draws is below the toast, and by STRATUM — not by luck within one.
|
|
//
|
|
// "Above" is measured against **other frames**: UIErrorsFrame's own declared `<FontString>` —
|
|
// the child that supplies the message font and the CENTER justification — is a real region of
|
|
// the frame and sorts above its message bands, exactly as a chat frame's does. It carries no
|
|
// text and draws nothing, so excluding the frame's own quads is what keeps this test about the
|
|
// thing it is named for.
|
|
let above: Vec<&ExtractedQuad> = quads
|
|
.iter()
|
|
.filter(|q| q.z > toast)
|
|
// `RaidWarningFrame` is excluded on exactly the same grounds, and it is the reference's
|
|
// own arrangement rather than ours: it is HIGH + toplevel too (ref RaidWarning.xml:4), so
|
|
// it raises above the toast in z — but it sits BELOW UIErrorsFrame on screen (anchored to
|
|
// its BOTTOM at -10) and its declared `<FontString>` carries no text, so nothing of it is
|
|
// ever painted over the toast. Excluding it keeps this test about a PANEL WINDOW drawing
|
|
// over the toast, which is what it is named for.
|
|
.filter(|q| {
|
|
!matches!(
|
|
s.quad_owner_name(q.target).as_deref(),
|
|
Some("UIErrorsFrame") | Some("RaidWarningFrame")
|
|
)
|
|
})
|
|
.collect();
|
|
assert!(
|
|
above.is_empty(),
|
|
"nothing may draw over the error toast while a panel is open: {above:?}"
|
|
);
|
|
assert_eq!(
|
|
toast >> STRATUM_SHIFT,
|
|
4,
|
|
"HIGH is stratum 4 — the ref's own for UIErrorsFrame"
|
|
);
|
|
}
|