Files
benilla-octo/crates/benilla-ui/src/script/tests/widget_surface.rs
T
gromandClaude Opus 5 75ba592fba ui: the Era map canvas over 1.12's own world map, and the Line region
What 1.12 withholds here is a framework, not a verb. Its world map is a fixed
1002x668 frame showing one zone — twelve tiles, a hit button, POI buttons, two
dropdowns — with no canvas, no pan, no zoom, no pins and no notion of where one
map sits on another. Every map addon written after 4.0 drives the canvas
framework instead, and there is no stock file to migrate from because there is
no stock feature.

The constraint that shaped every decision: `WorldMapFrame` already has eight
1.12 consumers on this install, all anchoring icons to `WorldMapDetailFrame`
and `WorldMapButton`. So nothing is replaced. The canvas is added, and those
two frames are reparented INTO it — at rest the rect they had, at scale 1 with
no pan, so the stock window is pixel-identical, and the payoff is that the 1.12
art and every icon on it now pan and scale WITH the canvas for free.

Clipping comes from a real ScrollFrame: this client clips a scroll child and
every descendant of it from one source, in the draw pass and the hit-test, so
drawing and hitting agree. The base-art layer a canvas addon hides is a proxy
that switches draw layers rather than the frame, because hiding the frame would
take the POI buttons and every addon's icons with it.

Three regressions, each found by measurement, each pinned: the anchor cycle
(container to detail frame to canvas to container — a cycle resolves to
nothing, and the symptom was `GetCenter()` nil inside the stock hover pass); the
base scale (a minScale below 1 opens the map zoomed out of its own window); and
`dataProviders` being a set rather than an array, because callers iterate it
with `pairs` and bind the key — an array handed that loop the number 1.

`CreateLine` is a Texture carrying two endpoints, drawn through the
explicit-corner quad path the cooldown wedge already uses. Nothing stands in
for it: a line between two moving points is a rotated quad whose angle and
length both change. `C_Texture.GetAtlasInfo` answers nil for every name, and
the nil is the answer — but the getter's presence is what makes the common
atlas guard take its fallback branch instead of raising.

The harness grew a second surface, and the finding is worth keeping: the full
screen map hides `UIParent`, so opening it in the ordinary render probe dropped
the corpus's draw column from 37/86 to 18/86 — nineteen unrelated addons. As
its own pass: no addon lost a quad and nineteen gained, pfQuest 374 -> 911,
pfUI 1063 -> 2259.

OpenMapClassic now reads 1/1 on all five columns, stitching 74 of the realm's
own zones. Corpus roster unchanged at 85 ok / 1 fail; the 12 data-half reds
byte-identical to HEAD's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 22:21:35 -07:00

810 lines
41 KiB
Rust

//! The widget-method surface gate, the widget sibling of [`super::reference_surface`]. The
//! reference's 23 widget registrar tables are disjoint, so a class's surface is its own table plus
//! those it inherits ([`WIDGET_CHAINS`]): `0x87c9b8` is the base `Region` map (the 19
//! `REGION_MAP_METHODS`), and `LootButton`'s own `0x847ce4` holds only `SetSlot`.
use std::collections::{BTreeMap, BTreeSet};
use crate::script::{widget_method_census, UiScript};
/// `(class, its own registrar table, the tables it inherits)`. `WorldFrame` is a `Frame` to Lua
/// and `TitleRegion` a bare `Region`; like `TaxiRouteFrame`, neither has a table of its own.
const WIDGET_CHAINS: &[(&str, &str, &[&str])] = &[
("Frame", FRAME, &[REGION]),
("WorldFrame", FRAME, &[REGION]),
("Button", BUTTON, FRAME_CHAIN),
("LootButton", "0x847ce4", &[BUTTON, FRAME, REGION]),
("CheckButton", "0x87bf74", &[BUTTON, FRAME, REGION]),
("EditBox", "0x87bb68", FRAME_CHAIN),
("StatusBar", "0x87b010", FRAME_CHAIN),
("Slider", "0x87b260", FRAME_CHAIN),
("ScrollFrame", "0x87b3c0", FRAME_CHAIN),
("Model", MODEL, FRAME_CHAIN),
("PlayerModel", PLAYER_MODEL, &[MODEL, FRAME, REGION]),
(
"DressUpModel",
"0x84f190",
&[PLAYER_MODEL, MODEL, FRAME, REGION],
),
(
"TabardModel",
"0x84ee40",
&[PLAYER_MODEL, MODEL, FRAME, REGION],
),
("MessageFrame", "0x87b960", FRAME_CHAIN),
("ScrollingMessageFrame", "0x87b5c0", FRAME_CHAIN),
("ColorSelect", "0x87abb0", FRAME_CHAIN),
("SimpleHTML", "0x87ba80", FRAME_CHAIN),
("MovieFrame", "0x87ab4c", FRAME_CHAIN),
("GameTooltip", "0x854198", FRAME_CHAIN),
("Minimap", "0x84c538", FRAME_CHAIN),
("Texture", "0x87c128", &[REGION]),
("FontString", "0x87c1d8", &[REGION]),
// The font object (`CreateFont`) inherits nothing.
("Font", "0x87c7c8", &[]),
("TitleRegion", REGION, &[]),
];
const REGION: &str = "0x87c9b8";
const FRAME: &str = "0x878ec0";
const BUTTON: &str = "0x879d00";
const MODEL: &str = "0x878948";
const PLAYER_MODEL: &str = "0x84f1fc";
const FRAME_CHAIN: &[&str] = &[FRAME, REGION];
/// Reference methods this client does not answer yet: one row per name, its classes and why. The
/// list only shrinks; a name with live callers belongs implemented.
const NOT_YET_ANSWERED: &[(&str, &str, &str)] = &[
// ── The MovieFrame trio ──
// The stock glue `MovieFrame.lua` calls all three; benilla plays no movie yet.
(
"StartMovie",
"MovieFrame",
"the intro-movie screen's own verb; benilla plays no movie yet",
),
("StopMovie", "MovieFrame", "the same, on the skip path"),
(
"EnableSubtitles",
"MovieFrame",
"the same — `MovieFrame.lua:35` passes `GetMovieSubtitles()`",
),
// ── The Minimap's four content setters ──
// The app draws the minimap's blips and arrow itself, never through Lua.
(
"SetArrowModel",
"Minimap",
"app-side art; no caller in the stock UI or either corpus",
),
("SetBlipTexture", "Minimap", "the same"),
("SetIconTexture", "Minimap", "the same"),
("SetPlayerModel", "Minimap", "the same"),
// ── The Button font-object and text-colour family ──
// `button.rs` keeps this font state but publishes none of it; nothing calls these.
(
"GetDisabledFontObject",
"Button CheckButton LootButton",
"the button font-state family; no caller anywhere measured",
),
(
"GetDisabledTextColor",
"Button CheckButton LootButton",
"the same family",
),
(
"GetHighlightFontObject",
"Button CheckButton LootButton",
"the same family",
),
(
"GetHighlightTextColor",
"Button CheckButton LootButton",
"the same family",
),
(
"GetTextFontObject",
"Button CheckButton LootButton",
"the same family",
),
(
"GetPushedTextOffset",
"Button CheckButton LootButton",
"the same family",
),
(
"SetPushedTextOffset",
"Button CheckButton LootButton",
"the same family",
),
// ── Line spacing, on the five classes that carry text ──
(
"GetSpacing",
"EditBox Font MessageFrame ScrollingMessageFrame",
"line spacing is not modelled by `ui_text`'s layout. FontString LEFT THIS LIST: a caller \
was measured (DirectedByRobertBWeide `:101`), and the reason given here — that the \
setter is a layout change rather than a stored value — was the wrong trade. It is both, \
and the half this client can honour is the storing half: the pair round-trips, the \
layout still ignores it, and the addon stops aborting mid-initialisation. The four \
classes left are unmeasured, so they stay",
),
(
"SetSpacing",
"EditBox Font MessageFrame ScrollingMessageFrame",
"the same — see `GetSpacing` above for why FontString is no longer here",
),
// ── EditBox mode read-backs ──
(
"IsAutoFocus",
"EditBox",
"the mode is set and not readable back; no caller anywhere measured",
),
("IsMultiLine", "EditBox", "the same"),
("IsNumeric", "EditBox", "the same"),
("IsPassword", "EditBox", "the same"),
// ── ScrollingMessageFrame's line-position surface ──
(
"GetCurrentLine",
"ScrollingMessageFrame",
"the buffer's position is internal; no caller measured",
),
("GetCurrentScroll", "ScrollingMessageFrame", "the same"),
("GetNumLinesDisplayed", "ScrollingMessageFrame", "the same"),
("SetScrollFromBottom", "ScrollingMessageFrame", "the same"),
// ── The colour picker's HSV pair ──
(
"GetColorHSV",
"ColorSelect",
"the RGB pair is published; nothing measured asks for HSV",
),
("SetColorHSV", "ColorSelect", "the same"),
// ── One texture read-back ──
(
"IsDesaturated",
"Texture",
"`SetDesaturated` is here and not readable back; no caller measured",
),
];
/// Methods answered here that 1.12 does not register on that class, each with why it stays.
const ALLOWED_BEYOND: &[(&str, &str, &str)] = &[
(
"GetItem",
"GameTooltip",
"ClassicAPI: what item this tooltip is showing, off TooltipState::subject",
),
(
"GetSpell",
"GameTooltip",
"ClassicAPI: what spell this tooltip is showing, off TooltipState::subject",
),
(
"HasItem",
"GameTooltip",
"ClassicAPI: whether the subject is an item",
),
(
"HasSpell",
"GameTooltip",
"ClassicAPI: whether the subject is a spell",
),
(
"GetUnit",
"GameTooltip",
"ClassicAPI: the display name and token SetUnit was given",
),
(
"HasUnit",
"GameTooltip",
"ClassicAPI: whether SetUnit left a token",
),
(
"GetUnitGUID",
"GameTooltip",
"ClassicAPI: the name and guid of the unit SetUnit saw, off the new TooltipState::unit_guid — a set-time fact, never a re-resolution of the token, which is what makes it answerable at all (classic_api/tooltip.rs's header). pfUI's modules/tooltip.lua:81 reads the second value and feeds UnitTokenFromGUID",
),
(
"GetGameObject",
"GameTooltip",
"ClassicAPI: the name and OBJECT_FIELD_ENTRY of the world object the plate was built for, off TooltipSubject::GameObject — a set-time fact recorded by world_tooltip_gameobject, the same mechanism GetUnitGUID uses and for the same reason. pfQuest's map.lua:296 reads the second value and keys its object node index by it; its absence was 236 raises in one session, because its reader hangs off a frame parented to GameTooltip and so fires on every tooltip",
),
(
"HasGameObject",
"GameTooltip",
"ClassicAPI: whether the plate is showing a world object, the GetGameObject sibling Has* every other subject already has",
),
(
"SetSpellByID",
"GameTooltip",
"ClassicAPI: fill the plate from a spell id alone. The one Set*ByID the family's \"needs an argument-shape check first\" note was lifted for, and the check is why it is allowed rather than removed: tooltip_spell::set_spell_by_id is the body all four of 1.12's own spell verbs funnel through and it already takes a bare id, so this adds a door onto existing behaviour and no behaviour of its own. SuperCleveRoidMacros/Core.lua:4217 reaches it from pfUI's ButtonEnter for a spell:<id> macro naming a spell in neither spellbook; its absence was 7 raises in one session and 7 action buttons with no tooltip at all",
),
(
"GetOwner",
"GameTooltip",
"ClassicAPI: the frame SetOwner was given, which the stock tooltip already keeps",
),
(
"IsEquippedItem",
"GameTooltip",
"ClassicAPI: whether the shown item is worn, over inventory_slots",
),
(
"SetUnitAura",
"GameTooltip",
"ClassicAPI: the modern spelling of 1.12's own SetUnitBuff/SetUnitDebuff pair, picked by the filter argument instead of by the verb name, and forwarding to them through the same method table so they stay the single implementation. HARMFUL anywhere in the filter selects debuffs, everything else (and an absent filter) helpful, which is modern's own default and makes the composite spellings like \"HELPFUL|PLAYER\" land correctly. pfUI's raid and group frames tooltip every buff icon through it (api/unitframes.lua:67), so its absence raised on every buff hover",
),
(
"SetHyperlinkCompareItem",
"GameTooltip",
"ClassicAPI: fill this shopping tooltip with what is WORN in the slot the link's item would take. Not a new mechanism — 1.12's own SetMerchantCompareItem and SetAuctionCompareItem already drive this exact chain, over the same index-into-candidate-slots grammar and the same CURRENTLY_EQUIPPED latch; this is that body reached from a link instead of a list row (tooltip_item/mod.rs's compare_link_against_worn). pfUI's modules/eqcompare.lua:21,24,72 hooks every Set*Item verb on GameTooltip, so its absence was 12 raises in one session, one per item hover",
),
(
"ClearAttribute",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: removes one key from the same per-frame store",
),
(
"GetAttribute",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: reads the same per-frame store",
),
(
"GetEffectiveAlpha",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: the arena's propagated effective_alpha, where the stock GetAlpha is the frame's own factor",
),
(
"GetRect",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",
"ClassicAPI: left, bottom, width, height from the four stock getters",
),
(
"GetSize",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",
"ClassicAPI (classic_api/frame.rs): the width/height pair, composed from the stock GetWidth/GetHeight",
),
(
"HookScript",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: chains a handler after whatever GetScript answers, through the stock SetScript",
),
(
"IsEventRegistered",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: reads the frame_events set RegisterEvent already fills; all-events mode answers for anything",
),
(
"SetAttribute",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: a per-frame key/value store, which is all this is stripped of secure semantics; the secure drivers are NOT built (see classic_api/frame.rs)",
),
(
"SetAttributeNoHandler",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: identical to SetAttribute here — it differs only in skipping the secure OnAttributeChanged handler, which does not exist",
),
(
"SetFormattedText",
"FontString",
"ClassicAPI: SetText(format(...)), composed from the stock SetText",
),
(
"GetStringHeight",
"FontString",
"ClassicAPI: the laid-out wrapped height, the other half of GetStringWidth's measure — \
AceGUI-3.0's Label and CheckBox call it unguarded",
),
(
"Execute",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): runs a snippet against the header's own shared environment. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"GetFrameRef",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): reads the named frame store. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"SetFrameRef",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): writes it; Cinematic's wheels pass their click-catcher this way (MenuWheel.lua:352). The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"WrapScript",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): attaches a snippet to a child's handler, which may suppress it by returning false (MenuWheel.lua:398). The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"UnwrapScript",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): the pair's other half. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"ClearBindings",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): the binding half, a NO-OP: no override-binding layer here to write into. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"ClearBinding",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"SetBinding",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"SetBindingClick",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"SetBindingSpell",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"SetBindingItem",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"SetBindingMacro",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI (compat/classic_api/secure.rs, which has the whole reasoning): as ClearBindings. The \
family exists so an untainted snippet may call protected functions, and 1.12 has \
neither taint nor protected functions — the restriction has no counterpart here, the \
plumbing does",
),
(
"CreateAnimationGroup",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame FontString Texture",
"ClassicAPI (compat/classic_api/animation.rs, which states what it does and does not do): \
the animation SURFACE with nothing animated — 1.12 has no animation system and this \
client's UI pass has no interpolation to hang one on, so a non-looping group finishes \
the moment it plays and a looping one keeps playing. What it buys is the constructor: a \
nil method aborted Cinematic's whole options panel at Options.lua:7106, over one \
spinner. On both region leaves too, because that spinner's group is on a Texture",
),
(
"SetNormalFontObject",
"Button CheckButton LootButton",
"ClassicAPI: the later-era spelling of 1.12's SetTextFontObject (0x780400) — the one name \
this family left behind, since SetHighlightFontObject (0x780700) and \
SetDisabledFontObject (0x780580) are 1.12's own. Hands out the same function value, so \
the two spellings cannot drift. Ace3 calls it unguarded on every button it builds \
(AceConfigDialog-3.0.lua:586)",
),
(
"IsMouseOver",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame FontString Texture",
"2.0 moved FrameXML's own `MouseIsOver(region, ...)` onto the widget. 1.12 has the \
computation and not the method, so this is a method spelling of a function the player's \
chain already defines — written out rather than forwarded because the argument order \
differs and because a frame may be asked before UIParent.lua has run. On all three \
tables because 2.0 put it on Region, and the body reads nothing a region lacks",
),
(
"CreateLine",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"A `Line` region: two endpoints and a thickness, drawn as the quad between them. 1.12 has \
no line primitive and nothing stands in for one — a line between two MOVING points is a \
rotated quad whose angle and length both change every frame, which no rect-anchored \
texture expresses. Called unguarded at both sites that need it: a flight route \
(Map/Stitch.lua:717) and the map's drawing tool (Map/Sketch.lua:377). Built over the \
stock CreateTexture on the same table, so a line is an ordinary Texture in every respect \
but its geometry, which is what the callers rely on — they paint it with \
SetColorTexture and SetVertexColor like any texture",
),
(
"SetStartPoint",
"Texture",
"A Line's first end. NOT SetPoint: the point names a place on the TARGET and nothing on \
the line, because a line has ends rather than edges. On the Texture table because a Line \
is a Texture here (RegionData::line), so a plain Texture answers it inertly — the same \
sharing the title region already has with the texture methods",
),
(
"SetEndPoint",
"Texture",
"The Line's other end, see SetStartPoint",
),
(
"SetThickness",
"Texture",
"The Line's width in px; floored at 1, the reference's hairline, because a non-finite or \
negative width makes the quad's corners meaningless",
),
(
"GetThickness",
"Texture",
"The read half, so the pair round-trips; 1 by default",
),
(
"AddMaskTexture",
"Texture",
"The map canvas: 1.12 has no texture masking at all, and nothing can stand in for it \
because the point is that a RECTANGULAR texture draws as a non-rectangular shape. A \
seamless world map is that operation and nothing else — a zone's parchment cut to the \
zone's coastline — so without it every zone draws as a paper rectangle over its \
neighbours' oceans and labels. Called unguarded where it is used: Map/Close.lua:162",
),
(
"RemoveMaskTexture",
"Texture",
"The other half of AddMaskTexture, so a caller that swaps a mask can take the old one \
off; without it a re-masked tile keeps the first cut forever",
),
(
"CreateMaskTexture",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"The region AddMaskTexture takes: a Texture that shapes others and never draws itself. \
Built over the stock CreateTexture on the same table, so a mask is an ordinary Texture \
in every other respect. Map/Close.lua:159 builds one before any texture can ask for it",
),
(
"SetRotation",
"Texture",
"1.12 registers SetRotation on PlayerModel alone (0x84f1fc), so a Texture there cannot \
turn. The renderer has carried the angle since the minimap arrow, which the host draws; \
this is the Lua setter for it. The map's own player arrow and its compass rail need it",
),
(
"GetRotation",
"Texture",
"The read half of SetRotation, so the pair round-trips; 0 by default",
),
(
"SetDesaturation",
"Texture",
"The later-era FRACTIONAL form of 1.12's own SetDesaturated. This renderer greys \
all-or-nothing (one material flag, not a blend weight), so a fraction is taken as on \
past the midpoint — an approximation, and the honest one available, since the only use \
of it in the corpus is dimming art the caller has already decided to dim \
(Map/Close.lua desaturates minimap tiles by a fixed amount so the painted map reads \
over them)",
),
(
"GetDesaturation",
"Texture",
"The read half, answering the caller's own fraction rather than the rounded flag",
),
(
"SetHorizTile",
"Texture",
"Repeat the art across the region at its own texel size. 1.12 reaches the same end with \
SetTexCoord(0, n, 0, 1) — UVs past the texture, which is the idiom this renderer keys \
its wrap mode on — but has no switch meaning 'however many times it takes', because \
that count is rect/texels and 1.12's Lua is never told the texel size. Applied: the \
host computes the count once the art has loaded, and an explicit SetTexCoord wins",
),
(
"SetVertTile",
"Texture",
"As SetHorizTile, for the other axis",
),
(
"GetHorizTile",
"Texture",
"The read half of SetHorizTile; false by default",
),
(
"GetVertTile",
"Texture",
"The read half of SetVertTile; false by default",
),
(
"HasFocus",
"EditBox",
"1.12's EditBox table has SetFocus and ClearFocus and no getter at all, so an addon of \
that era keeps its own flag. The fact is already tracked here (Model::focused_editbox, \
the client's own DAT_00cf4dc8) and was simply never published. Called unguarded inside \
an OnEditFocusLost callback (Map/Nav.lua:374), so a nil method raised every time a \
search box lost focus",
),
(
"SetCursorPosition",
"EditBox",
"1.12 has no cursor setter at all — the caret moves only by typing, clicking or \
HighlightText. Lands through HighlightText's own clamp and UTF-8 snap with the selection \
collapsed, so there is one definition of where a caret may sit. Map/News.lua:282 calls \
it to scroll a long read-only link box back to its start",
),
(
"SetPropagateKeyboardInput",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"4.x's switch for whether a frame with an OnKeyDown handler lets the key reach the next \
frame and the binding system. STORED, NOT APPLIED, and the reason is that 1.12's answer \
is always-propagate: its key routing hands the key to the binding system regardless of \
what a handler did, so there is no swallow mode for false to select. Refusing it costs a \
caller its whole key path — Map/Sketch.lua:579 arms a catcher frame and flips the flag \
inside its own handler, so a nil method raises once per keypress",
),
(
"GetPropagateKeyboardInput",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"The read half, so the pair round-trips; true by default, which is what this client does \
unconditionally",
),
(
"CanWordWrap",
"FontString",
"ClassicAPI: the read half of SetWordWrap, so the pair round-trips. True by default, \
which is what this client's layout actually does",
),
(
"SetFixedFrameLevel",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: 9.0 told the frame manager to stop re-deciding a frame's level. There is no \
such manager here — level is whatever SetFrameLevel last wrote and nothing rewrites it — \
so the guarantee already holds and the no-op is the honest answer. Ace3 calls it \
unguarded at AceConfigDialog-3.0.lua:576",
),
(
"SetFixedFrameStrata",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: as SetFixedFrameLevel, for strata. Ace3, AceConfigDialog-3.0.lua:575",
),
(
"SetWordWrap",
"FontString",
"ClassicAPI: 3.x's whether-to-wrap switch, which 1.12 has no spelling for (its \
SetNonSpaceWrap decides WHERE a wrap may fall, not whether one happens). Stored, not \
applied, like SetSpacing and for the same reason — the layout wraps to the declared \
width unconditionally — and kept because the missing setter cost Cinematic its whole \
options panel at Options.lua:4130",
),
(
"RegisterUnitEvent",
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
"ClassicAPI: the stock registration narrowed to named units — same list, same order, and \
tick.rs's walk skips a frame whose row for the event does not name arg1. Addons written \
for it do not guard it: Cinematic/Frames.lua:2611 registers UNIT_AURA for \"player\" at \
file scope, so the nil method stopped the file dead. On every frame kind rather than \
Frame alone because event registration lives on the one shared frame method table, which \
is where 1.12 keeps RegisterEvent too — the two are registered alike or neither is",
),
(
"SetColorTexture",
"Texture",
"ClassicAPI: the modern spelling of SetTexture's colour arm, which 1.12 already has \
(0x770360's 8x8 solid surface; region/paint.rs defaults the alpha to 1 as it does). \
Retail split the two names when SetTexture stopped taking a colour, so every Era-era \
addon writes this one — Cinematic/Options.lua:701 paints its list border with it and \
died at session start without it, and OpenMapClassic its map chrome",
),
(
"SetShown",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",
"ClassicAPI: the Show/Hide pair as one call; frame_state.rs notes 1.12 registers no SetShown. On the two region leaves it is NOT that composition — they have no Show/Hide to compose from (the reference's Region map is geometry and identity only), so classic_api/frame.rs writes the same hidden flag region.rs's own Show/Hide do. pfUI calls it on a FontString: f.group:SetShown(...), api/unitframes.lua:582",
),
(
"SetSize",
"Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame",
"ClassicAPI: the width/height pair, composed from the stock SetWidth/SetHeight",
),
];
/// The reference's widget rows, `table_va -> {name}`.
fn reference_tables() -> BTreeMap<String, BTreeSet<String>> {
let tsv = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../reference/1.12-shapes.tsv"
);
let text = std::fs::read_to_string(tsv).expect("reference/1.12-shapes.tsv");
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for line in text.lines() {
if line.starts_with('#') || line.starts_with("name\t") {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
if f.len() < 5 || f[4] != "widget" {
continue;
}
out.entry(f[3].to_string())
.or_default()
.insert(f[0].to_string());
}
out
}
/// `class -> the effective reference surface` (its own table and everything it inherits).
fn reference_surface_by_class() -> BTreeMap<&'static str, BTreeSet<String>> {
let tables = reference_tables();
assert!(
tables.len() == 23,
"the shapes table gave {} widget registrar tables, not the reference's 23 — it changed \
shape, and every count below is against the wrong denominator",
tables.len()
);
let mut out = BTreeMap::new();
for (class, own, bases) in WIDGET_CHAINS {
let mut set = tables.get(*own).cloned().unwrap_or_default();
for base in *bases {
set.extend(tables.get(*base).cloned().unwrap_or_default());
}
assert!(!set.is_empty(), "{class}: empty reference surface");
out.insert(*class, set);
}
out
}
/// `class -> what this VM answers`, asked of a live instance of each.
fn ours_by_class() -> BTreeMap<String, BTreeSet<String>> {
let script = UiScript::new().expect("VM");
let rows = widget_method_census(&script).expect("census");
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (class, method) in rows {
assert_ne!(
method, "!NOT-INSTANTIABLE",
"{class} could not be instantiated — the census cannot speak for a class it never made"
);
out.entry(class).or_default().insert(method);
}
out
}
/// A declared list, `name -> (classes, reason)`.
fn declared(list: &[(&str, &str, &str)]) -> BTreeMap<String, BTreeSet<String>> {
list.iter()
.map(|(name, classes, _)| {
(
(*name).to_string(),
classes.split_whitespace().map(str::to_string).collect(),
)
})
.collect()
}
/// Render a `class -> names` map as declared-list rows, ready to paste in.
fn as_rows(diff: &BTreeMap<String, BTreeSet<String>>) -> String {
let mut by_name: BTreeMap<&String, Vec<&String>> = BTreeMap::new();
for (class, names) in diff {
for n in names {
by_name.entry(n).or_default().push(class);
}
}
by_name
.iter()
.map(|(name, classes)| {
format!(
" (\"{name}\", \"{}\", \"WHY\"),",
classes
.iter()
.map(|c| c.as_str())
.collect::<Vec<_>>()
.join(" ")
)
})
.collect::<Vec<_>>()
.join("\n")
}
/// Every widget method the 1.12 client registers on a class is answered by ours, or listed.
#[test]
fn every_1_12_widget_method_is_answered() {
let reference = reference_surface_by_class();
let ours = ours_by_class();
let listed = declared(NOT_YET_ANSWERED);
let mut missing: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut total = 0usize;
for (class, want) in &reference {
let have = ours.get(*class).cloned().unwrap_or_default();
total += want.len();
let gap: BTreeSet<String> = want
.iter()
.filter(|n| !have.contains(*n))
.filter(|n| !listed.get(*n).is_some_and(|c| c.contains(*class)))
.cloned()
.collect();
if !gap.is_empty() {
missing.insert((*class).to_string(), gap);
}
}
assert!(
total > 2_000,
"only {total} class/method pairs on the reference side — the chain stopped resolving"
);
assert!(
missing.is_empty(),
"the 1.12 client registers these on a class benilla does not answer them on, and they are \
not listed:\n{}\n\nImplement it, or add the row to NOT_YET_ANSWERED with the reason it \
has not been built.",
as_rows(&missing)
);
// A listed name that is answered now fails, so the list cannot outlive its fix.
let mut healed: Vec<String> = Vec::new();
for (name, classes, _) in NOT_YET_ANSWERED {
for class in classes.split_whitespace() {
if ours.get(class).is_some_and(|h| h.contains(*name)) {
healed.push(format!("{class}:{name}"));
}
}
}
assert!(
healed.is_empty(),
"these are answered now — take them out of NOT_YET_ANSWERED so the list keeps meaning what \
it says: {healed:?}"
);
}
/// Every widget method benilla answers is one 1.12 registers on that class, or a listed exception.
#[test]
fn our_widget_methods_stay_inside_the_1_12_surface() {
let reference = reference_surface_by_class();
let ours = ours_by_class();
let listed = declared(ALLOWED_BEYOND);
let mut beyond: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for (class, have) in &ours {
let Some(want) = reference.get(class.as_str()) else {
panic!("{class} is instantiable here but has no row in WIDGET_CHAINS");
};
let extra: BTreeSet<String> = have
.iter()
.filter(|n| !want.contains(*n))
// Our host bridge and VM internals are marked by prefix, as `reference_surface` does.
.filter(|n| !n.starts_with("Benilla") && !n.starts_with("__benilla_"))
.filter(|n| !listed.get(*n).is_some_and(|c| c.contains(class)))
.cloned()
.collect();
if !extra.is_empty() {
beyond.insert(class.clone(), extra);
}
}
assert!(
beyond.is_empty(),
"benilla answers these on a class the 1.12 client does not register them on:\n{}\n\n\
1.12 is the target. Either remove it, give it its 1.12 spelling, or add \
the row to ALLOWED_BEYOND WITH the reason it has to stay — an addon that \
feature-detects an unexplained superset takes a path we cannot \
honour.",
as_rows(&beyond)
);
let mut gone: Vec<String> = Vec::new();
for (name, classes, _) in ALLOWED_BEYOND {
for class in classes.split_whitespace() {
if !ours.get(class).is_some_and(|h| h.contains(*name)) {
gone.push(format!("{class}:{name}"));
}
}
}
assert!(
gone.is_empty(),
"these are no longer exposed — take them out of ALLOWED_BEYOND: {gone:?}"
);
}