Files
benilla-octo/crates/benilla-app/assets/ui/UiPanels.xml
T
SamandClaude Fable 5 6e3a67e039 your corpse answers the mouse, and a cinematic's last holes close
- a corpse is a body you can hover, cursor and click: it enters the same ray pass as units with the winner switched on type at publish (the reference's own shape — one trace over every object), the cursor classifier is transcribed whole (lootable-by-me → the pickup cursor, grayed by range; else the insignia leg behind the learned-skill latch; your own body falls through to the clear), and right-click is "not mounted AND lootable" → CMSG_LOOT; CORPSE_FIELD_FLAGS bit 5 is the PvP insignia flag, not "reclaimable" as an old note had it; the widening also caught that a GameObject anywhere on screen could steal the click from a nearer corpse, and that a left-click on a body cleared your target as if you had clicked ground
- the cinematic sweep closes eleven holes: a shot plays its sequence band's WIDTH, not its end — two shipped fly-bys band away from zero and ran long with their opening frames frozen (the Night Elf intro measured 102333 → 102000 ms); CMSG_NEXT_CINEMATIC_CAMERA announces every shot, the first included — the old "between shots" reading meant it was sent NEVER, since every 1.12 sequence row carries exactly one camera; the ack invariant is stated once (one CMSG_COMPLETE_CINEMATIC per accepted trigger, over-acking tolerated, under-acking never — a dropped ack despawns the world around you until relog); the hardware cursor hides for the shot; and the music cut lands in the frame that starts it — it was one frame late, exactly long enough for a first login's zone fanfare to start under the narration and be stamped as played
- RunBinding exists, and a fly-by stops handing the world its mouse: CinematicFrame's OnKeyDown hands the SCREENSHOT chord back to its binding through RunBinding, a global we never had, so every press during a fly-by raised and took no picture; and the mouse was fully live under a shot — right-drag turned the character, clicks picked targets through the flying camera, both buttons walked the body — so the pointer now reads as over the UI while a shot plays
- AdvanceTime and ReplaceIconTexture are carved off the bytes: AdvanceTime really is a no-op in the reference (its one reachable call is mov eax,1; ret), ReplaceIconTexture is a material swap over a model's type-14 texture slots that stores nothing on the widget, and SetModel(nil) RAISES — the "documented clear" our binding claimed was invented, not read; and the addon roster now says WHOSE package is short when a .toc names files the folder doesn't hold — nine of the corpus's twenty-nine load failures are the addon's own manifest, which the real client also shrugs off ("Couldn't open %s", carry on), so they stop reading as our defects

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:55:36 +07:00

2101 lines
123 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Ui>
<!--
The UIPanel slot manager (decision 0084 §2) — benilla's own implementation of the
panel-window plumbing the reference keeps in UIParent.lua. Loaded FIRST in the shipped-XML
list (ui_script.rs) so `UIPanelWindows`/`ShowUIPanel`/`HideUIPanel` exist before any panel
frame's OnLoad/OnEvent references them.
Specification: the 1.12 `UIParent.lua` extract (patch-2.MPQ), cited by line number
throughout so every rule below can be checked against it. What is quoted VERBATIM is the
**data** — each `UIPanelWindows` row's `area`/`pushable`/`whileDead`, which IS the
reference's window arbitration (a different number is a different window order), and the
`UISpecialFrames` seeds. The code around that data is ours.
Scope: all four areas are live — `left`, `center`, `full` and `doublewide` — and so is
`UIChildWindows` (decision 1507: all four of the reference's child windows ship now).
`doublewide` was the last one out, on the honest grounds that its one 1.12 tenant is the
auction house and benilla didn't ship one; decision 1511 is the arc that does, so the slice
came with it rather than the window working around it. **`UISpecialFrames` is not ours to
write into** (decision 1206): it is the table an ADDON puts its own window in, and 34 corpus
call sites do.
Rows are keyed by the frame names we actually ship. Most of those ARE the reference's own
(`GossipFrame`, `MerchantFrame`, `LootFrame`); where ours differs, the row is `Benilla*` and
its comment names the reference row it stands in for.
-->
<Script><![CDATA[
-- UIParent.lua l.14-42: UIPanelWindows["FrameName"] = { area, pushable, whileDead }. Only
-- the rows benilla ships today; the ref rows with no benilla window yet (Tabard,
-- Battlefield, WorldStateScore, Minigame) join as their windows land (audited
-- row-by-row against the bytes, decision 1507). Four have since left that list: the
-- doublewide AuctionFrame with decision 1511, PetStableFrame with 1676, HelpFrame with
-- 1673, and Petition + GuildRegistrar with 1672 — all of them are rows below.
UIPanelWindows = {}
UIPanelWindows["GossipFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["MerchantFrame"] = { area = "left", pushable = 0 }
-- The item-text reader (decision 0572, ItemTextFrame.xml): the ref's own l.20 verbatim
-- (`UIPanelWindows["ItemTextFrame"] = { area = "left", pushable = 0 }`). The missing row
-- WAS bug B288 (decision 1507): the reader already opened through ShowUIPanel, but
-- unregistered that call is a bare Show() in no slot — so a quest giver's gossip seated
-- itself at the left slot straight over the open note, page text and greeting interleaved.
-- Registered, the two are ordinary left-slot rivals (both pushable 0): one replaces the
-- other, and the replaced reader's own OnHide sends its CloseItemText().
UIPanelWindows["ItemTextFrame"] = { area = "left", pushable = 0 }
-- The mail window (decision 0544): the reference UIParent.lua l.37 registers MailFrame
-- area="left", pushable=0. The OpenMailFrame is NOT a panel — it is a UIChildWindow
-- (UIParent.lua l.46; 0544 misread that line as UISpecialFrames, 1507 corrects it): shown
-- through ShowUIPanel's bare-Show unregistered branch exactly as the ref shows it
-- (MailFrame.lua l.206), hidden by SetCenterFrame's child-window loop when a frame takes
-- the center seat, and closed with its parent by MailFrame's own OnHide. ESC still closes
-- it on our own hard-listed chain in ToggleGameMenu below — never via UISpecialFrames,
-- the ADDONS' list (1206), where an addon's `tremove` could break the client's own ESC.
UIPanelWindows["MailFrame"] = { area = "left", pushable = 0 }
-- The script error log (decision 1495) — OURS, with no reference row to transcribe. It is
-- registered here for exactly one reason: an unregistered frame takes ShowUIPanel's bare
-- `frame:Show()` branch (no left-slot placement, and `CloseAllWindows` never sees it), so
-- without this row the window would flip IsShown() without landing on screen and ESC would
-- not close it. `pushable = 9` puts it behind everything already registered: a diagnostic
-- must never shove a player's real window out of the slot it wanted. whileDead is ours by
-- the same logic (1507): a Lua error in the death flow is exactly one you need to read.
UIPanelWindows["BenillaScriptLogFrame"] = { area = "left", pushable = 9, whileDead = 1 }
-- The trade window (decision 0592 P1, TradeFrame.xml): the ref's own UIParent.lua l.27
-- registration (`UIPanelWindows["TradeFrame"] = { area = "left", pushable = 1 }`). Without
-- this row ShowUIPanel takes its unregistered branch — a bare frame:Show() with no left-slot
-- placement — so the window flips IsShown() but never lands on screen (the bug: OPEN_WINDOW
-- arrived on both clients, yet no window). pushable=1 lets a pushable=0 panel (a merchant)
-- shove it to center, the reference's browse-while-trading arbitration.
UIPanelWindows["TradeFrame"] = { area = "left", pushable = 1 }
-- The trainer window (decision 0237 phase 3, TrainerFrame.xml): an NPC session exactly like
-- Gossip/Merchant above (area "left", pushable 0) — competes for/replaces them at the slot.
UIPanelWindows["BenillaTrainerFrame"] = { area = "left", pushable = 0 }
-- The bank window (decision 0604 phase 4, BankFrame.xml): the ref's own UIParent.lua l.28
-- registration (`UIPanelWindows["BankFrame"] = { area = "left", pushable = 6 }`), quoted
-- verbatim — the same left-slot competition as the other NPC-session windows above.
UIPanelWindows["BankFrame"] = { area = "left", pushable = 6 }
-- The taxi-map window (decision 0484 phase 2, TaxiFrame.xml): the ref's own UIParent.lua
-- registration line (`UIPanelWindows["TaxiFrame"] = { area = "left", pushable = 0 }`),
-- quoted verbatim — same left-slot competition as Gossip/Merchant/Trainer above.
UIPanelWindows["TaxiFrame"] = { area = "left", pushable = 0 }
-- The stable-master window (decision 1676, PetStable.xml): the ref's own UIParent.lua l.39
-- registration line (`UIPanelWindows["PetStableFrame"] = { area = "left", pushable = 0 }`),
-- quoted verbatim — the same left-slot competition as Gossip/Merchant/Trainer/Taxi above,
-- which is what makes the stable master's gossip window give way to the stable it opens.
-- The reference lists it between MailFrame (l.37) and DressUpFrame (l.41), which is where
-- it sits here too, inside the NPC-session cluster those rows are grouped into.
UIPanelWindows["PetStableFrame"] = { area = "left", pushable = 0 }
-- The auction house (decision 1511, AuctionFrame.xml): the reference's own registration,
-- quoted verbatim from `Blizzard_AuctionUI.lua` l.13 rather than UIParent.lua — the window
-- ships as a loadable addon there, so it registers its own row. **The one and only
-- `doublewide` row in all of 1.12**: an 832-wide window takes the left AND centre seats at
-- once, which is why the slice above exists at all. Its `pushable = 0` is quoted and inert
-- — ShowUIPanel's doublewide arm never consults it (l.392-397).
UIPanelWindows["AuctionFrame"] = { area = "doublewide", pushable = 0 }
-- The dressing room (decision 1060, DressUpFrame.xml): the ref's own UIParent.lua l.41
-- registration (`UIPanelWindows["DressUpFrame"] = { area = "left", pushable = 2 }`), quoted
-- verbatim — pushable=2 lets the NPC-session windows (pushable=0) shove it aside, which is
-- what makes "ctrl-click a merchant's item" show both.
UIPanelWindows["DressUpFrame"] = { area = "left", pushable = 2 }
UIPanelWindows["QuestFrame"] = { area = "left", pushable = 0 }
-- The ref's own l.25 verbatim — whileDead carried since 1507: `UnitIsDead` is a live unit
-- binding now, so the flag decides, and checking your quest log while dead is 1.12's call.
UIPanelWindows["QuestLogFrame"] = { area = "left", pushable = 0, whileDead = 1 }
UIPanelWindows["LootFrame"] = { area = "left", pushable = 7 }
-- The character window (decision 0208 phase 1a, CharacterFrame.xml): the ref's own row is
-- `{ area = "left", pushable = 2 , whileDead = 1}` (UIParent.lua l.19, spacing and all) —
-- NOT the pushable=0 this line carried from 0208 until 1507 re-read the bytes. The 2 is
-- load-bearing arbitration: an NPC session (pushable 0) opening under an open character
-- sheet slides it to the center seat — vendor left, your equipment beside it — instead of
-- replacing it; and whileDead=1 lets you look your corpse's gear over while dead.
UIPanelWindows["CharacterFrame"] = { area = "left", pushable = 2, whileDead = 1 }
-- The inspect window (decision 0631, InspectFrame.xml): the ref's own InspectFrame row
-- (`Blizzard_InspectUI.lua:4` — area "left", pushable 0), so inspecting swaps out whichever
-- left-slot window was open, exactly as the character window does.
UIPanelWindows["BenillaInspectFrame"] = { area = "left", pushable = 0 }
-- The spellbook (decision 0216 §8, SpellBookFrame.xml): the ref's own SpellBookFrame row
-- verbatim (`{ area = "left", pushable = 0, whileDead = 1 }`, UIParent.lua l.21; whileDead
-- carried since 1507) — same left-slot competition as every window above.
UIPanelWindows["SpellBookFrame"] = { area = "left", pushable = 0, whileDead = 1 }
-- The talent window (decision 0304 §4, TalentFrame.xml): the ref's own TalentFrame row
-- verbatim (`{ area = "left", pushable = 6, whileDead = 1 }`, Blizzard_TalentUI.lua:71) —
-- pushable 6 one tier above the plain NPC-session/player-state windows' 0. whileDead was
-- omitted here while `UnitIsDead` didn't exist; it is a live unit binding now, so 1507
-- carries the ref's flag on every row that has one.
UIPanelWindows["BenillaTalentFrame"] = { area = "left", pushable = 6, whileDead = 1 }
-- The social window (decision 0668) — the ref's own registration line verbatim
-- (`UIPanelWindows["FriendsFrame"] = { area = "left", pushable = 0, whileDead = 1 }`,
-- UIParent.lua l.29). whileDead: a corpse run is exactly when you want to whisper someone.
UIPanelWindows["FriendsFrame"] = { area = "left", pushable = 0, whileDead = 1 }
-- The two guild-charter windows (decision 1672), the ref's own UIParent.lua l.33-34
-- verbatim. They are ordinary left-slot rivals of each other and of the gossip menu, all
-- pushable 0 — which is exactly right: selecting the registrar's gossip row makes the
-- server close the menu and push the charter list, so the registrar replacing the gossip
-- window in the same slot IS the handoff. Both must be registered or ShowUIPanel degrades
-- to a bare Show() in no slot, which is what left the item-text reader interleaved with a
-- gossip window in B288 (1507).
UIPanelWindows["GuildRegistrarFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["PetitionFrame"] = { area = "left", pushable = 0 }
-- The macro window (decision 0983, MacroFrame.xml) — the ref's own registration line
-- verbatim (`UIPanelWindows["MacroFrame"] = { area = "left", pushable = 5, whileDead = 1 }`,
-- Blizzard_MacroUI.lua:7). whileDead is the ref's and it earns its keep: writing a macro
-- during a corpse run is a real thing people do.
UIPanelWindows["BenillaMacroFrame"] = { area = "left", pushable = 5, whileDead = 1 }
-- The crafting window (decision 0437 phase 2, TradeSkillFrame.xml): NOT an NPC session (it
-- opens off your own cast, TradeSkillFrame.xml's own header comment) — the ref Lua's own
-- registration line (Blizzard_TradeSkillUI.lua:13) is `{ area = "left", pushable = 3 }`,
-- quoted verbatim.
UIPanelWindows["BenillaTradeSkillFrame"] = { area = "left", pushable = 3 }
-- The Enchanting window (decision 0437 phase 3, CraftFrame.xml): the ref Lua's own
-- registration line (`Blizzard_CraftUI.lua:24`), quoted verbatim — `pushable = 4`, NOT `3`
-- (grepped both ref files directly to confirm: `3` is TradeSkillFrame's own line-13 value,
-- a different window/depth — CraftFrame.xml's own header comment has the same note).
UIPanelWindows["BenillaCraftFrame"] = { area = "left", pushable = 4 }
-- The game menu (decision 0674, GameMenuFrame.xml) — the ref's own registration line
-- verbatim (`UIPanelWindows["GameMenuFrame"] = { area = "center", pushable = 0,
-- whileDead = 1 }`, UIParent.lua l.15), and the FIRST **center**-area row benilla ships.
-- "center" is not a position here, it is a MODE: a native-center frame takes the center
-- slot keeping its own anchor (the menu is screen-CENTER by its own XML), closes every
-- other window on the way in, and — while it holds the slot — makes ShowUIPanel refuse
-- every non-center panel (`CanOpenPanels` below). That is the rule behind "you can't open
-- anything with the ESC menu up". whileDead: dying is exactly when you want to log out.
UIPanelWindows["GameMenuFrame"] = { area = "center", pushable = 0, whileDead = 1 }
-- The options window (decision 0950, OptionsFrame.xml) — the ref's own registration
-- verbatim (`UIPanelWindows["OptionsFrame"] = { area = "center", pushable = 0,
-- whileDead = 1 }`, UIParent.lua l.16): the second native-center row. Same MODE as the
-- menu above — it takes the screen, and dying is exactly when you might want to retune
-- the client. (The ref's l.17-18 register SoundOptionsFrame/UIOptionsFrame the same way;
-- those panels are this one window's category rows now, so the rows have no frames.)
UIPanelWindows["OptionsFrame"] = { area = "center", pushable = 0, whileDead = 1 }
-- The GM help window (HelpFrame.xml) — the ref's own registration verbatim
-- (`UIPanelWindows["HelpFrame"] = { area = "center", pushable = 0, whileDead = 1 }`,
-- UIParent.lua l.35): the THIRD native-center row, and the first one that is not part of
-- the settings shell. Same MODE as the two above — it keeps its own screen-CENTER anchor,
-- closes every other window on the way in, and while it holds the slot ShowUIPanel refuses
-- every non-center panel (`CanOpenPanels` below). That is deliberate here rather than
-- incidental: petitioning a GM is a full stop, not something you do beside your bags.
-- whileDead, because being dead somewhere you cannot leave is one of the things you page a
-- GM about — and it is what lets the ticket toast's HELP_TICKET dialog reach the window
-- from a corpse. (The ref's l.40 WorldStateScoreFrame row is center too; no frame here.)
UIPanelWindows["HelpFrame"] = { area = "center", pushable = 0, whileDead = 1 }
-- (0997's KeyBindingFrame row is gone — decision 1008 folded key bindings into the
-- options window above, the 1.15 layout's own placement; KeyBindingsPage.xml.)
-- The world map (decision 0203 phase 2) — the ref's own line verbatim (UIParent.lua l.30)
-- and the first **full** row. It was a plain toplevel until now, showing itself with a bare
-- `WorldMapFrame:Show()` and so bypassing this manager entirely: with the game menu up it
-- was the ONE window that still opened (director-reported). Routing its toggle through
-- ShowUIPanel is what subjects it to the same rules as everything else.
UIPanelWindows["WorldMapFrame"] = { area = "full", pushable = 0, whileDead = 1 }
-- The cinematic letterbox (CinematicFrame.xml) — the ref's own line verbatim
-- (UIParent.lua l.31). The row is what makes CINEMATIC_START's `ShowUIPanel(this, 1)`
-- actually seat the frame: unregistered, ShowUIPanel takes its bare-`Show()` branch and
-- the bars would appear without the panel manager ever closing what they cover. `full`
-- because a fly-by owns the screen, and no `whileDead` — the ref gives it none, and a
-- cinematic is not something a corpse needs.
UIPanelWindows["CinematicFrame"] = { area = "full", pushable = 0 }
-- UIChildWindows — the ref's own table verbatim (UIParent.lua l.44-50; its comment:
-- windows that "rely on a parent frame to be open. If the parent closes or a pushable
-- frame overlaps them they must be hidden."). Dropped as "none exist yet" until 1507 —
-- all four ship now: the open letter (MailFrame.xml) and the guild pane's three
-- satellites (FriendsFrame.xml). The parent-close half is each parent's own OnHide
-- (MailFrame hides the letter, FriendsFrame_OnHide hides the satellites); this table
-- serves the OVERLAP half — SetCenterFrame's hide loop below — because the child popups
-- sit exactly where the center seat puts a window.
UIChildWindows = {
"OpenMailFrame", "GuildControlPopupFrame",
"GuildMemberDetailFrame", "GuildInfoFrame",
}
-- The real client stores `.left`/`.center`/`.doublewide`/`.fullscreen` as fields directly on
-- the `UIParent` frame table (UIParent.lua l.789-885). benilla has no `UIParent` frame object
-- yet (a later decision's job) — so the two slots this v1 tracks live in a private chunk-local
-- table instead of squatting the `UIParent` global name; a future `CreateFrame("Frame",
-- "UIParent")` then lands cleanly on an empty global rather than silently coexisting with our
-- state. Every function below is defined in this same chunk, so they all close over it.
local slots = { left = nil, center = nil, fullscreen = nil, doublewide = nil }
-- SetLeftFrame(frame) — UIParent.lua l.809-824. Hides the old left occupant, SetPoints +
-- Shows the new one at the left slot's screen anchor (verified: TOPLEFT UIParent, 0, -104).
-- "UIParent" as the relativeTo name doesn't resolve to a real frame yet (none exists) — the
-- engine's SetPoint falls back to the frame's own parent-or-screen default, which for our
-- top-level panel frames IS the screen root, so this already anchors correctly; the string is
-- kept for fidelity/forward-compat with the day a real UIParent frame lands.
function SetLeftFrame(frame)
local oldFrame = slots.left
slots.left = frame
if oldFrame then
oldFrame:Hide()
end
if frame then
frame:SetPoint("TOPLEFT", "UIParent", "TOPLEFT", 0, -104)
frame:Show()
end
end
-- SetCenterFrame(frame, skipSetPoint) — UIParent.lua l.826-850, the `UIChildWindows` hide
-- loop (l.839-846) carried since 1507: a frame ARRIVING at the center seat lands exactly
-- where the child popups sit, so they are put away first. Arriving is the ref's exact
-- trigger — MovePanelToCenter's slide re-seats by direct assignment after a
-- SetCenterFrame(nil), so a PUSHED panel does not run the loop; kept as transcribed.
-- (The ref's `for index, value in UIChildWindows do` is Lua 5.0's iterator-less
-- generic-for, which our VM cannot run — decision 1202; `ipairs` is the same walk.)
function SetCenterFrame(frame, skipSetPoint)
local oldFrame = slots.center
slots.center = frame
if oldFrame then
oldFrame:Hide()
end
if frame then
frame:Show()
if not skipSetPoint then
frame:SetPoint("TOPLEFT", "UIParent", "TOPLEFT", 384, -104)
end
for _, name in ipairs(UIChildWindows) do
local childWindow = getglobal(name)
if childWindow then childWindow:Hide() end
end
end
end
-- MovePanelToLeft() — UIParent.lua l.868-875: the center occupant slides back to the left slot
-- (used when the left slot vacates and a "left-area" frame is holding center).
function MovePanelToLeft()
if slots.center then
SetLeftFrame(nil)
slots.center:SetPoint("TOPLEFT", "UIParent", "TOPLEFT", 0, -104)
slots.left = slots.center
slots.center = nil
end
end
-- MovePanelToCenter() — UIParent.lua l.877-885: the left occupant slides to the center slot
-- (used when a higher-`pushable` frame wants the left spot). `:Raise()` (l.881) has no benilla
-- frame-level equivalent yet — guarded, same posture as the pin's `UnitIsDead` guard below.
function MovePanelToCenter()
if slots.left then
SetCenterFrame(nil)
slots.left:SetPoint("TOPLEFT", "UIParent", "TOPLEFT", 384, -104)
if slots.left.Raise then
slots.left:Raise()
end
slots.center = slots.left
slots.left = nil
end
end
-- GetLeftFrame / GetCenterFrame — UIParent.lua l.916-922.
function GetLeftFrame()
return slots.left
end
function GetCenterFrame()
return slots.center
end
-- SetFullScreenFrame(frame) — UIParent.lua l.852-866. ONE line dropped: the reference also
-- does `UIParent:Hide()`/`:Show()` around the swap, hiding the whole game UI behind the
-- fullscreen frame.
--
-- BOTH of the reasons this note used to give are now FALSE (decision 1724). A real
-- `UIParent` frame exists (UIParent.xml), and the world map is no longer the only `full`
-- row: `CinematicFrame` is a second one, and it paints nothing by design. So this dropped
-- line is exactly what makes a cinematic hide the HUD through `crate::ui_hide::UiHidden`
-- instead of through here — and `UiHidden` also means "the UI takes no mouse", which is
-- ALT-Z's meaning and the reason the world took every click of a fly-by.
--
-- Restoring it needs the HUD parented to UIParent first (the 32 dropped `parent="UIParent"`
-- declarations 1724 measured); hiding UIParent today would reach almost nothing.
function SetFullScreenFrame(frame)
local oldFrame = slots.fullscreen
slots.fullscreen = frame
if oldFrame then
oldFrame:Hide()
end
if frame then
frame:Show()
end
end
function GetFullScreenFrame()
return slots.fullscreen
end
-- SetDoublewideFrame(frame) — UIParent.lua l.788-808. One frame takes the left AND center
-- seats at once: both incumbents are dropped (nil'd first, then hidden — the order matters,
-- since an OnHide that re-enters this manager must not find itself still seated), and the
-- new frame lands at the left slot's own screen anchor, because a doublewide window simply
-- starts where left starts and is wide enough to cover both.
--
-- Note what the reference does NOT do: `HideUIPanel` has no doublewide branch (l.754-786),
-- so nothing ever clears this seat — `UIParent.doublewide` keeps pointing at the auction
-- window forever once it has been opened, and every later ShowUIPanel re-hides an
-- already-hidden frame. We reproduce that rather than inventing a clear: `Hide()` on a
-- hidden frame fires nothing, so the difference is unobservable, and a "fix" here would be
-- a silent divergence in the one function every window's placement runs through.
function SetDoublewideFrame(frame)
local oldLeft = slots.left
local oldCenter = slots.center
slots.doublewide = frame
slots.left = nil
slots.center = nil
if oldLeft then
oldLeft:Hide()
end
if oldCenter then
oldCenter:Hide()
end
if frame then
frame:SetPoint("TOPLEFT", "UIParent", "TOPLEFT", 0, -104)
frame:Show()
end
end
-- GetDoublewideFrame — UIParent.lua l.924-926.
function GetDoublewideFrame()
return slots.doublewide
end
-- The area a frame is REGISTERED for — or nil, which is equally the answer for no frame at
-- all and for a frame this manager has never heard of. Four rules below turn on exactly
-- that question ("is a native-center window holding the screen?", "is the frame parked at
-- center a left-area one that should slide back?"), and all four want an absent frame and
-- an unregistered one to answer the same way. The reference open-codes the pair of nil
-- checks at each site, which is four chances to leave one of them out.
local function AreaOf(frame)
local info = frame and UIPanelWindows[frame:GetName()]
return info and info.area
end
-- CanOpenPanels() — UIParent.lua l.887-910. Its dead-player arms are commented out in the
-- reference itself (both of them, with the note that the fear/out-of-control gate was
-- "previously"); what survives is one rule: **a native-center frame open ⇒ nothing else
-- opens**. The game menu is the only such frame (decision 0674), so this reads: with the
-- ESC menu up, no window opens — not from a keybind, not from an NPC's OPEN_WINDOW.
function CanOpenPanels()
if AreaOf(GetCenterFrame()) == "center" then
return nil
end
return 1
end
-- Everything that can REFUSE an open, asked in the reference's own order (UIParent.lua
-- l.663-689). Answers false when the frame must not open. Not a pure predicate, and that
-- is the point: `force` is precisely the right to throw the incumbent out instead of being
-- turned away, so each refusal and its eviction are one decision and are written as one.
local function AdmitPanel(info, force)
-- A dead player gets only the windows that asked for it (l.663-666) — and hears why:
-- NotWhileDeadError is the binary's 0x48d340 (wow-re §5 cross-check, 2026-08-21 —
-- decision 1507's open item): push id 0x7e into the message catalog, whose row is
-- ERR_PLAYER_DEAD → UI_ERROR_MESSAGE, sound "NONE". Our binding (ui_errors.rs)
-- pushes the same key onto the engine's client-local error queue, which the app
-- resolves against GlobalStrings and fires as the same event. The binding is a pure
-- display trigger — this line, like the reference's, decides WHEN.
if UnitIsDead("player") and not info.whileDead then
NotWhileDeadError()
return false
end
-- l.668-675: nothing opens behind a full-screen frame. The world map is the only
-- `full` row we register, so this reads: nothing opens behind the map.
if GetFullScreenFrame() and info.area ~= "full" then
if not force then
return false
end
SetFullScreenFrame(nil)
end
-- l.677-689: with a native-center frame open, only another center request gets
-- through. Redundant with the caller's CanOpenPanels gate for anything that doesn't
-- force (the reference double-checks it too); kept because it is the ONLY path that
-- can displace the menu, and dropping it would silently turn `force` into a no-op.
-- No benilla caller passes `force` yet.
if AreaOf(GetCenterFrame()) == "center" and info.area ~= "center" then
if not force then
return false
end
SetCenterFrame(nil)
end
return true
end
-- Seating a **left-area** panel (l.719-751). The left slot is what it wants; `pushable`
-- settles what happens to whoever is already sitting there. Read the comparisons as "the
-- higher number is the one that gets moved along" — and two zeroes will not move for each
-- other at all, which is what makes one NPC window replace another outright.
local function SeatLeftAreaPanel(frame, info)
local leftFrame = GetLeftFrame()
if not leftFrame then
SetLeftFrame(frame)
return
end
local leftInfo = UIPanelWindows[leftFrame:GetName()]
local centerFrame = GetCenterFrame()
if centerFrame then
-- Both seats taken (l.744-751): outrank the center occupant and it slides back to
-- the left while we take center; otherwise settle for replacing the left one.
if info.pushable > UIPanelWindows[centerFrame:GetName()].pushable then
MovePanelToLeft()
SetCenterFrame(frame)
else
SetLeftFrame(frame)
end
elseif leftInfo.pushable == 0 and info.pushable == 0 then
-- Neither will budge — two NPC sessions — so we replace it outright (l.729-732,
-- the gossip<->merchant case).
SetLeftFrame(frame)
elseif leftInfo.pushable > info.pushable then
-- The incumbent outranks us, so it is promoted to center and we take the left slot
-- it vacates (l.735-741 — highest priority goes to center).
MovePanelToCenter()
SetLeftFrame(frame)
else
SetCenterFrame(frame)
end
end
-- ShowUIPanel(frame) — UIParent.lua l.649-751, now the WHOLE slice: left, center, full
-- and doublewide (pin §3 steps 1,3-5, plus the native-center mode decision 0674 needed and
-- the `full` slot the world map took in 0203/0674). `doublewide` (l.706-716) arrived with
-- the auction house, decision 1511 — the last arm below is now genuinely unreachable for a
-- registered row and stays only as the defensive fallback its comment says it is.
--
-- Two questions, in order: may it open at all, and where does it sit.
function ShowUIPanel(frame, force)
if not frame or frame:IsVisible() then
return
end
local info = UIPanelWindows[frame:GetName()]
-- l.655-657. Note the reference's own precedence: an UNREGISTERED frame is not gated
-- (the `info and` term), so a bare toplevel still shows with the menu up. The gate is
-- asked unconditionally, because addons replace CanOpenPanels and expect to be asked.
local panelsAllowed = CanOpenPanels()
if not panelsAllowed and info and info.area ~= "center" then
return
end
if not info then
frame:Show()
return
end
if not AdmitPanel(info, force) then
return
end
if info.area == "full" then
-- l.692-696: full-screen frames just replace each other, and they take everything
-- else with them on the way in.
CloseAllWindows()
SetFullScreenFrame(frame)
elseif info.area == "center" then
-- l.697-702: native-center frames likewise replace each other and take the
-- screen. `skipSetPoint` is the load-bearing argument here — the menu keeps its
-- own CENTER anchor instead of being dropped at the left-area seat.
CloseWindows()
CloseAllBags()
SetCenterFrame(frame, 1)
elseif info.area == "doublewide" then
-- l.706-710: it wants both seats, so it takes both and nothing else is consulted
-- — including its own `pushable`, which is why the auction house's registered 0
-- never means anything.
SetDoublewideFrame(frame)
elseif info.area == "left" then
-- l.712-716: a doublewide incumbent is squatting the seat we are about to use, so
-- it goes first. In the reference this is a bare statement between the branches
-- rather than part of one, but every earlier arm has already returned by then, so
-- the LEFT path is the only one that reaches it — which is why it lives here. And
-- since `HideUIPanel` never clears the slot, this is a plain Hide of a frame that
-- may well already be hidden — see SetDoublewideFrame's note.
local doublewide = GetDoublewideFrame()
if doublewide then
doublewide:Hide()
end
SeatLeftAreaPanel(frame, info)
else
-- An area string this manager does not know: defensive fallback only.
frame:Show()
end
end
-- HideUIPanel(frame) — UIParent.lua l.754-786. Which seat is the frame sitting in? That is
-- the whole function: vacate that one. A frame in no seat at all was never this manager's
-- to place, so it is simply hidden.
function HideUIPanel(frame)
if not frame or not frame:IsShown() then
return
end
if frame == GetFullScreenFrame() then
SetFullScreenFrame(nil)
elseif frame == GetCenterFrame() then
SetCenterFrame(nil)
elseif frame == GetLeftFrame() then
-- l.777-782: a LEFT-area panel parked at center (it was pushed there) slides back
-- into the seat we are giving up, rather than being stranded beside an empty left
-- slot. A native-center frame stays where it is.
if AreaOf(GetCenterFrame()) == "left" then
MovePanelToLeft()
else
SetLeftFrame(nil)
end
else
-- Seated nowhere: a plain toplevel, or an addon's frame we never placed.
frame:Hide()
end
end
-- UISpecialFrames (UIParent.lua l.52-55) — **the addon's own ESC list**, and the reason
-- this stopped being "dropped: none exist yet" (decision 1206).
--
-- `tinsert(UISpecialFrames, "MyFrame")` is THE 1.12 idiom for "ESC should close my
-- window", and it is 34 call sites across the corpus. With the table absent that line is
-- `bad argument #1 to 'tinsert' (table expected, got nil)` — which killed 11 addons at
-- load, the second-largest wall in the survey after the Lua-5.0 generic-for.
--
-- Seeded with the reference's own two entries — **both** of them now. 1206 seeded only
-- `ItemRefTooltip`, and its Rejected section wrote down why, verbatim: *"Listing
-- `ColorPickerFrame`. The reference seeds it; we do not ship one, and a name that resolves
-- to nothing is skipped harmlessly — but seeding a frame we lack would be writing a claim
-- we cannot honour."* ColorPickerFrame.xml ships it, so the claim is honourable now, and
-- the entry is the reference's own rather than one of ours.
--
-- Note it does NOT contradict the paragraph above (our own escapable toplevels stay
-- hard-listed in the ESC chain). The concern there is an addon's `tremove` breaking the
-- CLIENT's own ESC; here the ESC chain carries the colour picker on its own rung — and has
-- to, because ESC on this window must CANCEL (restore the colour), which this walk's plain
-- `Hide` cannot do. So the table entry is not what makes ESC work; it is what makes
-- `CloseWindows` put the picker away when something ELSE closes the world's windows —
-- opening the game menu, a `CloseAllWindows()` — which is exactly the reference's own
-- behaviour on that path, cancel-less Hide and all.
UISpecialFrames = { "ItemRefTooltip", "ColorPickerFrame" }
-- CloseWindows(ignoreCenter) — UIParent.lua l.928-957. Both once-dropped slices are now
-- carried: the UISpecialFrames loop at l.947-954, and the doublewide seat (decision 1511).
function CloseWindows(ignoreCenter)
local fullScreenFrame = GetFullScreenFrame()
local doublewideFrame = GetDoublewideFrame()
local centerFrame = GetCenterFrame()
local leftFrame = GetLeftFrame()
HideUIPanel(leftFrame)
HideUIPanel(fullScreenFrame)
-- l.938. HideUIPanel has no doublewide branch, so this lands on its "seated nowhere"
-- arm and is a plain Hide; the seat itself is never vacated (SetDoublewideFrame's note).
HideUIPanel(doublewideFrame)
-- l.934, quoted as-is: `found` deliberately does NOT consider the doublewide frame,
-- even though the line above closes it. Reproduced rather than corrected — ESC's "did
-- I close anything?" answer is this value, and changing it would change which key
-- press opens the game menu. (Computed off the locals read before any Hide landed.)
local found = leftFrame or centerFrame or fullScreenFrame
-- `ignoreCenter` spares a NATIVE-center frame and nothing else: a left-area panel
-- merely parked at the center seat closes with the rest of them.
if centerFrame and not (ignoreCenter and AreaOf(centerFrame) == "center") then
HideUIPanel(centerFrame)
end
-- The special frames, the reference's own loop (l.947-954): a plain Hide, never
-- HideUIPanel, because these hold no panel slot. Its `for index, value in
-- UISpecialFrames do` is Lua 5.0's iterator-less generic-for, which our VM cannot run
-- (decision 1202) — `ipairs` is the same walk in the dialect we have.
for _, name in ipairs(UISpecialFrames) do
local frame = getglobal(name)
if frame and frame:IsVisible() then
frame:Hide()
found = 1
end
end
return found
end
-- CloseAllWindows(ignoreCenter) — UIParent.lua l.970-982. The real fn first loops
-- ContainerFrame1..NUM_CONTAINER_FRAMES to hide any open bags, then calls CloseWindows() for
-- the panel slots, returning whether anything closed. benilla ships a single backpack window
-- (BenillaBagFrame, decision 0068 T2), so that loop collapses to hiding it. The bag is NOT a
-- UIPanel (no slot registration) — it must be closed here explicitly, exactly as the real
-- client closes its ContainerFrames outside the slot system. Naming BenillaBagFrame here
-- mirrors UIParent.lua naming ContainerFrame in the very same function.
-- The five bag windows (the real fn's ContainerFrame1..NUM_CONTAINER_FRAMES): the backpack
-- plus the four equipped-bag windows. Chunk-LOCAL rather than BagFrame.xml's own
-- BENILLA_BAG_WINDOWS global, so every user below stays nil-safe on a surface without the
-- bag file (a headless panel test) — each still resolves per window through `getglobal`.
-- (It was three identical inline copies until the game menu needed a fourth.)
local BAG_WINDOWS = { "BenillaBagFrame", "BenillaBagFrame1", "BenillaBagFrame2",
"BenillaBagFrame3", "BenillaBagFrame4" }
-- ESC closes the KEYRING too, but nothing else here does. The ref's split, exactly: its
-- CloseAllWindows loops every ContainerFrame and hides whatever is shown (the keyring
-- included), while CloseAllBags/OpenBackpack/CloseBackpack are id-driven over the real bags
-- and never see container -2 (decision 0765). So the keyring is appended for the ESC path
-- alone — a vendor opening your bags must not open your keyring with them.
local ESC_CLOSES = { "BenillaBagFrame", "BenillaBagFrame1", "BenillaBagFrame2",
"BenillaBagFrame3", "BenillaBagFrame4", "BenillaKeyRingFrame" }
function CloseAllWindows(ignoreCenter)
local bagsVisible = nil
-- Hide every open bag window. Now that 'B' opens all bags at once (BagFrame.xml
-- BenillaBagToggle_OnClick), ESC must close all of them, not just the backpack. The bag
-- windows are NOT UIPanels (no slot registration) — they must be closed here explicitly,
-- exactly as the real client closes its ContainerFrames outside the slot system.
for _, name in ipairs(ESC_CLOSES) do
local bag = getglobal(name)
if bag and bag:IsShown() then
bag:Hide()
bagsVisible = 1
end
end
local windowsVisible = CloseWindows(ignoreCenter)
return bagsVisible or windowsVisible
end
-- CloseAllBags() — the ref ContainerFrame.lua l.702-707 (`CloseBackpack()` + `CloseBag(1..N)`),
-- called by ShowUIPanel's native-center branch above. Distinct from CloseBackpack below on
-- purpose: this is the UNCONDITIONAL close (the ref's CloseBag ignores the was-open memory),
-- because a center frame takes the whole screen — nothing is being "restored afterwards"
-- the way a vendor window restores the bags it opened.
function CloseAllBags()
for _, name in ipairs(BAG_WINDOWS) do
local bag = getglobal(name)
if bag and bag:IsShown() then
bag:Hide()
end
end
end
-- OpenBackpack / CloseBackpack — the real ContainerFrame.lua globals (l.187-217) the
-- vendor/bank/mail/trade windows call from their OnShow/OnHide so your bags open with them
-- (playing igBackPackOpen/Close alongside the window's own panel sound — the two "go
-- together"), and you can sell/deposit from the open bag. Homed here in benilla's panel-glue
-- file (rather than the bag's own, as in the real client) so every window that opens can call
-- them regardless of FrameXML load order — the windows are looked up by name. The real fn
-- tracks `backpackWasOpen` so a bag you already had open is LEFT open on the window's close;
-- benilla stores that on each frame (`wasOpen`). benilla DIVERGES from the ref here on
-- purpose: the real OpenBackpack touches only the backpack, but ours opens EVERY equipped
-- bag with the window (director's call — decision 0561; its stated reason was matching the
-- 'B' all-bags toggle, which 1494 split apart — the call itself stands on its own),
-- so the names keep the ref's caller contract while the body loops the five windows. Every
-- step is guarded, so a surface without the bag file — a headless panel test — no-ops
-- rather than erroring.
function OpenBackpack()
for bagId = 0, 4 do
-- The backpack (bag 0) always exists; an equipped-bag window only when a bag is in
-- the slot (the same existence test as BagFrame.xml's OpenAllBags).
local exists = bagId == 0
or (GetContainerNumSlots and GetContainerNumSlots(bagId) > 0)
local bag = exists and getglobal(BAG_WINDOWS[bagId + 1])
if bag then
if bag:IsShown() then
bag.wasOpen = 1 -- already open: leave it, and don't close it on window hide
else
bag.wasOpen = nil
BenillaBagWindow_Repaint(bag)
bag:Show() -- OnShow → igBackPackOpen
end
end
end
end
function CloseBackpack()
for _, name in ipairs(BAG_WINDOWS) do
local bag = getglobal(name)
if bag and bag:IsShown() and not bag.wasOpen then
bag:Hide() -- OnHide → igBackPackClose
end
end
end
-- ToggleGameMenu(clicked) — UIParent.lua l.1465-1497, the ESCAPE binding's action
-- (TOGGLEGAMEMENU), run by the host (crates/benilla ui_script/input.rs) when no EditBox
-- consumed the key, and by the Main Menu micro button with `clicked = 1`. It was
-- benilla's own `BenillaOnEscape` while there was no menu to open (decision 0674 retires
-- that name — this IS the reference function now, and an addon-visible one).
--
-- TWO behaviours in one function, which is the reference's own shape:
-- · `clicked` (the micro button, l.1466-1480) — a TOGGLE. Menu up ⇒ close it; menu down ⇒
-- CloseMenus() + CloseAllWindows() first, then open. It never cancels a cast or drops a
-- target: clicking a button is not "get me out of this".
-- · no argument (the ESC key, l.1482-1496) — the LADDER, one eater per press, in order:
-- if ( StaticPopup_EscapePressed() ) then
-- elseif ( OptionsFrame:IsVisible() ) then OptionsFrameCancel:Click()
-- elseif ( GameMenuFrame:IsVisible() ) then PlaySound("igMainMenuQuit"); HideUIPanel(GameMenuFrame)
-- elseif ( CloseMenus() ) then
-- elseif ( SpellStopCasting() ) then
-- elseif ( SpellStopTargeting() ) then
-- elseif ( CloseAllWindows() ) then -- l.1491
-- elseif ( ClearTarget() ) then
-- else PlaySound("igMainMenuOpen"); ShowUIPanel(GameMenuFrame) -- l.1494-1495
-- The ORDER is the law (the director's two-press report, decision 0449): ESC mid-cast
-- cancels the cast and ONLY the cast; the target drops on a later press once nothing
-- else claims it; and only a press with NOTHING left to eat opens the menu.
--
-- Every reference branch is present now (the OptionsFrame rung landed with decision
-- 0950 — ours hides rather than cancel-clicks; its own comment below says why). ONE
-- benilla ADDITION, ahead of everything: a held cursor item is
-- dropped (ClearCursor). The cursor-item model (decision 0068 T2) has no real-client ESC
-- handling to mirror — vanilla leaves the item on the cursor — so this is a deliberate
-- benilla convenience (ESC as the universal "get me out" key). Guarded so it stays inert
-- were the cursor slice ever absent. Three further benilla rungs sit between the popup and
-- CloseMenus (stack split, world map, open mail); each is commented at its own line with
-- the reference mechanism it stands in for.
function ToggleGameMenu(clicked)
-- Looked up rather than indexed off the global: a headless panel test loads this file
-- without GameMenuFrame.xml behind it, and the reference's bare `GameMenuFrame:` would
-- abort the whole chain there (the `CloseMenus and` guard two rungs down, same reason).
local menu = getglobal("GameMenuFrame")
-- The micro button's toggle (l.1466-1480). Its own first move is the ref's too
-- (l.1467-1469): an open options window is put away before the menu toggles.
if clicked then
local options = getglobal("OptionsFrame")
if options and options:IsVisible() then
HideUIPanel(options)
end
if menu and menu:IsVisible() then
PlaySound("igMainMenuQuit")
HideUIPanel(menu)
elseif menu then
if CloseMenus then CloseMenus() end
CloseAllWindows()
PlaySound("igMainMenuOpen")
ShowUIPanel(menu)
end
return
end
if CursorHasItem and CursorHasItem() then
ClearCursor()
end
-- An open escapable dialog eats the ESC first (the ref's StaticPopup_EscapePressed —
-- defined with the popup engine below): OnCancel(data, "clicked") + hide, but ONLY for
-- hideOnEscape entries, so e.g. the DEATH release popup ignores ESC like the ref's.
if StaticPopup_EscapePressed() then
return
end
-- The colour picker next (ColorPickerFrame.xml). The reference hooks ESC on the frame
-- itself (`<OnKeyDown>` l.177-184, whose ESCAPE arm is `HideUIPanel(this)` +
-- `cancelFunc(previousValues)`); that frame does not transcribe its OnKeyDown (see its
-- own header — the engine can deliver one since 1319; the deferral is now about the
-- existence gate, not the engine), so it routes through this shared chain like the
-- world map does. **Cancel-CLICK, not hide** — the ref's own arm restores the previous colour, and
-- clicking the real button is how the options rung below already spells this (the ref's
-- `OptionsFrameCancel:Click()`), so there is one cancel path, not two.
local picker = getglobal("ColorPickerFrame")
if picker and picker:IsVisible() then
ColorPickerCancelButton:Click()
return
end
-- The options window next (l.1483-1484, live since decision 0950 — this rung sat
-- unfilled while GameMenuFrame.xml's disabled buttons were the gap). The ref
-- cancel-clicks (`OptionsFrameCancel:Click()`); ours hides: benilla applies changes
-- live, there is no pending-change model for a Cancel to roll back, so putting the
-- window away IS the whole gesture — and since 1008 that covers the Keybindings
-- page too (live-commit; its OnHide hook disarms a straggling bind capture, and
-- while a capsule IS armed the host's capture seam swallows ESC before this chain
-- ever exists — the press becomes a BIND, 1.12's law). Looked up like the menu
-- above (a headless panel test loads this file without OptionsFrame.xml behind it).
local options = getglobal("OptionsFrame")
if options and options:IsVisible() then
HideUIPanel(options)
return
end
-- The menu itself closes next (l.1485-1487) — before every close/cancel rung below,
-- so ESC with the menu up only puts the menu away. Note the sound: closing by ESC
-- plays igMainMenuQuit, the same kit the Exit Game button uses — the reference's
-- choice, not a copy/paste slip here.
if menu and menu:IsVisible() then
PlaySound("igMainMenuQuit")
HideUIPanel(menu)
return
end
-- The stack-split spinner next (decision 0216 §6/slice 2, StackSplit.xml): the real
-- client's own StackSplitFrame hooks ESC directly (its OnKeyDown, `enableKeyboard="true"`)
-- — this engine has no plain-frame keyboard capture (StackSplit.xml's own header comment),
-- so it routes through this shared chain instead. Just hides (StackSplitFrame_OnHide
-- clears the owner's hasStackSplit flag) — no accept, matching a real Cancel click.
local split = getglobal("StackSplitFrame")
if split and split:IsVisible() then
split:Hide()
return
end
-- The fullscreen world map next (the ref's OnKeyDown ESCAPE arm — WorldMapFrame.xml
-- l.628; ours routes through this chain rather than transcribing that frame's own
-- OnKeyDown, which the engine could deliver since 1319).
local worldmap = getglobal("WorldMapFrame")
if worldmap and worldmap:IsVisible() then
-- HideUIPanel, not a bare Hide: the map holds the full-screen slot (decision 0203's
-- `full` row, wired 0674), and hiding it behind the manager's back would leave the
-- slot pointing at an invisible frame — which then refuses every future panel.
HideUIPanel(worldmap)
return
end
-- The open-letter toplevel (decision 0544): the ref registers OpenMailFrame in
-- UISpecialFrames (UIParent.lua l.46), so ESC closes it BEFORE the mail panel itself —
-- routed through this chain since this v1 drops the UISpecialFrames table. Its OnHide
-- runs the taken-letter auto-delete + close sound (MailFrame.lua OpenMailFrame_OnHide).
local openmail = getglobal("OpenMailFrame")
if openmail and openmail:IsVisible() then
HideUIPanel(openmail)
return
end
-- The ref chain's CloseMenus leg (UIParent.lua l.1488, defined with the dropdown
-- engine): an open right-click/dropdown menu closes and eats the press — BEFORE the
-- cast cancel, so ESC with a menu up spares a running cast, exactly the ref order.
-- Guarded like CursorHasItem above: inert when the dropdown slice isn't loaded.
if CloseMenus and CloseMenus() then
return
end
-- The ref chain's SpellStopCasting leg (UIParent.lua l.1489): ESC stops the FIRST
-- of {running auto-repeat, in-flight cast} and eats the press — windows close on
-- the NEXT press, exactly the reference order. A CHANNEL doesn't answer (nil): the
-- ref's 0x6e6e80 can't stop one — the vanilla "/stopcasting can't stop a channel"
-- quirk (wow-re esc-stopcasting.md, decision 0454) — so mid-channel the press
-- falls through. Truthy only when something was actually stopped (the host's
-- stoppable mirror), so an idle ESC still falls through to CloseAllWindows.
if SpellStopCasting() then
return
end
-- The ref chain's SpellStopTargeting leg (UIParent.lua l.1490, decision 0792): ESC
-- with the ground-targeting cursor up cancels the targeting — word cleared, no
-- packet — and eats the press. After the cast rung, exactly the artifact's order
-- (while merely targeting nothing is in flight, so SpellStopCasting answered nil).
if SpellStopTargeting() then
return
end
-- CloseAllWindows eats the press when it closed anything (l.1491); only a press with
-- nothing left to close reaches ClearTarget (l.1492) — the ref's last eater, and the
-- ONLY route to the target drop: the raw-key Esc clear the app used to run beside
-- this chain double-fired on the first press (decision 0449 retires it).
if CloseAllWindows() then
return
end
if ClearTarget() then
return
end
-- Nothing left to eat (l.1493-1495): the press opens the menu. ClearTarget answering
-- nil is what gets us here — which is why its `1`/nil return is load-bearing, and why
-- the ladder above must never return an unconditional truthy.
if menu then
PlaySound("igMainMenuOpen")
ShowUIPanel(menu)
end
end
-- ── The inspect-cursor pair (ref UIParent.lua l.1805-1817) ────────────────────────────
-- `CursorUpdate` is the ctrl-hover "can I inspect this?" cursor swap, and `CursorOnUpdate`
-- is the OnUpdate wrapper that only re-runs it while the tooltip is still ours. Both are
-- **FrameXML, not engine** (`reference/1.12-globals.tsv` marks them `framexml`, while
-- `ResetCursor` and `ShowInspectCursor` beside them are `engine`) — so they are ours to
-- write, and every piece they reach for already resolves here: `IsControlKeyDown`,
-- `ShowInspectCursor`, `ResetCursor` and `GameTooltip:IsOwned` are all present.
--
-- Two corpus addons call `CursorUpdate` (AdvancedTradeSkillWindow, WhoHas) and one calls
-- `CursorOnUpdate`. It is also a blocker — one of several — for two reference templates
-- addons inherit (`GroupLootFrameTemplate`, `ContainerFrameItemButtonTemplate`), and it is
-- called by the SOURCED `ContainerFrame.lua`'s keyring fork.
--
-- `this.hasItem` is read, not required: the reference tests it as a plain truthy field the
-- hovering frame sets for itself, so a frame that never sets it simply takes the
-- `ResetCursor` arm — which is why this is safe to ship before anything sets it.
function CursorUpdate()
if ( IsControlKeyDown() and this.hasItem ) then
ShowInspectCursor()
else
ResetCursor()
end
end
function CursorOnUpdate()
if ( GameTooltip:IsOwned(this) ) then
CursorUpdate()
end
end
-- SetDesaturation(texture, desaturation) — ref UIParent.lua l.1499-1510, plus two guards
-- it does not have: a nil texture, and a texture carrying no `SetDesaturated` method at
-- all (the reference calls it unconditionally, l.1501; ours keeps the guard for a region
-- leaf that never grew the verb). Since decision 1327 the engine HAS a desaturating
-- shader, so `shaderSupported` is true and the greyscale arm is the one taken — the
-- vertex-colour branch below is now the ref's own no-shader fallback, kept because it is
-- the ref's and because a texture without the method still has to grey somehow. Written as
-- the wrapper rather than open-coded at each call site precisely so that flip was one
-- function. First caller here is the game menu's bag-bar greying (decision 0674);
-- PartyFrame.xml's disconnected-member branch has been CALLING this global since decision
-- 0434 with nothing defining it — an error thrown every time a party member went offline,
-- which is why it went unnoticed.
function SetDesaturation(texture, desaturation)
if not texture then return end
local shaderSupported = texture.SetDesaturated and texture:SetDesaturated(desaturation)
if not shaderSupported then
if desaturation then
texture:SetVertexColor(0.5, 0.5, 0.5)
else
texture:SetVertexColor(1.0, 1.0, 1.0)
end
end
end
-- ── The shared dialog IS the ref's StaticPopup engine (decision 0308 §3) ─────────────────
-- Re-expressed from the patch-chain StaticPopup.lua (mpqx extraction, decision 0246):
-- the StaticPopupDialogs registry + StaticPopup_Show(which, arg1, arg2) + the OnUpdate
-- countdown/StartDelay machinery — the death arc's dialogs exercise nearly all of it, and
-- StaticPopup_Show/StaticPopupDialogs are 1.14 addon-API surface (0068). v1 scope:
-- · TWO dialog instances (STATICPOPUP_NUMDIALOGS = 2; the ref runs 4) — the deepest
-- nesting exercised so far is decline-res re-showing DEATH (Show precedes the click's
-- hide, so a free second instance is required). Grow toward 4 when a third coexists.
-- · info fields honored: text, button1, button2, DisplayButton2, OnShow(data),
-- OnHide(data), OnAccept(data, data2) (truthy return = keep open — RECOVER_CORPSE
-- waits for the server, XP_LOSS's first Accept re-asks), OnCancel(data, reason)
-- (reason ∈ "clicked"|"timeout"|"override"), OnUpdate(elapsed, dialog), timeout,
-- StartDelay, delayText, sound, whileDead, hideOnEscape, cancels, showAlert (the
-- 420 widen + DialogAlertIcon, decision 1068).
-- · hasEditBox + maxLetters + EditBoxOnEnterPressed are live (decision 0668's
-- ADD_FRIEND/ADD_IGNORE are the first customers): the box shows, caps its length,
-- opens empty, and re-anchors the buttons under itself.
-- · hasWideEditBox is live too (the guild arc's SET_GUILDMOTD / SET_GUILDPLAYERNOTE /
-- SET_GUILDOFFICERNOTE, registered in FriendsFrame.xml): the 350x64 centred box
-- replaces the narrow one, and the dialog widens to 420. See the widen arm below for
-- the reference's own oddity — the widen is keyed on `which`, not on hasWideEditBox.
-- · hasMoneyFrame is live (decision 1580's CONFIRM_TALENT_WIPE is the first customer,
-- and the reference's other five are all price confirms): a SmallMoneyFrameTemplate
-- child under the text, shown per entry, with the buttons dropping to -24 to clear it
-- and the frame growing 16px. The AMOUNT is not the engine's — the entry's own
-- OnShow/the event arm calls MoneyFrame_Update, exactly as the reference does.
-- · interruptCinematic is LIVE as of the cinematic arc: a dialog without it is
-- refused while one plays, the ref's own l.1454 gate (it was inert only while
-- 0196 acked every cinematic instantly, so nothing could be playing).
-- · Accepted but v1-inert: exclusive, notClosableByLogout, preferredIndex.
-- · text_arg1/2 stash on the DIALOG frame (the ref stashes them on the Text region;
-- our regions don't carry Lua fields).
-- · GetText("SECONDS"/"MINUTES") localization collapses to English singular/plural.
-- · Button widths stay the template's fixed 120 (the ref widens past 120/110 via
-- GetTextWidth; the widen law joins when a longer label first needs it).
-- · Resize runs per tick while shown, not only on text swaps: GetHeight's measure has a
-- frame of latency (the measured-width convention below), so the height self-corrects
-- the frame after any text change.
STATICPOPUP_NUMDIALOGS = 2
StaticPopupDialogs = {}
local function StaticPopupTimeUnit(n, minutes)
if minutes then
if n == 1 then return "minute" end
return "minutes"
end
if n == 1 then return "second" end
return "seconds"
end
function StaticPopup_FindVisible(which)
for index = 1, STATICPOPUP_NUMDIALOGS do
local frame = getglobal("StaticPopup" .. index)
if frame:IsShown() and frame.which == which then
return frame
end
end
return nil
end
-- Ref l.1870-1877 (returns the frame NAME — the ref's contract).
function StaticPopup_Visible(which)
local frame = StaticPopup_FindVisible(which)
if frame then
return frame:GetName()
end
return nil
end
function StaticPopup_Hide(which)
for index = 1, STATICPOPUP_NUMDIALOGS do
local frame = getglobal("StaticPopup" .. index)
if frame:IsShown() and frame.which == which then
frame:Hide()
end
end
end
-- Ref l.1428-1438, all three legs.
--
-- Note the ref measures the NARROW `EditBox` for the height even when the shown box is the
-- wide one — the wide box is 64 tall, anchored CENTER, and floats over a frame sized for a
-- 32-tall box. Transcribed as written: the guild MOTD dialog's look is that arithmetic.
--
-- The money leg does NOT measure the coin row either: it is the plain height plus a flat
-- 16, which is the room the -24 button drop below leaves for it. Same arithmetic, same
-- reason — the ref sizes these from constants, not from what is in the frame.
function StaticPopup_Resize(dialog, which)
local text = getglobal(dialog:GetName() .. "Text")
local button1 = getglobal(dialog:GetName() .. "Button1")
local editBox = getglobal(dialog:GetName() .. "EditBox")
local info = StaticPopupDialogs[which]
if info and info.hasEditBox then
dialog:SetHeight(16 + text:GetHeight() + 8 + editBox:GetHeight() + 8
+ button1:GetHeight() + 16)
elseif info and info.hasMoneyFrame then
dialog:SetHeight(16 + text:GetHeight() + 8 + button1:GetHeight() + 32)
else
dialog:SetHeight(16 + text:GetHeight() + 8 + button1:GetHeight() + 16)
end
end
-- Ref l.1441-1700. Returns the dialog, or nil when refused (whileDead gate / no free
-- instance) — calling info.OnCancel() on refusal exactly as the ref does.
function StaticPopup_Show(which, text_arg1, text_arg2, data)
local info = StaticPopupDialogs[which]
if not info then
return nil
end
-- The whileDead gate (ref l.1447-1452). UnitIsDeadOrGhost lands with the death arc's
-- ghost predicate — guarded like CursorHasItem in ToggleGameMenu until then.
if UnitIsDeadOrGhost and UnitIsDeadOrGhost("player") and not info.whileDead then
if info.OnCancel then
info.OnCancel()
end
return nil
end
-- The cinematic gate (ref l.1454-1459), the whileDead gate's twin: a fly-by owns the
-- screen, so a dialog that has not declared itself worth interrupting one is cancelled
-- rather than raised over it. `InCinematic` is guarded like the predicate above so a
-- VM without the binding behaves as it did before.
if InCinematic and InCinematic() and not info.interruptCinematic then
if info.OnCancel then
info.OnCancel()
end
return nil
end
-- Showing X hides a visible X.cancels with an "override" cancel (ref l.1470-1485).
if info.cancels then
local frame = StaticPopup_FindVisible(info.cancels)
if frame then
frame:Hide()
local OnCancel = StaticPopupDialogs[frame.which].OnCancel
if OnCancel then
OnCancel(frame.data, "override")
end
end
end
-- Showing DEATH sweeps every visible non-whileDead dialog (ref l.1501-1512).
if which == "DEATH" then
for index = 1, STATICPOPUP_NUMDIALOGS do
local frame = getglobal("StaticPopup" .. index)
if frame:IsShown() and not StaticPopupDialogs[frame.which].whileDead then
frame:Hide()
local OnCancel = StaticPopupDialogs[frame.which].OnCancel
if OnCancel then
OnCancel(frame.data, "override")
end
end
end
end
-- A visible same-type dialog is overridden in place; else take a free instance
-- (ref l.1515-1553).
local dialog = StaticPopup_FindVisible(which)
if dialog then
if info.OnCancel then
info.OnCancel(dialog.data, "override")
end
dialog:Hide()
end
if not dialog then
for i = 1, STATICPOPUP_NUMDIALOGS do
local frame = getglobal("StaticPopup" .. i)
if not frame:IsShown() then
dialog = frame
break
end
end
end
if not dialog then
if info.OnCancel then
info.OnCancel()
end
return nil
end
-- The text. Countdown/delay dialogs start blank — their OnUpdate writes it
-- (ref l.1556-1574's which-list, verbatim).
local text = getglobal(dialog:GetName() .. "Text")
if (which == "DEATH") or (which == "CAMP") or (which == "QUIT")
or (which == "DUEL_OUTOFBOUNDS") or (which == "RECOVER_CORPSE")
or (which == "RESURRECT") or (which == "RESURRECT_NO_SICKNESS")
or (which == "INSTANCE_BOOT") or (which == "CONFIRM_SUMMON")
or (which == "AREA_SPIRIT_HEAL") then
text:SetText(" ")
dialog.text_arg1 = text_arg1
dialog.text_arg2 = text_arg2
else
text:SetText(format(info.text, text_arg1, text_arg2))
end
-- The width/alert dress (ref l.1576-1589), an if/elseif in the ref's own order: every
-- Show resets width 320 / icon hidden; a GUILD MESSAGE dialog widens to 420 with NO
-- icon; otherwise a showAlert entry widens to 420 and shows the icon. HELP_TICKET's
-- 350 arm waits for that dialog.
--
-- **The widen is keyed on `which`, not on `hasWideEditBox`** — that is the reference's
-- own shape, transcribed rather than improved: a fourth wide-box dialog would get the
-- 350x64 box inside an un-widened 320 frame until its name joins this list. (The ref's
-- list also names SET_GUILDPLAYERNOTE twice, l.1582; the duplicate is inert.)
local alertIcon = getglobal(dialog:GetName() .. "AlertIcon")
alertIcon:Hide()
dialog:SetWidth(320)
if (which == "SET_GUILDMOTD") or (which == "SET_GUILDPLAYERNOTE")
or (which == "SET_GUILDOFFICERNOTE") then
dialog:SetWidth(420)
elseif info.showAlert then
dialog:SetWidth(420)
alertIcon:Show()
end
-- The edit box (ref l.1599-1624): shown only for a hasEditBox entry, capped at the
-- entry's maxLetters, and always empty on open — a name left over from the last time
-- the dialog ran would be sent by a reflexive Enter. `hasWideEditBox` picks the 350x64
-- centred box INSTEAD of the narrow one; exactly one of the two is ever shown, and
-- neither is when the entry has no box at all (the ref's three-way, verbatim).
local editBox = getglobal(dialog:GetName() .. "EditBox")
local wideEditBox = getglobal(dialog:GetName() .. "WideEditBox")
if info.hasEditBox then
if info.hasWideEditBox then
wideEditBox:SetMaxLetters(info.maxLetters or 0)
wideEditBox:SetText("")
wideEditBox:Show()
editBox:Hide()
else
editBox:SetMaxLetters(info.maxLetters or 0)
editBox:SetText("")
editBox:Show()
wideEditBox:Hide()
end
else
editBox:Hide()
wideEditBox:Hide()
end
-- The money frame (ref l.1627-1632): shown for a hasMoneyFrame entry, hidden
-- otherwise. The engine never fills it — the AMOUNT belongs to whoever raised the
-- dialog, who calls MoneyFrame_Update on it (the ref's own division: its
-- CONFIRM_TALENT_WIPE arm in UIParent.lua does exactly that with the event's arg1).
getglobal(dialog:GetName() .. "MoneyFrame"):Hide()
if info.hasMoneyFrame then
getglobal(dialog:GetName() .. "MoneyFrame"):Show()
end
-- The buttons (ref l.1634-1676; fixed 120 widths — header note). Button2 shows only
-- when the entry names it AND DisplayButton2 (when present) allows it; a solo button1
-- re-seats centered under the text. With an edit box the pair hangs off the BOX's
-- bottom instead of the text's (ref l.1641-1643), which is what leaves room for it;
-- with a money frame the pair stays on the TEXT and drops 24 instead of 8, which is
-- what leaves room for the coins (ref l.1644-1646).
local buttonAnchor = info.hasEditBox and (dialog:GetName() .. "EditBox")
or (dialog:GetName() .. "Text")
local buttonDrop = info.hasMoneyFrame and -24 or -8
local button1 = getglobal(dialog:GetName() .. "Button1")
local button2 = getglobal(dialog:GetName() .. "Button2")
if info.button2 and (not info.DisplayButton2 or info.DisplayButton2()) then
button1:ClearAllPoints()
button2:ClearAllPoints()
button1:SetPoint("TOPRIGHT", buttonAnchor, "BOTTOM", -6, buttonDrop)
button2:SetPoint("LEFT", dialog:GetName() .. "Button1", "RIGHT", 13, 0)
button2:SetText(info.button2)
button2:Show()
else
button1:ClearAllPoints()
button1:SetPoint("TOP", buttonAnchor, "BOTTOM", 0, -8)
button2:Hide()
end
if info.button1 then
button1:SetText(info.button1)
button1:Show()
else
button1:Hide()
end
-- Misc vars (ref l.1677-1690). `which` is set before Show so OnShow reads it.
dialog.which = which
dialog.timeleft = info.timeout or 0
dialog.hideOnEscape = info.hideOnEscape
dialog.data = data
if info.StartDelay then
dialog.startDelay = info.StartDelay()
button1:Disable()
else
dialog.startDelay = nil
button1:Enable()
end
dialog:Show()
StaticPopup_Resize(dialog, which)
if info.sound then
PlaySound(info.sound)
end
return dialog
end
-- Ref l.1850-1868: a truthy OnAccept return keeps the dialog open.
function StaticPopup_OnClick(dialog, index)
local which = dialog.which
local info = StaticPopupDialogs[which]
if not info then
return
end
local dontHide = nil
if index == 1 then
if info.OnAccept then
dontHide = info.OnAccept(dialog.data, dialog.data2)
end
else
if info.OnCancel then
info.OnCancel(dialog.data, "clicked")
end
end
if not dontHide and (which == dialog.which) then
dialog:Hide()
end
end
-- The edit box's two keys (ref StaticPopup.xml's template scripts). Enter runs the
-- entry's EditBoxOnEnterPressed hook, which by convention does the accept work and hides
-- the dialog itself; Escape dismisses the dialog through the ordinary cancel path so a
-- half-typed name never acts.
function StaticPopup_EditBoxOnEnterPressed(editBox)
local dialog = editBox:GetParent()
local info = StaticPopupDialogs[dialog.which]
if info and info.EditBoxOnEnterPressed then
info.EditBoxOnEnterPressed(dialog.data)
end
end
function StaticPopup_EditBoxOnEscapePressed(editBox)
local dialog = editBox:GetParent()
editBox:ClearFocus()
dialog:Hide()
end
-- Ref l.1832-1848: the open/close pair sounds on EVERY path, then the entry's own hook.
function StaticPopup_OnShow()
PlaySound("igMainMenuOpen")
local info = StaticPopupDialogs[this.which]
if info and info.OnShow then
info.OnShow(this.data)
end
end
function StaticPopup_OnHide()
PlaySound("igMainMenuClose")
local info = StaticPopupDialogs[this.which]
if info and info.OnHide then
info.OnHide(this.data)
end
end
-- Ref l.1712-1808: the timeout countdown (expiry → OnCancel "timeout" + hide), the
-- per-tick countdown text for the timer dialogs, the StartDelay gate (button1 disabled,
-- delayText counts down, then the real text swaps in and the button enables), then the
-- entry's own OnUpdate. The INSTANCE_BOOT/CONFIRM_SUMMON special text formats join with
-- their dialogs; their which-list slots stay so the gate is the ref's.
function StaticPopup_OnUpdate(dialog, elapsed)
local info = StaticPopupDialogs[dialog.which]
if not info then
return
end
if (dialog.timeleft or 0) > 0 then
local which = dialog.which
local timeleft = dialog.timeleft - elapsed
if timeleft <= 0 then
dialog.timeleft = 0
if info.OnCancel then
info.OnCancel(dialog.data, "timeout")
end
dialog:Hide()
return
end
dialog.timeleft = timeleft
if (which == "DEATH") or (which == "CAMP") or (which == "QUIT")
or (which == "DUEL_OUTOFBOUNDS") or (which == "INSTANCE_BOOT")
or (which == "CONFIRM_SUMMON") or (which == "AREA_SPIRIT_HEAL") then
local text = getglobal(dialog:GetName() .. "Text")
timeleft = ceil(timeleft)
if timeleft < 60 then
text:SetText(format(info.text, timeleft, StaticPopupTimeUnit(timeleft, false)))
else
local m = ceil(timeleft / 60)
text:SetText(format(info.text, m, StaticPopupTimeUnit(m, true)))
end
end
end
if dialog.startDelay then
local which = dialog.which
local timeleft = dialog.startDelay - elapsed
if timeleft <= 0 then
dialog.startDelay = nil
local text = getglobal(dialog:GetName() .. "Text")
text:SetText(format(info.text, dialog.text_arg1, dialog.text_arg2))
getglobal(dialog:GetName() .. "Button1"):Enable()
return
end
dialog.startDelay = timeleft
if (which == "RECOVER_CORPSE") or (which == "RESURRECT")
or (which == "RESURRECT_NO_SICKNESS") then
local text = getglobal(dialog:GetName() .. "Text")
timeleft = ceil(timeleft)
if (which == "RESURRECT") or (which == "RESURRECT_NO_SICKNESS") then
if timeleft < 60 then
text:SetText(format(info.delayText, dialog.text_arg1, timeleft,
StaticPopupTimeUnit(timeleft, false)))
else
local m = ceil(timeleft / 60)
text:SetText(format(info.delayText, dialog.text_arg1, m,
StaticPopupTimeUnit(m, true)))
end
else
if timeleft < 60 then
text:SetText(format(info.delayText, timeleft,
StaticPopupTimeUnit(timeleft, false)))
else
local m = ceil(timeleft / 60)
text:SetText(format(info.delayText, m,
StaticPopupTimeUnit(m, true)))
end
end
end
end
StaticPopup_Resize(dialog, dialog.which)
if info.OnUpdate then
info.OnUpdate(elapsed, dialog)
end
end
-- Ref l.1879-1893: only hideOnEscape dialogs respond — a shown DEATH popup deliberately
-- does NOT close on ESC. Returns truthy when something closed (the caller's chain stops).
function StaticPopup_EscapePressed()
local closed = nil
for index = 1, STATICPOPUP_NUMDIALOGS do
local frame = getglobal("StaticPopup" .. index)
if frame:IsShown() and frame.hideOnEscape then
local OnCancel = StaticPopupDialogs[frame.which].OnCancel
if OnCancel then
OnCancel(frame.data, "clicked")
end
frame:Hide()
closed = 1
end
end
return closed
end
-- Delete-item confirm strings — the real 1.12 GlobalStrings.lua (patch-2.MPQ,
-- Interface\FrameXML\GlobalStrings.lua), quoted verbatim (extracted via benilla-extract).
local DELETE_ITEM_TEXT = "Do you want to destroy %s?" -- DELETE_ITEM; %s = the item name
local YES_TEXT = "Yes" -- YES
local NO_TEXT = "No" -- No
-- Ref StaticPopup.lua l.659-679, verbatim — its own OnUpdate poll (cursor emptied by any
-- other path → hide) replaces the old bespoke `deleteItemFlow` CURSOR_UPDATE driver.
-- exclusive is v1-inert (engine header note); showAlert is live since 1068 — the ref's
-- own "display the error image" case is this very dialog.
StaticPopupDialogs["DELETE_ITEM"] = {
text = DELETE_ITEM_TEXT,
button1 = YES_TEXT,
button2 = NO_TEXT,
OnAccept = function()
DeleteCursorItem()
end,
OnCancel = function()
ClearCursor()
end,
OnUpdate = function()
if not CursorHasItem() then
StaticPopup_Hide("DELETE_ITEM")
end
end,
timeout = 0,
whileDead = 1,
exclusive = 1,
showAlert = 1,
hideOnEscape = 1,
}
-- BenillaDeleteItemConfirmDriver's OnEvent — DELETE_ITEM_CONFIRM(name, quality) is the
-- engine's world-drop of an item payload (decision 0216 §3, cursor.rs::world_drop) →
-- ref UIParent.lua:344-352. DEFERRED (0216 §3): the ref splits quality >= 3 to
-- DELETE_GOOD_ITEM (the typed-confirmation editbox variant) — no editbox variant yet, so
-- every quality (arg2) shows DELETE_ITEM for now.
function BenillaDeleteItemConfirmDriver_OnEvent(frame)
if event == "DELETE_ITEM_CONFIRM" then
StaticPopup_Show("DELETE_ITEM", arg1)
end
end
-- ── The shared frame-fade kit — ref UIParent.lua:1103-1219 is the specification (the
-- function names are its published API and the arithmetic is the same lerp); the code is
-- ours. FADEFRAMES is a plain ARRAY walked by index rather than by `pairs`, because
-- UIFrameFadeUpdate removes from it mid-walk. The ref drives UIFrameFadeUpdate from
-- UIParent's OnUpdate; our driver is the BenillaFadeDriver frame below. Callers: the quest
-- detail write-on (QuestFrame.xml) fades TextAlphaDependentFrame in over
-- QUESTINFO_FADE_IN. ─────────────────────────────────────────────────────────────────────
FADEFRAMES = {}
-- Where `frame` sits in the fade list, searching from `from` onward, or nil. Three of the
-- verbs below ask exactly this question, and the reference spells the index walk out
-- longhand inside each of them.
local function FadeIndexOf(frame, from)
local index = from or 1
while FADEFRAMES[index] do
if FADEFRAMES[index] == frame then
return index
end
index = index + 1
end
return nil
end
function UIFrameFadeRemoveFrame(frame)
local index = FadeIndexOf(frame)
while index do
table.remove(FADEFRAMES, index)
-- Resume from the hole rather than from the start: a frame listed twice goes twice.
index = FadeIndexOf(frame, index)
end
end
-- Where each mode starts and ends when the caller doesn't say. The tests below are
-- `not fadeInfo.startAlpha`, so an explicit 0 — falsey in most languages, TRUTHY in Lua —
-- is the caller's and stands.
local FADE_ENDPOINTS = {
IN = { startAlpha = 0, endAlpha = 1.0 },
OUT = { startAlpha = 1.0, endAlpha = 0 },
}
function UIFrameFade(frame, fadeInfo)
if not fadeInfo.mode then
fadeInfo.mode = "IN"
end
local endpoints = FADE_ENDPOINTS[fadeInfo.mode]
if endpoints then
if not fadeInfo.startAlpha then fadeInfo.startAlpha = endpoints.startAlpha end
if not fadeInfo.endAlpha then fadeInfo.endAlpha = endpoints.endAlpha end
end
frame:SetAlpha(fadeInfo.startAlpha)
frame.fadeInfo = fadeInfo
-- Already in the list? The new info is installed above and takes over on the next
-- tick; the frame keeps its place instead of being listed twice (the ref's early
-- return).
if FadeIndexOf(frame) then
return
end
frame:Show()
table.insert(FADEFRAMES, frame)
-- Wake the driver: it hides itself the moment the list empties (its OnUpdate gate),
-- so the ONE insertion point is where it is shown again. Every other verb only
-- removes or rewrites entries.
BenillaFadeDriver:Show()
end
-- The two directions are one call with the mode flipped; whatever endpoints the caller
-- leaves out, UIFrameFade fills in above.
local function FadeToMode(mode, frame, timeToFade, startAlpha, endAlpha)
UIFrameFade(frame, {
mode = mode,
timeToFade = timeToFade,
startAlpha = startAlpha,
endAlpha = endAlpha,
})
end
function UIFrameFadeIn(frame, timeToFade, startAlpha, endAlpha)
FadeToMode("IN", frame, timeToFade, startAlpha, endAlpha)
end
function UIFrameFadeOut(frame, timeToFade, startAlpha, endAlpha)
FadeToMode("OUT", frame, timeToFade, startAlpha, endAlpha)
end
function UIFrameFadeUpdate(elapsed)
local index = 1
while FADEFRAMES[index] do
local frame = FADEFRAMES[index]
local fadeInfo = frame.fadeInfo
if not fadeInfo.fadeTimer then fadeInfo.fadeTimer = 0 end
fadeInfo.fadeTimer = fadeInfo.fadeTimer + elapsed
if fadeInfo.fadeTimer < fadeInfo.timeToFade then
if fadeInfo.mode == "IN" then
frame:SetAlpha((fadeInfo.fadeTimer / fadeInfo.timeToFade)
* (fadeInfo.endAlpha - fadeInfo.startAlpha) + fadeInfo.startAlpha)
elseif fadeInfo.mode == "OUT" then
frame:SetAlpha(((fadeInfo.timeToFade - fadeInfo.fadeTimer) / fadeInfo.timeToFade)
* (fadeInfo.startAlpha - fadeInfo.endAlpha) + fadeInfo.endAlpha)
end
else
frame:SetAlpha(fadeInfo.endAlpha)
if fadeInfo.fadeHoldTime and fadeInfo.fadeHoldTime > 0 then
fadeInfo.fadeHoldTime = fadeInfo.fadeHoldTime - elapsed
else
UIFrameFadeRemoveFrame(frame)
if fadeInfo.finishedFunc then
fadeInfo.finishedFunc(fadeInfo.finishedArg1, fadeInfo.finishedArg2,
fadeInfo.finishedArg3, fadeInfo.finishedArg4)
fadeInfo.finishedFunc = nil
end
end
end
index = index + 1
end
end
function UIFrameIsFading(frame)
if FadeIndexOf(frame) then
return 1
end
return nil
end
-- ── The tab kit (ref UIPanelTemplates.lua l.1-29/90-133, transcribed VERBATIM shape) ──
-- One selected tab per group: the frame carries numTabs/selectedTab, tabs are named
-- <frame>Tab1..N (CharacterFrameTabButtonTemplate instances — the template lives in this
-- file). SelectTab swaps the InActive art for the Active art and disables the button (a
-- selected tab doesn't re-click); DeselectTab reverses it. Omitted vs ref: the
-- isDisabled/SetDisabledTabState arm (no window has a disabled tab yet) and the SelectTab
-- tab-tooltip hide (our tabs set no tooltip). First consumer: the merchant's
-- Merchant/Buyback pair.
-- The kit's XML-facing entry point (ref l.2-4), and the one member of it this client never
-- had. Not an omission anyone decided: our own windows wire each tab's OnClick to their own
-- handler (the merchant's Merchant/Buyback pair, CharacterFrame's pages), so nothing here
-- ever reached for the generic one. An ADDON does the opposite — it puts the reference's
-- name straight in its <OnClick> and lets the kit do the work:
--
-- Enchantrix EnchantrixBarker.xml:61 (Enchantrix_BarkerOptions_Frame)
-- Outfitter Outfitter.xml:7 (OutfitterFrame)
-- SimpleActionSets SimpleActionSets.xml:342 (this:GetParent())
-- TheoryCraft TheoryCraft.xml:163 (TheoryCraft)
--
-- Four addons, four separate files, so 1207's one-library caveat does not apply. A raw grep
-- says FIVE: QuestItemTemplates.xml:17 has the call commented out. Reading the line is what
-- tells those apart, and the survey's own count of 4 was the right one.
--
-- Missing, every one of them raises on each click of the tab, so the addon's options window
-- stays on whatever page it opened with and cannot be navigated at all. `this` is the
-- clicked tab button, whose id is its slot in the row; the frame is passed in separately
-- because the tab's parent is not always the frame owning the row — SimpleActionSets is the
-- case that proves it, passing `this:GetParent()` explicitly.
function PanelTemplates_Tab_OnClick(frame)
PanelTemplates_SetTab(frame, this:GetID())
end
function PanelTemplates_SetNumTabs(frame, numTabs)
frame.numTabs = numTabs
end
function PanelTemplates_SetTab(frame, id)
frame.selectedTab = id
PanelTemplates_UpdateTabs(frame)
end
function PanelTemplates_GetSelectedTab(frame)
return frame.selectedTab
end
function PanelTemplates_UpdateTabs(frame)
if not frame.selectedTab then return end
for i = 1, frame.numTabs do
local tab = getglobal(frame:GetName() .. "Tab" .. i)
if tab.isDisabled then
PanelTemplates_SetDisabledTabState(tab)
elseif i == frame.selectedTab then
PanelTemplates_SelectTab(tab)
else
PanelTemplates_DeselectTab(tab)
end
end
end
-- The disabled-tab arm (ref UIPanelTemplates.lua l.94-105 + l.135-148) — the piece this
-- kit deliberately omitted until a window needed it. First customer: the social window's
-- GUILD tab, which `InGuildCheck` disables for a character in no guild — i.e. permanently
-- until the guild arc exists (decision 0668). A disabled tab wears the INACTIVE
-- three-slice art (like an unselected tab) but is un-clickable with a greyed label, so it
-- reads as "there, not for you" rather than as the selected one.
function PanelTemplates_DisableTab(frame, index)
getglobal(frame:GetName() .. "Tab" .. index).isDisabled = 1
PanelTemplates_UpdateTabs(frame)
end
function PanelTemplates_EnableTab(frame, index)
local tab = getglobal(frame:GetName() .. "Tab" .. index)
tab.isDisabled = nil
tab:SetDisabledTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
PanelTemplates_UpdateTabs(frame)
end
function PanelTemplates_SetDisabledTabState(tab)
local name = tab:GetName()
getglobal(name .. "Left"):Show()
getglobal(name .. "Middle"):Show()
getglobal(name .. "Right"):Show()
tab:Disable()
tab.text = tab:GetText()
tab:SetDisabledTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
getglobal(name .. "LeftDisabled"):Hide()
getglobal(name .. "MiddleDisabled"):Hide()
getglobal(name .. "RightDisabled"):Hide()
end
function PanelTemplates_DeselectTab(tab)
local name = tab:GetName()
getglobal(name .. "Left"):Show()
getglobal(name .. "Middle"):Show()
getglobal(name .. "Right"):Show()
tab:Enable()
getglobal(name .. "LeftDisabled"):Hide()
getglobal(name .. "MiddleDisabled"):Hide()
getglobal(name .. "RightDisabled"):Hide()
end
function PanelTemplates_SelectTab(tab)
local name = tab:GetName()
getglobal(name .. "Left"):Hide()
getglobal(name .. "Middle"):Hide()
getglobal(name .. "Right"):Hide()
tab:Disable()
getglobal(name .. "LeftDisabled"):Show()
getglobal(name .. "MiddleDisabled"):Show()
getglobal(name .. "RightDisabled"):Show()
end
-- The text-fit (ref PanelTemplates_TabResize, UIPanelTemplates.lua l.31-88 — now the WHOLE
-- function, `(padding, tab, absoluteSize, maxWidth)`): the tab is text + padding wide, plus
-- the two end slices; both middle slices stretch to the text; the text box itself un-sizes
-- to natural. `maxWidth` CAPS it and pins the label's box to the cap instead — the arm that
-- keeps a long tab inside its window, which the first transcription dropped and the macro
-- window's character tab then overran (0983 → 0991). `absoluteSize` is the ref's third arm,
-- transcribed with it so the next caller has it.
--
-- One deviation: the ref reads the label's laid-out GetWidth (its C measure is synchronous);
-- ours reads GetStringWidth — the same number once the measure round-trip lands, which is
-- why the caller below re-checks from OnUpdate instead of OnShow.
function PanelTemplates_TabResize(padding, tab, absoluteSize, maxWidth)
local name = tab:GetName()
local text = getglobal(name .. "Text")
local sideWidths = 2 * getglobal(name .. "Left"):GetWidth()
local width, tabWidth
if absoluteSize then
if absoluteSize < sideWidths then
width, tabWidth = 1, sideWidths
else
width, tabWidth = absoluteSize - sideWidths, absoluteSize
end
text:SetWidth(width)
else
width = text:GetStringWidth() + (padding or 24)
if maxWidth and width > maxWidth then
width = maxWidth + (padding or 24)
text:SetWidth(width)
else
text:SetWidth(0)
end
tabWidth = width + sideWidths
end
getglobal(name .. "Middle"):SetWidth(width)
getglobal(name .. "MiddleDisabled"):SetWidth(width)
tab:SetWidth(tabWidth)
-- benilla addition: hand back the width just declared. `tab:GetWidth()` cannot serve
-- it — that reads the last RESOLVED rect, which inside a settle is this frame's stale
-- input, not the value one line above just set. A clamp that read it back saw the old
-- width, declined to fire, and the settle's gate then locked the un-clamped result in
-- for good (decision 1002).
return tabWidth
end
-- The tab template's per-frame settle: the ref does TabResize(0) once, in OnShow
-- (CharacterFrameTemplates.xml l.77-80), trusting a synchronous text measure. Our
-- GetStringWidth has a frame of latency, so the template's OnUpdate calls this until the
-- measured width settles (then it's a no-op compare per frame). The ref sizes the hover
-- highlight alongside it; this kit does not — both tab templates anchor their highlight to
-- their own side edges, so it is the tab by construction and no settle can desync it
-- (decision 1004).
--
-- The fit PARAMETERS live on the tab (`benillaTabPadding`/`benillaTabMaxWidth`, defaulting
-- to the +0 fit every window tab has always used) rather than at the call site, because
-- there is exactly one call site and it is this settle loop: an instance that ran its own
-- TabResize in OnLoad — as the reference's macro tabs do — would just be overwritten on the
-- next frame. Setting the two fields IS how an instance asks for the reference's own fit.
-- THE STRUCTURAL CLAMP — a tab may never grow past its window's drawn right edge, whatever
-- the instance asked for (decision 1002; the director's "fix this properly to prevent it
-- ever again"). The reference's own caps are numbers hand-tuned to ITS font: the macro
-- window's 150 is right for the enUS FRIZQT__ the 1.12 client rasterizes and for nothing
-- else in particular, and the label it guards carries a CHARACTER NAME — the one string
-- whose width no window author can know. So the instance's cap is a preference and the
-- window's edge is the law.
--
-- `benillaTabRightInset` is how a window declares how far its DRAWN plate stops short of
-- its frame rect, because no kit can introspect an art margin. The macro window's is 40:
-- its 384-unit frame is painted by a 256-wide slice plus a 128-wide one that carries a
-- transparent tail. Clamping to the frame rect instead — the first attempt — clamped to a
-- boundary the player cannot see, and the tab still walked out over the world.
--
-- The clamp is applied to the RESULT, through TabResize's own `absoluteSize` arm, rather
-- than folded into `maxWidth`: that parameter is compared against `text + padding` but
-- assigned as `maxWidth + padding`, so a room budget expressed through it is off by the
-- padding and silently fails to bind. `absoluteSize` sets the tab width outright, which is
-- exactly what a room budget is.
function BenillaTab_Room(tab)
local parent = tab:GetParent()
if not parent then return nil end
local left, right = tab:GetLeft(), parent:GetRight()
if not left or not right then return nil end
local room = (right - (tab.benillaTabRightInset or 0)) - left
if room < 1 then room = 1 end
return room
end
function BenillaTabButton_OnUpdate(tab)
local text = getglobal(tab:GetName() .. "Text")
local w = (text and text:GetStringWidth()) or 0
if w <= 0 then return end
local pad = tab.benillaTabPadding or 0
local room = BenillaTab_Room(tab)
if tab.benillaFitWidth == w and tab.benillaFitRoom == room then return end
tab.benillaFitWidth, tab.benillaFitRoom = w, room
local fitted = PanelTemplates_TabResize(pad, tab, nil, tab.benillaTabMaxWidth)
if room and fitted > room then
PanelTemplates_TabResize(pad, tab, room)
end
-- No highlight sizing here on purpose — BOTH tab templates anchor their highlight to
-- their own two side edges, so it IS the tab by construction (decision 1004).
end
]]></Script>
<!-- The fade kit's tick source — the ref hangs UIFrameFadeUpdate on UIParent's OnUpdate
(UIParent.xml l.16-22); benilla has no UIParent frame, so a dedicated 1x1 driver ticks it.
It runs only while FADEFRAMES has entries: the engine dispatches OnUpdate to visible
frames only (tick.rs), so hiding itself on an empty list stops the per-frame walk cold,
and UIFrameFade — the single inserter — shows it again (decision 1396's class). -->
<Frame name="BenillaFadeDriver">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts><OnUpdate>if not FADEFRAMES[1] then this:Hide() return end UIFrameFadeUpdate(arg1)</OnUpdate></Scripts>
</Frame>
<!--
The window tab button (ref CharacterFrameTabButtonTemplate — Interface\FrameXML\
CharacterFrameTemplates.xml in patch.MPQ, NOT the base FrameXML.toc set; extracted via
mpqx, transcribed VERBATIM: 115x32, the Active/InActive three-slice pair with the ref's
exact TexCoords, text CENTER +2, the ADD highlight inset 10px). "Disabled" IS "selected"
here — PanelTemplates_SelectTab (script kit above) disables the button and shows the
*Disabled (= Active) slices. The ref's OnShow text-fit (PanelTemplates_TabResize(0)) runs
from OnUpdate here instead — BenillaTabButton_OnUpdate settles once the async text measure
lands, then no-ops (the ref's C measure is synchronous; the 115px XML width is just the
pre-fit state). Its OnShow *also* sets the highlight width, which is dead code in the
reference and dropped here: this highlight is anchored LEFT/RIGHT, and two opposing anchors
resolve the size from the span without consulting the width at all (wow-re layout.md,
VERIFIED). The small tab now says the same thing the same way (1004). One deviation: the ref's OnLoad
SetFrameLevel(+4) raise (our loader has no cross-frame level raise wired into templates
yet; the tabs sit below the window art's hit-rect so nothing overlaps them). OnClick is
the instance's to define (the ref merchant overrides it too).
-->
<Button name="CharacterFrameTabButtonTemplate" virtual="true">
<Size><AbsDimension x="115" y="32"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeftDisabled" file="Interface\PaperDollInfoFrame\UI-Character-ActiveTab">
<Size><AbsDimension x="20" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="5"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.15625" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddleDisabled" file="Interface\PaperDollInfoFrame\UI-Character-ActiveTab">
<Size><AbsDimension x="88" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeftDisabled" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.15625" right="0.84375" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentRightDisabled" file="Interface\PaperDollInfoFrame\UI-Character-ActiveTab">
<Size><AbsDimension x="20" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddleDisabled" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.84375" right="1.0" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentLeft" file="Interface\PaperDollInfoFrame\UI-Character-InActiveTab">
<Size><AbsDimension x="20" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0" right="0.15625" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\PaperDollInfoFrame\UI-Character-InActiveTab">
<Size><AbsDimension x="88" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.15625" right="0.84375" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentRight" file="Interface\PaperDollInfoFrame\UI-Character-InActiveTab">
<Size><AbsDimension x="20" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddle" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.84375" right="1.0" top="0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<ButtonText name="$parentText">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="2"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormalSmall"/>
<HighlightFont inherits="GameFontHighlightSmall"/>
<DisabledFont inherits="GameFontHighlightSmall"/>
<HighlightTexture name="$parentHighlightTexture" file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="10" y="2"/></Offset></Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="2"/></Offset></Anchor>
</Anchors>
</HighlightTexture>
<Scripts>
<OnUpdate>BenillaTabButton_OnUpdate(self)</OnUpdate>
</Scripts>
</Button>
<!--
The SMALL tab button (ref `TabButtonTemplate`, UIPanelTemplates.xml l.303-402, transcribed
VERBATIM: the HelpFrameTab Active/Inactive three-slice pair on 16/32/16 slices, text at
BOTTOM +5, the ADD highlight at +2,-8 — anchored to both side edges rather than sized in
Lua, the one departure, and why: see the highlight below). Not the same thing as
`CharacterFrameTabButtonTemplate` above — that one is the big *window* tab along a panel's
bottom edge; this is the compact tab a panel uses to switch a list INSIDE itself. The
social window's Friends/Ignore pair is the first customer (decision 0668); the reference
also uses it for the help frame and the raid pane.
Slice naming matches the big tab's, so the shared PanelTemplates_SelectTab/DeselectTab kit
drives both unchanged — "Disabled" is "selected" here too. Same OnUpdate text-fit for the
same reason (our GetStringWidth lands a frame late).
-->
<Button name="TabButtonTemplate" virtual="true">
<Size><AbsDimension x="115" y="32"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeftDisabled" file="Interface\HelpFrame\HelpFrameTab-Active">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddleDisabled" file="Interface\HelpFrame\HelpFrameTab-Active">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeftDisabled" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.75" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentRightDisabled" file="Interface\HelpFrame\HelpFrameTab-Active">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddleDisabled" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.75" right="1.0" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentLeft" file="Interface\HelpFrame\HelpFrameTab-Inactive">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\HelpFrame\HelpFrameTab-Inactive">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.75" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentRight" file="Interface\HelpFrame\HelpFrameTab-Inactive">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddle" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.75" right="1.0" top="0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<ButtonText name="$parentText">
<Size><AbsDimension x="0" y="13"/></Size>
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="5"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormalSmall"/>
<HighlightFont inherits="GameFontHighlightSmall"/>
<DisabledFont inherits="GameFontHighlightSmall"/>
<!-- The highlight spans the tab's own two side edges, so it tracks any fit for free. The
ref sizes it in Lua instead — `SetWidth(GetTextWidth() + 31)` at OnLoad, which on the
template's own +0 fit (tab = text + 32) lands at tab − 1: "the highlight is the tab",
said in arithmetic. It only stops meaning that when an instance passes a padding or a
cap, and the macro window's tabs pass both — which is how the director got a highlight
13 units proud of tab 1 and 35 short of tab 2 in the same screenshot (decision 1004).
The big tab template above already says it this way (LEFT/RIGHT, no Lua); this is the
same statement on 16-unit end slices. Two opposing anchors resolve the size from the
span and never consult the width, so the declared 5 is inert on x (wow-re layout.md,
VERIFIED); the y anchors are both BOTTOM, so 32 still sets the height. -->
<HighlightTexture name="$parentHighlightTexture" file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD">
<Size><AbsDimension x="5" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="2" y="-8"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="2" y="-8"/></Offset></Anchor>
</Anchors>
</HighlightTexture>
<Scripts>
<OnUpdate>BenillaTabButton_OnUpdate(self)</OnUpdate>
</Scripts>
</Button>
<!--
The shared dialog instance — the ref's StaticPopup1 (decision 0308 §3; the engine lives in
the script block above). Geometry/art transcribed VERBATIM from the real StaticPopup.xml
(mpqx extraction — template l.30-120: 320x72, the UI-DialogBox backdrop, text
GameFontHighlight 290 wide at TOP -16, 128x20 UI-DialogBox-Button buttons with the
gold/white/gray state-font trio; instance l.231-239: TOP of the screen at (0,-128)) and
StaticPopup.lua's no-editbox button seat (l.1645-1649: button1 TOPRIGHT of text's BOTTOM
at (-6,-8), button2 LEFT of button1 at (13,0)) — the XML anchors are the two-button seat;
StaticPopup_Show re-anchors per entry (a solo button1 re-seats centered).
-->
<Button name="StaticPopupButtonTemplate" virtual="true">
<Size><AbsDimension x="128" y="20"/></Size>
<NormalTexture file="Interface\Buttons\UI-DialogBox-Button-Up"><TexCoords left="0.0" right="1.0" top="0.0" bottom="0.625"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-DialogBox-Button-Down"><TexCoords left="0.0" right="1.0" top="0.0" bottom="0.625"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-DialogBox-Button-Disabled"><TexCoords left="0.0" right="1.0" top="0.0" bottom="0.625"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-DialogBox-Button-Highlight" alphaMode="ADD"><TexCoords left="0.0" right="1.0" top="0.0" bottom="0.625"/></HighlightTexture>
<NormalFont inherits="GameFontNormal"/>
<DisabledFont inherits="GameFontDisable"/>
<HighlightFont inherits="GameFontHighlight"/>
</Button>
<!-- The dialog template + 2 instances (the ref runs 4; two is what the flows built so far
need — the deepest live nesting is decline-a-res re-showing DEATH, where Show runs before
the click's hide so a second instance must be free; the ref's own StaticPopup2 case).
Extend toward 4 the day a third coexists. StaticPopup2 seats below 1 (the ref's
StaticPopup_SetUpPosition stack, simplified to a static anchor). -->
<!-- `toplevel` is the ref's (StaticPopup.xml l.3): a confirm popup must come to the front over
whatever DIALOG-strata window raised it (decision 1318). -->
<Frame name="StaticPopupTemplate" virtual="true" toplevel="true" enableMouse="true" frameStrata="DIALOG" hidden="true">
<Size><AbsDimension x="320" y="72"/></Size>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" inherits="GameFontHighlight">
<Size><AbsDimension x="290" y="0"/></Size>
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor></Anchors>
</FontString>
<!-- The showAlert icon (ref template l.58-67 verbatim: 64x64 DialogAlertIcon at
LEFT +12). Show resets it hidden and shows it only for a showAlert entry —
the ref keeps the text 290 wide and centered even at the 420 width, so the
icon owning the left margin IS the reference look, not an overlap bug. -->
<Texture name="$parentAlertIcon" file="Interface\DialogFrame\DialogAlertIcon">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="12" y="0"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="$parentButton1" inherits="StaticPopupButtonTemplate" text="Yes">
<Anchors><Anchor point="TOPRIGHT" relativeTo="$parentText" relativePoint="BOTTOM"><Offset><AbsDimension x="-6" y="-8"/></Offset></Anchor></Anchors>
<Scripts><OnClick>StaticPopup_OnClick(self:GetParent(), 1)</OnClick></Scripts>
</Button>
<Button name="$parentButton2" inherits="StaticPopupButtonTemplate" text="No">
<Anchors><Anchor point="LEFT" relativeTo="$parentButton1" relativePoint="RIGHT"><Offset><AbsDimension x="13" y="0"/></Offset></Anchor></Anchors>
<Scripts><OnClick>StaticPopup_OnClick(self:GetParent(), 2)</OnClick></Scripts>
</Button>
<!-- The name-entry box (ref StaticPopup.xml l.84-140): 130x32 at BOTTOM +45, the
chat-input three-slice border, hidden unless the entry sets hasEditBox. First
customer: the social window's ADD_FRIEND / ADD_IGNORE dialogs (decision 0668).
Enter runs the entry's own EditBoxOnEnterPressed (the ref's hook name) and Escape
dismisses the whole dialog, which is the ref's EditBoxOnEscapePressed default. -->
<EditBox name="$parentEditBox" historyLines="1" hidden="true" autoFocus="false">
<Size><AbsDimension x="130" y="32"/></Size>
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="45"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\ChatFrame\UI-ChatInputBorder-Left">
<Size><AbsDimension x="75" y="32"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.29296875" top="0" bottom="1.0"/>
</Texture>
<Texture file="Interface\ChatFrame\UI-ChatInputBorder-Right">
<Size><AbsDimension x="75" y="32"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.70703125" right="1.0" top="0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEnterPressed>StaticPopup_EditBoxOnEnterPressed(self)</OnEnterPressed>
<OnEscapePressed>StaticPopup_EditBoxOnEscapePressed(self)</OnEscapePressed>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
<!-- The coin row (ref StaticPopup.xml l.203-217, verbatim seat): a
SmallMoneyFrameTemplate under the text, hidden unless the entry sets
hasMoneyFrame. Type "STATIC" is the money kit's "a number somebody parked on the
frame" source, which is what MoneyFrame_Update(name, amount) leaves behind — so
the dialog shows a cost its raiser worked out, never a live purse. First customer:
the class trainer's CONFIRM_TALENT_WIPE (decision 1580). -->
<Frame name="$parentMoneyFrame" inherits="SmallMoneyFrameTemplate" hidden="true">
<Anchors>
<Anchor point="TOP" relativeTo="$parentText" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>SmallMoneyFrame_OnLoad(); MoneyFrame_SetType("STATIC")</OnLoad>
</Scripts>
</Frame>
<!-- The WIDE entry box (ref StaticPopup.xml l.138-202): 350x64 at CENTER, hidden
unless the entry sets hasWideEditBox. Its border is the same chat-input art
stretched over three slices — a 256 left cap, a 75 right cap, and a 5-wide middle
pinned between the two so it fills whatever is left. First customers: the guild
arc's SET_GUILDMOTD / SET_GUILDPLAYERNOTE / SET_GUILDOFFICERNOTE. It shares the
narrow box's two key handlers, which take the box as an argument for exactly this
reason. -->
<EditBox name="$parentWideEditBox" historyLines="1" hidden="true" autoFocus="false">
<Size><AbsDimension x="350" y="64"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\ChatFrame\UI-ChatInputBorder-Left">
<Size><AbsDimension x="256" y="32"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentRight" file="Interface\ChatFrame\UI-ChatInputBorder-Right">
<Size><AbsDimension x="75" y="32"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="10" y="0"/></Offset></Anchor></Anchors>
<TexCoords left="0.70703125" right="1.0" top="0" bottom="1.0"/>
</Texture>
<Texture file="Interface\ChatFrame\UI-ChatInputBorder-Left">
<Size><AbsDimension x="5" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.29296875" right="1.0" top="0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEnterPressed>StaticPopup_EditBoxOnEnterPressed(self)</OnEnterPressed>
<OnEscapePressed>StaticPopup_EditBoxOnEscapePressed(self)</OnEscapePressed>
</Scripts>
<FontString inherits="ChatFontNormal"/>
</EditBox>
</Frames>
<!-- The engine handlers (ref StaticPopup.xml template Scripts): open/close sounds ride
inside StaticPopup_OnShow/OnHide so every path sounds; OnUpdate drives the countdown/
StartDelay machinery (arg1 = elapsed, RF-0025). -->
<Scripts>
<OnShow>StaticPopup_OnShow()</OnShow>
<OnHide>StaticPopup_OnHide()</OnHide>
<OnUpdate>StaticPopup_OnUpdate(this, arg1)</OnUpdate>
</Scripts>
</Frame>
<Frame name="StaticPopup1" inherits="StaticPopupTemplate">
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-128"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="StaticPopup2" inherits="StaticPopupTemplate">
<Anchors><Anchor point="TOP" relativeTo="StaticPopup1" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
</Frame>
<!-- The delete-item confirm driver (decision 0216 §3): DELETE_ITEM_CONFIRM names no frame of
its own (the ref handles it in UIParent.lua's OnEvent), so this is a dedicated hidden
listener. The old CURSOR_UPDATE watch is gone — the DELETE_ITEM registry entry's own
OnUpdate poll (ref-verbatim) owns the auto-hide now. -->
<Frame name="BenillaDeleteItemConfirmDriver" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
self:RegisterEvent("DELETE_ITEM_CONFIRM")
</OnLoad>
<OnEvent>BenillaDeleteItemConfirmDriver_OnEvent(self)</OnEvent>
</Scripts>
</Frame>
</Ui>