Files
benilla-octo/crates/benilla-app/assets/ui/UiPanels.xml
T
SamandClaude Fable 5 a683d5524f the interface code is ours: the transcription debt is paid
- forty files that carried the reference's literal Lua are rewritten as our own expression of the behaviour — worst identical run 61 lines down to 7 — and the README now says exactly that
- the overlap gate measures the code itself across all four reference corpora, and ratchets: the number may only go down
- a lost session ends at the login screen instead of silently re-authenticating and stealing the account back
- a building's skybox is a real model: Caverns of Time's white ceiling becomes its painted nebula and additive stars

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:21:03 +07:00

1818 lines
103 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: `left`, `center` and `full` are all live. `doublewide` and `UIChildWindows` are not
— nothing benilla ships wants them — but the registration table keeps their exact shape so
a later row drops in unchanged. **`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/anticipates today (pin §1): Gossip + Merchant are live; Loot registers
-- now (pushable=7) so the coming loot window lands on an already-wired system.
UIPanelWindows = {}
UIPanelWindows["GossipFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["MerchantFrame"] = { 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 special-frame toplevel
-- (UIParent.lua l.46 UISpecialFrames), closed by ESC in ToggleGameMenu below. Our own
-- escapable toplevels stay hard-listed in that chain rather than pushed into
-- UISpecialFrames — the table exists now (1206) but it is the ADDONS' list, and mixing our
-- windows into it would make an addon's `tremove` able to break the client's own ESC.
UIPanelWindows["MailFrame"] = { area = "left", pushable = 0 }
-- 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 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 }
UIPanelWindows["QuestLogFrame"] = { area = "left", pushable = 0 }
UIPanelWindows["LootFrame"] = { area = "left", pushable = 7 }
-- The character window (decision 0208 phase 1a, CharacterFrame.xml): the ref's own
-- CharacterFrame row (area "left", pushable 0) — competes for/replaces the other left-slot
-- windows exactly like Gossip/Merchant/Quest/QuestLog above.
UIPanelWindows["CharacterFrame"] = { area = "left", pushable = 0 }
-- 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
-- (area "left", pushable 0) — same left-slot competition as every window above.
UIPanelWindows["SpellBookFrame"] = { area = "left", pushable = 0 }
-- The talent window (decision 0304 §4, TalentFrame.xml): the ref's own TalentFrame row is
-- `{ area = "left", pushable = 6, whileDead = 1 }` — pushable 6 kept (one tier above the
-- plain NPC-session/player-state windows' 0, matching the ref exactly); `whileDead` is
-- OMITTED like every other row here (none of them model it, even though ShowUIPanel's own
-- guard already reads `info.whileDead` — ShowUIPanel's own comment: `UnitIsDead` doesn't
-- exist yet, so the guard is inert either way today).
UIPanelWindows["BenillaTalentFrame"] = { area = "left", pushable = 6 }
-- 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 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 }
-- (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 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 }
-- 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.840-846) is dropped: no such frames exist in benilla yet (out of v1 scope).
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
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. benilla has no UIParent frame (the note on `slots` above), and the only
-- full frame we ship — the world map — already paints its own full-screen blackout
-- (WorldMapFrame.xml), which is what that Hide buys the reference.
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
-- 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. UnitIsDead doesn't exist yet
-- (pin §3) — guarded rather than assumed absent-forever.
if type(UnitIsDead) == "function" and UnitIsDead("player") and not info.whileDead then
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, the left+center+full slice (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). Still skipped by scope: `doublewide` (l.707-710 — no such row is
-- registered), which is why the last arm below is a bare Show.
--
-- 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 == "left" then
SeatLeftAreaPanel(frame, info)
else
-- No `doublewide` row is registered — 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. The doublewide slice stays dropped
-- (we have none); the UISpecialFrames loop at l.947-954 is now carried.
function CloseWindows(ignoreCenter)
local leftFrame = GetLeftFrame()
local centerFrame = GetCenterFrame()
local fullScreenFrame = GetFullScreenFrame()
local found = leftFrame or centerFrame or fullScreenFrame
HideUIPanel(leftFrame)
HideUIPanel(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 — matching the 'B' all-bags toggle),
-- 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)`); this engine gives plain frames no raw key events, so
-- it routes through this chain exactly like the AddOns list on the next rung.
-- **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 AddOns list next (decision 1197). The reference's glue twin hooks ESC directly
-- (`AddonList_OnKeyDown`, whose ESCAPE arm is `AddonList_OnCancel`); this engine gives
-- plain frames no raw key events, so it routes through this shared chain like the
-- stack-split spinner and the world map do. **Cancel, not hide** — ESC on that window
-- means "discard what I just ticked", which is the reference's own arm and the reason
-- the panel stages its edits at all.
local addons = getglobal("AddonListFrame")
if addons and addons:IsVisible() then
AddonList_OnCancel()
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 since frames get no raw key events).
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 cannot, see below). The
-- reference itself has a no-shader fallback, and OURS IS ALWAYS THAT PATH: this engine has
-- no SetDesaturated, so `shaderSupported` is false exactly as on a 1.12 card without the
-- desaturation shader, and the grey comes from the ref's own vertex-colour branch. Written
-- as the wrapper rather than open-coded at each call site so the day a real desaturation
-- lands, one function changes. 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.
-- · Accepted but v1-inert: exclusive, interruptCinematic (cinematics are acked
-- instantly — 0196), notClosableByLogout, preferredIndex, hasWideEditBox (guild
-- MOTD/notes — the guild arc), and the money variant.
-- · 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, the no-editbox/no-money leg only (v1 has neither variant).
function StaticPopup_Resize(dialog, which)
local text = getglobal(dialog:GetName() .. "Text")
local button1 = getglobal(dialog:GetName() .. "Button1")
local editBox = getglobal(dialog:GetName() .. "EditBox")
if StaticPopupDialogs[which] and StaticPopupDialogs[which].hasEditBox then
dialog:SetHeight(16 + text:GetHeight() + 8 + editBox:GetHeight() + 8
+ button1:GetHeight() + 16)
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
-- 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 alert dress (ref l.1579-1589): every Show resets width 320 / icon hidden, then
-- a showAlert entry widens to 420 and shows the icon. The ref's neighbouring arms —
-- the guild-MOTD 420 widen and HELP_TICKET's 350 — wait for their dialogs.
local alertIcon = getglobal(dialog:GetName() .. "AlertIcon")
alertIcon:Hide()
dialog:SetWidth(320)
if 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. The wide variant is guild-only
-- (MOTD/notes), so it waits for the guild arc.
local editBox = getglobal(dialog:GetName() .. "EditBox")
if info.hasEditBox then
editBox:SetMaxLetters(info.maxLetters or 0)
editBox:SetText("")
editBox:Show()
else
editBox:Hide()
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.
local buttonAnchor = info.hasEditBox and (dialog:GetName() .. "EditBox")
or (dialog:GetName() .. "Text")
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, -8)
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)
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. -->
<Frame name="BenillaFadeDriver">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts><OnUpdate>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). -->
<Frame name="StaticPopupTemplate" virtual="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>
</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>