comments: six interface files say what each frame is and what it mirrors

A frame's comment names the stock file or 1.12 default its shape follows;
a Lua comment stays where the code would otherwise be "fixed". Notes the
code contradicts are gone. With XML and Lua comments stripped every file
is identical, and each is well-formed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-23 19:28:45 +07:00
co-authored by Claude Opus 5.5
parent 6a7a6a473d
commit a32300fad9
6 changed files with 284 additions and 804 deletions
@@ -1,65 +1,19 @@
<!--
OUR adapters over the reference's own `Interface\FrameXML\ContainerFrame.xml`/`.lua`.
**This client does not transcribe those files — it EXECUTES them, off the player's own patch
chain** (decision 1751; `ui_script::reference_ui` carries the rule, and `benilla.toc` carries
the position). The bag windows on screen ARE `ContainerFrame1..12`. What is in THIS file is
only benilla's, and only where a reference body reaches for something this client genuinely
does not have.
The rule, restated in one line so it can be applied without re-reading the module: the sourced
file's globals stand unmodified; ours, loading after, win where both define a name; and where a
reference body needs a benilla answer, it is WRAPPED here — never silently stubbed.
TWO THINGS THIS FILE USED TO DO AND NO LONGER DOES, both retired by 1751's swap:
· It declared `ContainerFrameItemButtonTemplate`, transcribed from ContainerFrame.xml, because
the Lua could be sourced and the XML could not. The XML is sourced now, so the reference's
own declaration is the live one and the copy is deleted — the whole point of the migration.
· It defined `updateContainerFrameAnchors()` as a deliberate no-op, because the reference's
body walks twelve `ContainerFrame` windows this client did not build. It builds them, so the
reference's own body is correct here and the stub is deleted. Five corpus addons (Bongos,
Bongos_ActionBar, the CT_BarMod family) hook that global and call through; they now reach the
real thing.
── The adapters, and why neither is a stub ──────────────────────────────────────────────────
`ContainerFrameItemButton_OnEnter` opens with a keyring fork (ref l.615-620):
if this:GetParent():GetID() == KEYRING_CONTAINER then
GameTooltip:SetInventoryItem("player", KeyRingButtonIDToInvSlotID(this:GetID()))
CursorUpdate()
return
end
That fork exists because the REAL client's keyring items are inventory slots, not container
slots, so it has to detour out of the container API to describe one. benilla's keyring is an
ordinary container (id -2, decision 0765): its slots come through `GetContainerItemInfo` and
`GameTooltip:SetBagItem` like every other bag's — which is exactly why the reference's keyring
WINDOW, its verbs and its sounds all take our keyring unchanged, and why this one line is the
only thing in that file the swap did not simply inherit. The detour is not merely unavailable
here (`KeyRingButtonIDToInvSlotID` is not a binding here) — it is *wrong* here, and supplying
the verb would buy an empty tooltip, because our keyring items were never fed into the
inventory-slot space it reads.
So the wrapper below answers the keyring case the way benilla answers every other bag, and
hands EVERY other slot straight to the reference's own body. The path the player is actually
on — hovering an ordinary bag slot — runs the real client's code, unedited.
── Named, not hidden: what still does not work through the sourced files ────────────────────
· `REPAIR_COST` is read by the reference's repair-mode arm. It is a `GlobalStrings.lua` key and
this client sources that file too, so it resolves at runtime — and the arm is unreachable
today regardless, since `GameTooltip:SetBagItem`'s repairCost is an INTERIM 0.
· `NO_EMPTY_KEYRING_SLOTS` is read by the reference's `PutKeyInKeyRing` when the ring is full,
and no such GlobalString exists — 1.12 ships only `NO_EMPTY_KEYRING_SLOTS_ERROR`. That is a
REFERENCE BUG, and this client used to un-quirk it. It does not any more: the reference's own
file runs, so a full keyring prints the reference's own empty error line. Faithful, and
deliberately so (1751: a divergence becomes faithful or becomes a record, never silent).
Our adapters over the stock `Interface\FrameXML\ContainerFrame.xml` and `.lua`, which run off
the player's patch chain: the bag windows are the stock `ContainerFrame1..12`. This file loads
after them, so its definitions win; a stock body that needs a benilla answer is wrapped here.
Deviation: benilla's keyring is a container (id -2) where the reference's is inventory slots,
so a keyring slot's tooltip comes from `GameTooltip:SetBagItem` like any bag's; the stock
keyring fork in `ContainerFrameItemButton_OnEnter` (ContainerFrame.lua l.615-620) reads the
inventory-slot space, which our keyring items are not in.
A full keyring prints the reference's empty error line: its `PutKeyInKeyRing` reads
`NO_EMPTY_KEYRING_SLOTS`, which 1.12 does not ship (it has `NO_EMPTY_KEYRING_SLOTS_ERROR`).
That is the reference's behaviour, kept.
-->
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<Script><![CDATA[
-- The reference's own body, captured before we shadow the name. Nil on a machine with no
-- client data (nothing was sourced) — which must be LOUD at the call, not a quiet no-op:
-- a bag hover that silently does nothing is precisely the failure mode that let Bagnon
-- ship "working" through four green survey columns.
-- The stock body, captured before the name is shadowed. It is nil when no client data was
-- sourced, and the call then says so rather than doing nothing.
local refItemButtonOnEnter = ContainerFrameItemButton_OnEnter
function ContainerFrameItemButton_OnEnter(button)
@@ -68,9 +22,8 @@
end
if not button then return end
-- benilla's keyring is a CONTAINER (0765), so the reference's inventory-slot detour is
-- both unavailable and wrong here; this is the same pair of calls its general path
-- makes for every other bag, which is all a keyring slot ever needed.
-- Our keyring is a container, so its slot takes the stock general path's calls for a
-- bag slot instead of the inventory-slot detour.
if button:GetParent():GetID() == KEYRING_CONTAINER then
if (button:GetRight() or 0) >= (GetScreenWidth() / 2) then
GameTooltip:SetOwner(button, "ANCHOR_LEFT")
@@ -90,50 +43,30 @@
refItemButtonOnEnter(button)
end
-- ══ The three bag verbs benilla answers differently (decisions 0561 + 1751) ═════════
--
-- `CloseAllBags`, `OpenBackpack` and `CloseBackpack` all exist in the sourced
-- `ContainerFrame.lua` too, and benilla's differ for one reason, not three: **0561, the
-- director's call that a window which opens your bags opens EVERY equipped bag**, not just
-- the backpack. The vendor/bank/mail/trade windows call `OpenBackpack` from their OnShow so
-- your bags come up with them and you can sell or deposit from one; the reference's own
-- body touches bag 0 alone. `CloseBackpack` is its inverse and has to match it, and
-- `CloseAllBags` is the same family's unconditional form.
--
-- **The `Benilla*` names, and the publish under them, are load ORDER** (1751). Order
-- decides every collision and the manifest is the order: this file loads after the
-- reference's `ContainerFrame.xml`, so the three publishes at the foot are the ones that
-- stand. The bodies lived in our UIParent.lua counterpart until 1988 retired it — a
-- harness with no bag stack now meets the loader's stand-ins for the three names
-- (`test_ui::UIPARENT_STAND_INS`), not a nil.
--
-- What CHANGED with the swap is only the plumbing: these used to walk five benilla windows
-- by name and stash `wasOpen` on each frame. The windows are the reference's recycled
-- `ContainerFrame1..12` now — frame N is a different bag every time it opens — so the
-- was-open memory is keyed by BAG ID, and every open and close goes through the
-- reference's own `OpenBag`/`CloseBag`/`IsBagOpen` rather than a raw Show/Hide. That buys
-- the reference's own guards for free (OpenBag's CanOpenPanels + NotWhileDeadError,
-- ToggleBag's `size > 0`) and means an addon replacing those verbs is honoured here too.
-- ══ The three bag verbs benilla answers differently ═══════════════════════════════════
-- Deviation: `OpenBackpack` opens every equipped bag, not only the backpack, so the
-- vendor, bank, mail and trade windows that call it from OnShow bring up every bag to sell
-- or deposit from; `CloseBackpack` is its inverse, `CloseAllBags` the unconditional form.
-- The was-open memory is keyed by bag id because `ContainerFrame1..12` are recycled across
-- bags, and every open and close goes through the stock `OpenBag`/`CloseBag`/`IsBagOpen`,
-- so their guards and any addon that replaces them apply.
BENILLA_BAG_WAS_OPEN = {}
-- The UNCONDITIONAL close, called by ShowUIPanel's native-center branch above: a centre
-- frame takes the whole screen, so nothing is being "restored afterwards" the way a vendor
-- window restores the bags it opened, and the was-open memory is deliberately ignored.
-- Range 0..NUM_CONTAINER_FRAMES rather than the old five names — the reference's own
-- CloseAllBags covers the bank bags too (l.702-707), and now so does this.
-- The unconditional close, from the stock ShowUIPanel's centre branch: it ignores the
-- was-open memory and covers the bank bags, as the reference's CloseAllBags does
-- (l.702-707).
function BenillaCloseAllBags()
for bagId = 0, (NUM_CONTAINER_FRAMES or -1) do
CloseBag(bagId)
end
end
-- OpenBackpack — 0561's all-bags form. A bag already open is REMEMBERED and left alone, so
-- the paired CloseBackpack does not close a window the player opened themselves; that
-- memory is the reference's own `backpackWasOpen`, one entry per bag instead of one flag.
-- A bag already open is remembered and left alone, so CloseBackpack leaves open a window
-- the player opened; the reference's `backpackWasOpen`, one entry per bag.
function BenillaOpenBackpack()
for bagId = 0, (NUM_BAG_FRAMES or -1) do
-- The backpack always exists; an equipped-bag window only when a bag is in the
-- slot — `OpenBag` makes the same test itself, but the memory has to agree with it.
-- OpenBag's own test, repeated so the memory agrees with it: the backpack always,
-- another bag only when its slot holds one.
if bagId == 0 or GetContainerNumSlots(bagId) > 0 then
if IsBagOpen(bagId) then
BENILLA_BAG_WAS_OPEN[bagId] = 1
@@ -153,9 +86,8 @@
end
end
-- The publish — after the reference's container file, so these are the definitions that
-- stand (the stock `ShowUIPanel` calls `CloseAllBags` and the vendor's OnShow
-- `OpenBackpack`, and they reach 0561's bodies).
-- Published after the stock ContainerFrame.lua has loaded, so these are the definitions its
-- callers reach (the stock `ShowUIPanel`'s `CloseAllBags`, a vendor's `OpenBackpack`).
OpenBackpack = BenillaOpenBackpack
CloseBackpack = BenillaCloseBackpack
CloseAllBags = BenillaCloseAllBags
+50 -209
View File
@@ -1,106 +1,19 @@
<Ui>
<!--
The GAME MENU — the frame ESC opens (decision 0674). The last hole in the game shell's
outermost layer: until now ESC's chain ran out of things to close and did nothing, and the
Main Menu micro button was drawn but dead (our since-retired MicroMenu.xml's SCOPE note).
PROVENANCE (0985 — the three-source split, stated the same way in every era-shaped
window; OptionsFrame.xml carries the sibling block):
· STRUCTURE & BEHAVIOR — the 1.15 era client (wow-ui-source-era @ 1.15.9.68940): the
ladder shape, its groups and gaps, which entries exist (the SCOPE section below).
If 1.15 has the mechanism, it STAYS.
· CHROME MECHANISM — this window predates 1.13: the 1.12 client's OWN GameMenuFrame
is the chrome ground truth (the dialog backdrop, header plate, red buttons — the
same dialog family the 1.13/1.14 options windows kept), quoted below.
· ART BYTES — the 1.12 MPQ chain only; nothing later-client at runtime (0978's law).
Ground truth, all quoted rather than re-derived:
· **The frame** — ref `GameMenuFrame.xml` (patch.MPQ, extracted): 195x246, `frameStrata`
DIALOG, the UI-DialogBox backdrop with the 11/12/12/11 insets and 32px tile/edge, the
256x64 UI-DialogBox-Header at TOP +12 with MAIN_MENU centered 14px down it, and the
eight-button ladder — first button CENTER on the frame's TOP at (0,-37), each next one
TOP-to-BOTTOM at (0,-1), and Return to Game hung (0,-16) below Exit Game (the one gap in
the stack, which is what makes the last entry read as separate).
· **The button** — ref `GameMenuButtonTemplate` (UIPanelTemplates.xml l.403-410): a 144x21
`UIPanelButtonTemplate` (the UI-Panel-Button Up/Down/Disabled/Highlight four-state art,
TexCoords 0..0.625 / 0..0.6875) whose label trio is overridden to
GameFontHighlight/GameFontHighlight/GameFontDisable — i.e. the menu's labels are WHITE,
not the gold a normal panel button wears, and grey when disabled.
· **The labels** — GlobalStrings.lua (patch-2.MPQ), verbatim: VIDEOOPTIONS_MENU "Video
Options", SOUNDOPTIONS_MENU "Sound Options", UIOPTIONS_MENU "Interface Options",
MACROS "Macros", LOGOUT "Logout", EXIT_GAME "Exit Game",
RETURN_TO_GAME "Return to Game", MAIN_MENU "Main Menu". Declared as the real globals
(they are addon-visible API, decision 0068) and carried on each button's own
`text=` attribute — which this engine resolves as a global-string lookup exactly as
the reference does (decision 0995, `loader/widgets.rs`'s `resolve_text`).
· **The sounds** — the ref's own OnClick lines: igMainMenuOption (the five option buttons),
igMainMenuLogout, igMainMenuQuit, igMainMenuContinue. All four kits verified present in
SoundEntries.dbc (patch-2.MPQ) alongside the igMainMenuOpen/Close pair already in use.
· **The two logout dialogs** — ref `StaticPopup.lua` l.564-600 (CAMP + QUIT) is the
SPECIFICATION for them: the two dialog keys, the field names, the 20 s timeout and the
asymmetric button sets are contract, and the entries carrying them are written here in
our own shape on the shared popup engine (the stock StaticPopup), whose countdown-text branch
already lists both `which` names. Their driver events are the reference's own
(`re/events/event-catalog.tsv`: 276 PLAYER_CAMPING, 277 PLAYER_QUITING,
278 LOGOUT_CANCEL), fired by `crate::ui_logout` off `SMSG_LOGOUT_RESPONSE`.
NAMES are the reference's, unprefixed (`GameMenuFrame`, `GameMenuButton*`) — the same call
our retired MicroMenu.xml made for its straight transcriptions, and required here besides: `ToggleGameMenu`
and `GameMenuFrame` are Era API surface an addon addresses by name.
SCOPE / divergences (noted, not bugs):
- **The ladder is the ERA menu's** (director's call on the 0951 review — supersedes 0951
§3's five-button collapse): the Era client's own ESC menu shape, greyed where benilla
has nothing behind the entry yet. Geometry quoted from the era dump @ 1.15.9 (Gethe
wow-ui-source, classic_era, the 0950 pin):
· Blizzard_SharedXML/Classic/Frame/MainMenuFrameTemplates.xml l.18-33 — the menu is a
VerticalLayoutFrame, buttonTemplate MainMenuFrameButtonTemplate, padding
top/bottom/left/right = 32/28/28/28, spacing 0; l.11-16 — the button is a 144x21
UIPanelButtonTemplate with the white/white/grey label trio (the SAME art + trio the
1.12 GameMenuButtonTemplate carries — no era-only art in this menu at all).
· Shared/Frame/MainMenuFrameTemplates.lua l.61-71 — AddSection() puts a 20-unit gap
(the next button's topPadding) between groups; AddCloseButton uses the same 20.
· Shared/GameMenuFrame.lua l.62-162 — the Era ladder: Options · [gap] · AddOns, Edit
Mode, Support, Macros · [gap] · Logout, Exit Game · [gap] · Return to Game. (Store /
Rewards / What's New / Ratings are structurally off in Era — store disabled, no
account store, IsStandard() false, koKR only.)
· The layout math those constants produce, precomputed here (this engine has no layout
frames) — for OUR seven rungs, the era's eight minus AddOns (next bullet): width
28+144+28 = 200; button tops 32/73/94/115/156/177/218; height 218+21+28 = 267.
Anchored as a TOPLEFT chain so the gaps read in the XML.
· No Key Bindings rung — the era menu's own shape: 1.15 ceded the entry to the
Settings panel, and since decision 1008 ours lives there too (the Options window's
Keybindings category, KeyBindingsPage.xml). 0997 had carved the 1.13/1.14 seat back
in for its standalone window; that seat left with the window.
· No AddOns rung — the director's call: the char-select AddOns screen
(`char_select::addons`, the reference's own glue-side home for this list) is the only
addon UI. The in-game panel 1197 put behind this rung (AddonList.xml) is retired,
and the rung left with it — 1.12's own ESC menu has no AddOns row either.
· The header — Classic/Dialog/DialogTemplates.xml l.11-43 (ClassicDialogHeaderTemplate):
the same 256x64 UI-DialogBox-Header art, text = MAINMENU_BUTTON, swapped to
GameFontNormalMed1 (13px) by MainMenuFrameMixin:OnLoad. Both header idioms (1.12's
TOP+12 texture / era's 39-tall frame with the art centered) seat the art within half
a unit of each other; the 1.12 transcription is kept, the font and string are era's.
- **Edit Mode / Support ship DISABLED** — nothing behind them yet
(`GameMenuButton_Pending`): the template's own `-Disabled` art + grey label, exactly
how the era menu greys a dead entry (SetEnabled(false) → DisabledFont). Each goes live
by deleting one `GameMenuButton_Pending` call and writing its OnClick.
- The Options label stays a literal "Options": 1.12 GlobalStrings has no era
GAMEMENU_OPTIONS (checked; its OPTIONS_MENU is "Options Menu", a different string).
- The ref frame is `toplevel="true" movable="true" parent="UIParent"`. `parent="UIParent"` is
ours too, and it attaches for real: the loader does read a cross-file `parent=`, resolving
the name by global lookup at load (decision 1734), and the frame below declares it. Only
`movable` is dropped, and it is inert in the ref too (GameMenuFrame defines no drag
scripts, so it never actually moves).
`frameStrata="DIALOG"` is what puts it over the world UI, and that is kept.
**The "no frame-level raise" half of that reason is gone**: the raise law is built
(`script::object::toplevel`, wow-re `ui/scratch/toplevel-raise.md`), the loader honours
`toplevel="true"`, and the frame below carries it — so the Esc menu does come to the
front over any other DIALOG-strata frame it overlaps.
The game menu, the frame ESC opens. Its chrome is the reference GameMenuFrame.xml's: DIALOG
strata, the UI-DialogBox backdrop (insets 11/12/12/11, 32px tile and edge), the 256x64
header at TOP +12 with the title 14px down it, the reference's click sounds, and the stock
`GameMenuButtonTemplate`, which the era's MainMenuFrameButtonTemplate matches in art,
size and label fonts.
Deviation: the ladder is the 1.15 era client's ESC menu (GameMenuFrame.lua l.62-162) at
its layout numbers, padding 32 top and 28 elsewhere (MainMenuFrameTemplates.xml l.18-33)
and a 20-unit gap between groups (MainMenuFrameTemplates.lua l.61-71). It drops the era's
AddOns rung, because the character-select AddOns screen is the only addon UI; Key
Bindings is in the options window, as in the era.
Names are the reference's, unprefixed: addons address `GameMenuFrame` by name.
-->
<Script><![CDATA[
-- The reference GlobalStrings (patch-2.MPQ Interface\FrameXML\GlobalStrings.lua), quoted
-- verbatim. `X = X or "..."` so a later real strings table wins, the retired MicroMenu.xml's idiom.
-- Label strings as `X = X or "..."`, so a stock GlobalStrings value wins where one exists.
MAIN_MENU = MAIN_MENU or "Main Menu"
HUD_EDIT_MODE_MENU = HUD_EDIT_MODE_MENU or "Edit Mode"
GAMEMENU_SUPPORT = GAMEMENU_SUPPORT or "Support"
@@ -110,50 +23,29 @@
RETURN_TO_GAME = RETURN_TO_GAME or "Return to Game"
CANCEL = CANCEL or "Cancel"
-- One label setter for a button with nothing behind it yet (Edit Mode and Support): set
-- the ref's text, then disable (grey label + the template's -Disabled art). Making it
-- live is deleting the call and writing its ref OnClick.
-- Labels and disables a rung with nothing behind it yet (Edit Mode, Support), greyed on the
-- template's -Disabled art the way the era menu greys a dead entry.
function GameMenuButton_Pending(button, text)
button:SetText(text)
button:Disable()
end
-- ══ THE THREE STOCK CONSUMERS THAT ASK "IS AN OPTIONS WINDOW OPEN?" (decision 2177) ══
-- All three are the reference's own, off the chain, and all three name the reference's
-- three windows literally. Two of those names are now REAL hidden stock frames
-- (`SoundOptionsFrame` since 2147, `UIOptionsFrame` since 2115) and the third,
-- `OptionsFrame`, became one when the stock VIDEO window joined the manifest — so the
-- whole chain now reads "no", always, while the window the player actually opens is up.
-- Until then `OptionsFrame` WAS our window and every one of them answered by accident.
--
-- **Wrappers, not replacements.** Each stock function keeps running and keeps deciding;
-- ours adds the one term the reference could not know about. A rewrite would be thirty
-- lines of the reference's ladder copied into our tree to drift.
--
-- **HERE, not in `OptionsFrame.xml`, because of load order.** `UpdateMicroButtons` is
-- defined by `MainMenuBarMicroButtons.xml`, manifest l.576 — AFTER our options window at
-- l.522, where a wrapper would capture a nil. This file is the manifest's LAST row
-- (l.1273), so all three exist. It is also the right home on the merits: this is the ESC
-- menu, and these three are exactly "the ESC ladder and the micro button know about our
-- options window".
-- ══ Our options window in the stock "is an options window open?" checks ══
-- The three stock functions name the reference's options windows, which here are hidden
-- stock frames, so each is wrapped to add `BenillaOptionsFrame` and still runs the stock
-- body. Wrapped here because this file loads last: `UpdateMicroButtons`
-- (MainMenuBarMicroButtons.xml) loads after our options window.
local Ref_IsOptionFrameOpen = IsOptionFrameOpen
local Ref_ToggleGameMenu = ToggleGameMenu
local Ref_UpdateMicroButtons = UpdateMicroButtons
-- The one term all three add. Existence-guarded because these three wrappers stand in a
-- file that can be seated without the options window: in the client `BenillaOptionsFrame`
-- is manifest row 38 and this file is the last row, so it is always real — but a test kit
-- that seats the game menu alone must get "no options window is open", not a raise inside
-- the ESC handler.
-- Guarded for a test kit that loads the game menu without the options window.
local function BenillaOptions_IsOpen()
return BenillaOptionsFrame and BenillaOptionsFrame:IsVisible()
end
-- `UIParent.lua:996`. One caller: `CloseAllWindows_WithExceptions`, which passes
-- `ignoreCenter = 1` when an options window is up so the ESC sweep does not close the
-- centre panel out from under it. Ours IS the centre panel (`UIPanelWindows` row,
-- `area = "center"`), so answering "no" would have ESC close it through the sweep instead
-- of through its own rung, discarding pending edits with no Cancel semantics.
-- `UIParent.lua:996`, read by `CloseAllWindows_WithExceptions`: a yes keeps the ESC sweep
-- off the centre panel, which our window is, so ESC closes it through its own rung.
function IsOptionFrameOpen()
if BenillaOptions_IsOpen() then
return 1
@@ -161,20 +53,13 @@
return Ref_IsOptionFrameOpen()
end
-- `UIParent.lua:1465-1497`. The ESC ladder, and the rung matters as much as the answer:
-- the reference tries the static popup FIRST, then an open options window, then the game
-- menu. So this runs that first rung itself and returns if it fired — `StaticPopup_Escape -- Pressed` is a pure scan when nothing is shown (`StaticPopup.lua:1879-1893`), so the
-- delegate below re-running it costs nothing and changes nothing — then takes the options
-- rung, then hands the rest of the ladder back untouched.
--
-- The close is our own Close button's click, which is the same discard-and-hide the
-- reference reaches through `OptionsFrameCancel:Click()`: our `<OnHide>` drops the pending
-- page. (That alias is retired — `OptionsFrameCancel` is the stock video window's own
-- button now, and ours loading later would have clobbered it.)
-- `UIParent.lua:1465-1497`, the ESC ladder: static popup first, then an open options
-- window, then the rest of the stock ladder. The stock body re-runs the popup rung, a pure
-- scan when nothing is shown (`StaticPopup.lua:1879-1893`). Our Close button's click is
-- the discard-and-hide the reference reaches through `OptionsFrameCancel:Click()`.
function ToggleGameMenu(clicked)
if clicked then
-- The reference's `clicked` arm closes an open options window and then goes on to
-- toggle the game menu, rather than returning. Same here.
-- As the reference's `clicked` arm: close an options window, then still toggle.
if BenillaOptions_IsOpen() then
BenillaOptionsFrameCloseButton:Click()
end
@@ -190,17 +75,9 @@
return Ref_ToggleGameMenu(clicked)
end
-- `MainMenuBarMicroButtons.lua:47-58`. A POST-hook that can only ever push the button, never
-- release it: the reference has already decided from the six frames it knows, and this adds
-- the seventh. Written this way round so it cannot fight that decision — a wrapper that also
-- set NORMAL would be a second copy of the reference's whole `or` chain.
--
-- **Existence-guarded, and only here.** The shipped manifest always carries
-- `MainMenuBarMicroButtons.xml`, so in the client `Ref_UpdateMicroButtons` and
-- `MainMenuMicroButton` are always real. A partial test kit that seats this file and not
-- the micro bar is the case the guards are for, and a guard is the honest answer there:
-- there is no micro button to push, so there is nothing to do — as against a raise inside
-- an unrelated window's OnShow, which is what an unguarded wrapper turns that into.
-- `MainMenuBarMicroButtons.lua:47-58`: a post-hook that only ever pushes the Main Menu
-- button, never releases it, so the stock decision over its six frames stands and ours adds
-- the seventh. Guarded for a test kit that loads this file without the micro bar.
function UpdateMicroButtons()
if Ref_UpdateMicroButtons then
Ref_UpdateMicroButtons()
@@ -210,35 +87,20 @@
end
end
-- The CAMP/QUIT dialogs and the three-event driver MOVED OUT, to our UIParent.lua counterpart (the stock UIParent.lua since 1988) — the
-- reference's own homes for both (StaticPopup.lua l.564/586; UIParent.lua l.304-315).
-- They were here only because this is where they were first needed, and 1751 moving them
-- is one less thing to carry when this window follows them onto the chain.
]]></Script>
<!-- The era header text size (Classic/Dialog/DialogTemplates.xml + the OnLoad font swap):
GameFontNormalMed1 = SystemFont_Med2 = FRIZQT 13, gold, the MasterFont shadow. Era's
name, declared here beside its one consumer (the 0951 residue convention: Fonts.xml
stays the complete 1.12 set). -->
<!-- The era's header font, GameFontNormalMed1 (FRIZQT 13, gold): an era name 1.12's Fonts.xml
lacks, declared beside its one consumer. -->
<Font name="GameFontNormalMed1" inherits="GameFontNormal" virtual="true">
<FontHeight><AbsValue val="13"/></FontHeight>
</Font>
<!-- `GameMenuButtonTemplate` MOVED OUT, to UIPanelTemplates.xml — the reference's own file for
it (ref UIPanelTemplates.xml l.403-410), and now ColorPickerFrame.xml's Okay/Cancel need it
too, from far above this file's LAST-in-the-toc seat. It lived here, with its four state
textures inlined, only because `UIPanelButtonTemplate` had not been transcribed yet; it now
has been, so the reference's actual body (`inherits="UIPanelButtonTemplate"`, 144x21, the
white/white/grey label trio) replaces the private copy verbatim. Era's
MainMenuFrameButtonTemplate (Classic/Frame/MainMenuFrameTemplates.xml l.11-16) is that same
button — same art, same trio, same size — so the header's ART BYTES note is unchanged. -->
<!-- `toplevel` is the ref's own (GameMenuFrame.xml l.3) and was dropped here: without the bit
a DIALOG-strata window opened over another one stays behind it (the B180/Bagnon class,
decision 1318). Restored alone; the ref's `movable`/`parent` stay off deliberately. -->
<!-- The reference frame's attributes (GameMenuFrame.xml l.3) except `movable`, which is inert
there: the reference defines no drag scripts. -->
<Frame name="GameMenuFrame" parent="UIParent" frameStrata="DIALOG" toplevel="true" enableMouse="true" hidden="true">
<!-- The era layout engine's own numbers (header SCOPE note): 28+144+28 wide, 32-top /
28-bottom padding around our seven 21-unit rungs with three 20-unit section gaps. -->
<!-- The era layout: 28+144+28 wide; 32 top and 28 bottom padding around seven 21-unit
rungs and three 20-unit section gaps. -->
<Size><AbsDimension x="200" y="267"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
@@ -252,9 +114,8 @@
<Size><AbsDimension x="256" y="64"/></Size>
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="12"/></Offset></Anchor></Anchors>
</Texture>
<!-- Era swaps the header text to 13px (MainMenuFrameMixin:OnLoad →
SetHeaderFont(GameFontNormalMed1)); the seat is the 1.12 one (within half a
unit of era's — header SCOPE note). -->
<!-- The era's 13px header font (MainMenuFrameMixin:OnLoad) on the 1.12 seat, which
is within half a unit of the era's. -->
<FontString name="GameMenuFrameTitle" inherits="GameFontNormalMed1" text="MAIN_MENU">
<Anchors>
<Anchor point="TOP" relativeTo="GameMenuFrameHeader"><Offset><AbsDimension x="0" y="-14"/></Offset></Anchor>
@@ -263,18 +124,10 @@
</Layer>
</Layers>
<Frames>
<!-- The era ladder minus its AddOns rung (header SCOPE notes): Options · gap ·
Edit Mode, Support, Macros · gap · Logout, Exit Game · gap · Return to Game.
A TOPLEFT chain at the era layout engine's numbers — x 28, first top -32, 0
between group members, -20 at each section break. Live entries wire the era
OnClick intent; pending ones grey out through GameMenuButton_Pending until
something exists behind them. -->
<!-- Options — live: the era GameMenuFrame.lua l.88 entry (ShowUIPanel(SettingsPanel)
there, BenillaOptionsFrame here) plus an explicit HideUIPanel first. The ref leans on
the center-replacement rule for the swap (ShowUIPanel l.697-702: native-center
frames replace each other); hiding the menu explicitly says the same thing
without relying on the eviction path. -->
<!-- The era's Options entry (GameMenuFrame.lua l.88), opening our options window. A
literal label: 1.12 has no GAMEMENU_OPTIONS, and its OPTIONS_MENU is a different
string, "Options Menu". -->
<Button name="GameMenuButtonOptions" inherits="GameMenuButtonTemplate" text="Options">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="28" y="-32"/></Offset></Anchor>
@@ -304,8 +157,7 @@
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonSupport" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<!-- LIVE since decision 0983 (MacroFrame.xml). The ref's own body
(GameMenuFrame.xml l.118-122): hide the menu, then ShowMacroFrame(). -->
<!-- The reference's body (GameMenuFrame.xml l.118-122). -->
<Scripts>
<OnClick>
PlaySound("igMainMenuOption")
@@ -315,10 +167,8 @@
</Scripts>
</Button>
<!-- The three that are live. The ref's OnShow on both Logout and Quit disables the
button while either logout dialog is already up (you cannot re-arm a running
countdown); it runs on the menu's own show, which is when a re-opened menu has to
re-read that state. -->
<!-- The reference's OnShow disables Logout and Exit Game while a CAMP or QUIT countdown
is up, so a running countdown cannot be re-armed. -->
<Button name="GameMenuButtonLogout" inherits="GameMenuButtonTemplate" text="LOGOUT">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="GameMenuButtonMacros" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor>
@@ -369,23 +219,14 @@
</Scripts>
</Button>
</Frames>
<!-- ref GameMenuFrame OnShow/OnHide, verbatim: the micro row's Main Menu button pushes in
while the menu is up, and the bag bar greys out under it (you cannot open bags from
behind the menu — ShowUIPanel's center gate would refuse the window anyway, so the
greying is the visible half of a rule the panel manager already enforces). Guarded, in
the `CloseMenus and CloseMenus()` posture our retired panel file used: a headless test
loads this file without the action bar behind it. -->
<!-- The reference's OnShow/OnHide: the Main Menu micro button pushes in and the bag
buttons grey out while the menu is up. Guarded for a test that loads this file
without the action bar. -->
<Scripts>
<OnShow>
-- The shared era-window scale first (ERA_WINDOW_SCALE, OptionsFrame.xml — loaded
-- before this file): the era menu has no scale of its own (GameMenuFrame.xml +
-- MainMenuFrameTemplates, grepped clean), it draws at UIParent's px-per-unit —
-- the SAME density as the era options window, whose checkFit caps at 1. The knob
-- is our stand-in for that UIParent scale; without it the menu drew 28% larger
-- relative to the options window than the era pair ("esc menu scale too big" —
-- director, 2026-08-04). `or 1` guards a load-order regression only. NOTE: this
-- lives INSIDE the existing OnShow — a second Scripts/OnShow block on the same
-- frame would silently REPLACE this one (SetScript semantics: last wins).
-- Deviation: ERA_WINDOW_SCALE (OptionsFrame.xml, loaded earlier) stands in for the
-- UIParent scale the era draws this menu and its options window at.
-- A second OnShow on this frame would replace this one: the last SetScript wins.
this:SetScale(ERA_WINDOW_SCALE or 1)
if UpdateMicroButtons then UpdateMicroButtons() end
if Disable_BagButtons then Disable_BagButtons() end
+71 -148
View File
@@ -1,77 +1,23 @@
<Ui>
<!--
The KEY BINDINGS page (decision 1008, superseding 0997's standalone window) — the
Options window's Keybindings category, over the engine's binding table
(benilla_ui::script::keybind + crate::bindings — GetNumBindings/GetBinding/SetBinding/
GetBindingKey/GetBindingAction/LoadBindings/SaveBindings/GetCurrentBindingSet, the 1.12
Lua API verbatim). This file is the page MODULE: the templates and the whole script; the
page's static furniture (the rows, the checkbox row, the scroll seam, the search rows)
lives in OptionsFrame.xml as ordinary children of its Keybindings body, inheriting these
templates (manifest order puts this file first). Rows are the HONEST TREE: only commands
the engine really dispatches are registered, so only they (and only their non-empty
categories) appear.
PROVENANCE (0985 — the three-source split; OptionsFrame.xml carries the sibling block):
· STRUCTURE & BEHAVIOR — the era Settings panel's own Keybindings page (wow-ui-source-era
@ 1.15.9.68940: Blizzard_SettingsDefinitions_Frame/Keybindings.lua + Classic/
KeybindingsOverrides.lua + Blizzard_Settings_Shared/Blizzard_Keybindings.*): 1.15 put
keybindings INSIDE the Options window — the director's call this move executes — as
ONE list of EXPANDABLE per-category sections (SettingsKeybindingSectionMixin over
SettingsExpandableSectionMixin; 25-tall rows, sections COLLAPSED by default, categories
in first-appearance order, a bar's section existing only when the bar does — their
PROXY_SHOW_ACTIONBAR_N gate is our honest tree), the Character Specific Keybindings
checkbox as the page's first row (PROXY_CHARACTER_SPECIFIC_BINDINGS, confirm popup on
UNCHECK only), and per-binding search tags whose category REDIRECTS to this page
(Blizzard_SettingsPanel.lua l.578 `category.redirectCategory or category`) with the
matched rows LIVE in the results.
· CAPTURE & SET LAW — 1.12's own Blizzard_BindingUI (extracted from patch.MPQ, 0997),
KEPT over the era's listener where they differ (the game's fidelity truth is 1.12;
the window furniture is the era's): click a key capsule to arm; the next key or
mouse-3/4/5 press binds IMMEDIATELY (the host capture seam hands the canonical
ALT-CTRL-SHIFT chord to KeyBindings_OnHostKey); a stolen key names its victim in red
(KEY_UNBOUND_ERROR) only when the victim goes bare; left/right clicks select/deselect,
never bind; lone modifiers are ignored; the wheel binds like any other key, to ANY
command (decision 1295 — SetBinding refuses nothing but a key string that is not a key,
and the KEYBINDINGFRAME_MOUSEWHEEL_ERROR arm below is that refusal's message, not a
wheel rule); ESC while armed BINDS Escape (1.12's OnKeyDown has no ESCAPE filter — the
era's ESC-cancels law is the one era behavior NOT taken, recorded in 1008; the Unbind
Key button is the clearing affordance). ESC with nothing armed never reaches any of
this: the ladder is the TOGGLEGAMEMENU binding, and the armed seam swallows the press.
· ART BYTES — the 1.12 MPQ chain only (0978/0985): the capsules wear 1.12's own
UIPanelButtonTemplate2 panel-button art (the Classic client's silver stretch set —
UI-Silver-Button-Up/-Down/-Highlight/-Select, a 9-slice — is VERIFIED ABSENT from
the 1.12 chain: 0997, re-pinned and reverted-to-red in 1088 after 1085's gray-dress
detour), selection is 1.12's LockHighlight glow; section headers wear the 1.12
collapsible-list dress — the QuestLog's own UI-PlusButton/UI-MinusButton square
and gold label.
COMMIT LAW (1008, the page's one big behavior change from 0997): binds are LIVE-COMMIT.
The options window's whole grammar writes live (SetCVar on click, no Okay/Cancel pair),
and the era Settings panel commits keybinds as they land too — so every successful
bind/steal/unbind/reset saves the current set to disk at once
(SaveBindings(GetCurrentBindingSet())); 0997's Okay/Cancel/revert and the
CONFIRM_LOSE_BINDING_CHANGES popup die with the standalone window. The two destructive
gestures keep their confirms: Reset To Default (the page Defaults button) and leaving
the character-specific set (which permanently deletes it).
All strings are the 1.12 GlobalStrings globals (the VM executes the real file at boot)
through the `getglobal(...) or raw` fallback (the era's own `_G[x] or x` posture), so a
headless test without GlobalStrings still renders raw tokens.
SCOPE / divergences (noted, not bugs — the ledger is 1008):
- No Quick Keybind Mode row (era furniture over an overlay mechanism benilla lacks).
- 1.12's BLANK spacer-headers between multibar blocks are not modeled — both bottom
bars sit under the one MultiActionBar section.
- 1.12's "Key Bindings for <name>" title swap died with the standalone window: the page
title stays the category's, the checkbox is the character-set state (era's own shape).
- Search shows at most KEYBINDINGS_SEARCH_ROWS matched bindings (static row pool; the
era's results list scrolls, ours doesn't yet) — the group head jumps to the full page.
The key bindings page: the options window's Keybindings category, over the engine's
binding table and the 1.12 binding API. This file holds the templates and the script; the
rows are children of the Keybindings body in OptionsFrame.xml, which loads after it. Only
commands the engine dispatches are registered, so only they and their categories appear.
Deviation: the page is the era Settings panel's Keybindings page (Keybindings.lua,
KeybindingsOverrides.lua): one list of collapsible category sections, the Character
Specific Keybindings checkbox as its first row, and search results that open this page.
Capture and set follow 1.12's Blizzard_BindingUI where the two differ: a capsule click
arms, the next key or mouse 3/4/5 press binds at once, left and right clicks only select,
the wheel binds like any key, and ESC while armed binds Escape (1.12's OnKeyDown has no
ESCAPE filter; Unbind Key clears).
Deviation: every bind, unbind and reset saves at once, as the era panel commits, where
1.12 had Okay and Cancel; only Reset To Default and leaving the character set confirm.
-->
<!-- One KEY CAPSULE (1.12 KeyBindingFrameBindingButtonTemplate: UIPanelButtonTemplate2 at
180x22 with the small-highlight font trio; its RegisterForClicks takes all five
buttons so middle/4/5 presses reach the capture law). The SELECTED state is 1.12's
LockHighlight — the ADD highlight held on. -->
<!-- A key capsule, 1.12's KeyBindingFrameBindingButtonTemplate: UIPanelButtonTemplate2 art at
180x22 with the small font trio, registered for all five buttons so a middle, 4 or 5
press reaches the capture. Selected is its LockHighlight, the ADD highlight held on. -->
<Button name="KeyBindingButtonTemplate" virtual="true">
<Size><AbsDimension x="180" y="22"/></Size>
<ButtonText name="$parentText"/>
@@ -87,14 +33,10 @@
</Scripts>
</Button>
<!-- One LIST ROW, dual-mode (the era list mixes section headers and binding rows in one
25-tall rhythm — SettingsKeybindingSectionInitializer:GetExtent's single bindingHeight).
BIND mode: command name 170 wide at LEFT, the two capsules at 175/355 (0997's seats,
kept). HEADER mode: the whole row is one toggle button wearing 1.12's own collapsible
dress — the QuestLogTitleButton's 16x16 UI-Plus/MinusButton square at LEFT and the gold
label (the era's Options_ListExpand arrows have no 1.12 counterpart). The paint
(KeyBindings_PaintBindRow / _PaintHeaderRow) shows one mode's furniture and hides the
other's. -->
<!-- A list row, painted as a binding (name, two capsules) or as a section header: one toggle
in 1.12's QuestLogTitleButton dress, the 16x16 plus/minus square and gold label, since
the era's expand arrows are not in 1.12. Both are 25 tall, the era's one row height
(SettingsKeybindingSectionInitializer:GetExtent). -->
<Frame name="KeyBindingRowTemplate" virtual="true">
<Size><AbsDimension x="560" y="25"/></Size>
<Layers>
@@ -141,19 +83,17 @@
</Frame>
<Script><![CDATA[
-- ── Strings ── the 1.12 GlobalStrings globals, each with the raw fallback (a headless
-- harness without GlobalStrings still works; the era's own `_G[x] or x` posture).
-- ── Strings ── a 1.12 GlobalStrings global, or the fallback for a harness without them.
function KeyBindings_String(name, fallback)
local s = getglobal(name)
if s then return s end
return fallback
end
-- ── Key display ── the 1.12 GetBindingText law by hand: modifier prefixes stay the
-- canonical uppercase tokens, the base key reads its KEY_* global (Spacebar, Up Arrow,
-- Mouse Button 4...) else itself. "CTRL--" renders CTRL + the minus key. (The action
-- bar's ABBREVIATED labels ride the stock GetBindingText (UIParent.lua);
-- this page and the tooltips show the full form, both references' own split.)
-- ── Key display ── 1.12's GetBindingText in full form, as the reference shows it here and
-- in tooltips (the action bar's abbreviated form is the stock GetBindingText): modifier
-- prefixes stay uppercase tokens, the base key reads its KEY_* global, else itself.
-- "CTRL--" is CTRL plus the minus key.
function KeyBindings_KeyText(key)
if not key then
return NORMAL_FONT_COLOR_CODE .. KeyBindings_String("NOT_BOUND", "Not Bound") .. FONT_COLOR_CODE_CLOSE
@@ -178,10 +118,9 @@
return prefix .. KeyBindings_String("KEY_" .. rest, rest)
end
-- ── Page state ── no root frame anymore: the page's state lives here. `sections` is the
-- era section model; `selected`/`keyID` the 1.12 selection; `searchPainted` the live
-- search-result rows (rowName → binding index), set by OptionsFrame.xml's search display
-- and repainted after a bind lands on one.
-- ── Page state ── `sections` is the era section model, `selected`/`keyID` the 1.12
-- selection, and `searchPainted` maps OptionsFrame.xml's live search rows (row name to
-- binding index).
KeyBindingsPage = { sections = {}, items = {} }
KEYBINDINGS_ROW_BASE = "BenillaOptionsFrameContainerBodyKeybindingsRow"
@@ -189,10 +128,9 @@
KEYBINDINGS_ROW_HEIGHT = 25
KEYBINDINGS_SEARCH_ROWS = 8
-- ── The section model (era CreateKeybindingInitializers, cut to the honest tree): walk
-- GetBinding(i), group runs of one category token; order = first appearance, which IS
-- 1.12's Bindings.xml order (the registry registers in it). Sections default COLLAPSED
-- (the era Classic overrides pass no `expanded`); a rebuild keeps each token's state.
-- ── Section model ── the era's CreateKeybindingInitializers: one section per category
-- token in first-appearance order, which is 1.12's Bindings.xml order. Sections start
-- collapsed (the era's Classic overrides pass no `expanded`); a rebuild keeps each state.
function KeyBindings_BuildSections()
local page = KeyBindingsPage
local was = {}
@@ -214,8 +152,8 @@
KeyBindings_Flatten()
end
-- The flattened visible list (era EvaluateVisibility): every header, then the expanded
-- sections' rows. The faux scroll runs over this.
-- The visible list the faux scroll runs over (the era's EvaluateVisibility): each header,
-- then its rows when expanded.
function KeyBindings_Flatten()
local page = KeyBindingsPage
page.items = {}
@@ -229,8 +167,7 @@
end
end
-- The expand/collapse click (era OnExpandedChanged; the sound is the checkbox kit's,
-- the page's one click voice).
-- A header click (the era's OnExpandedChanged), voiced with the checkbox sound.
function KeyBindings_ToggleSection(n)
local sec = KeyBindingsPage.sections[n]
if not sec then return end
@@ -240,8 +177,7 @@
KeyBindingsPage_Update()
end
-- Expand/collapse by section index without the click voice — the capture fixture uses
-- it to open a section for the lens.
-- Expands or collapses a section silently; the capture fixture opens sections with it.
function KeyBindings_ExpandSection(n, on)
local sec = KeyBindingsPage.sections[n]
if not sec then return end
@@ -249,9 +185,8 @@
KeyBindings_Flatten()
end
-- ── Painters ── one row, one mode. Shared by the list repaint and the search rows
-- (which are the SAME template painted bind-mode — the era reflows live rows into its
-- results, and these capsules run the full capture law in place).
-- ── Painters ── one row in one mode, shared by the list and the search rows, whose
-- capsules capture in place as the era's live results do.
function KeyBindings_PaintBindRow(name, index)
local command, _, key1, key2 = GetBinding(index)
local b1 = getglobal(name .. "Key1Button")
@@ -265,7 +200,6 @@
local desc = getglobal(name .. "Description")
desc:SetText(KeyBindings_String("BINDING_NAME_" .. command, command))
desc:Show()
-- The 1.12 selection highlight: the pressed capsule holds its glow.
local page = KeyBindingsPage
if page.selected == command and page.keyID == 1 then
b1:LockHighlight()
@@ -320,16 +254,14 @@
end
FauxScrollFrame_Update(scroll, table.getn(page.items),
KEYBINDINGS_DISPLAYED, KEYBINDINGS_ROW_HEIGHT)
-- The trough is OURS — the reference has no trough at all, so nothing in its kit shows
-- one. The owner pairs it with the bar the reference's `_Update` just decided about
-- (1860); `BenillaScrollBar_SetShown` used to do it and is off this path now.
-- The trough is ours and the stock kit never shows one, so it follows the bar
-- `FauxScrollFrame_Update` has just shown or hidden.
BenillaScrollTrough_Follow(scroll:GetName())
KeyBindings_UpdateUnbindKey()
end
-- The page entry (BenillaOptionsFrame_SelectCategory's Keybindings arm): rebuild the tree —
-- the table may have moved under us (a rebind, a set switch) — sync the checkbox to
-- the live set, park the scroll at the top, repaint.
-- The page entry (BenillaOptionsFrame_SelectCategory): rebuilds the sections, since a
-- rebind or a set switch may have changed the table, then repaints from the top.
function KeyBindingsPage_Refresh()
KeyBindings_BuildSections()
local box = getglobal("BenillaOptionsFrameContainerBodyKeybindingsCharacterRowCheck")
@@ -346,9 +278,9 @@
getglobal("BenillaOptionsFrameContainerBodyKeybindingsOutput"):SetText(text)
end
-- ── Selection + the capture arm ── selecting a capsule arms the HOST's capture seam
-- (BenillaBindCapture): raw input is swallowed there and comes back canonical through
-- KeyBindings_OnHostKey. Deselecting (either way) disarms.
-- ── Selection ── selecting a capsule arms the host's capture (BenillaBindCapture), which
-- swallows raw input and hands the canonical chord to KeyBindings_OnHostKey; deselecting
-- disarms.
function KeyBindings_SetSelected(command, keyID)
KeyBindingsPage.selected = command
KeyBindingsPage.keyID = keyID
@@ -364,9 +296,8 @@
end
end
-- Repaint whichever view currently shows binding rows: the page list, or the live
-- search-result rows (a bind landed from one must relabel it — and its victim, which
-- may sit on another visible row).
-- Repaints whichever view shows binding rows, the list or every search row, since a bind
-- from one search row can unbind a victim shown on another.
function KeyBindings_RepaintVisible()
if KeyBindingsPage.searchPainted then
for name, index in pairs(KeyBindingsPage.searchPainted) do
@@ -379,10 +310,9 @@
end
function KeyBindingButton_OnClick(button)
-- The 1.12 law: while something is selected, only LEFT/RIGHT on a capsule
-- re-select or deselect — a middle/4/5 press is a BIND, and the host's armed
-- capture seam (which runs after this click) takes it, so this handler must not
-- touch the selection for those buttons.
-- As 1.12: while armed, only a left or right click reselects or deselects. A middle,
-- 4 or 5 press is a bind that the armed capture takes after this click, so it must
-- leave the selection alone.
local page = KeyBindingsPage
if page.selected then
if button ~= "LeftButton" and button ~= "RightButton" then
@@ -406,15 +336,14 @@
KeyBindings_RepaintVisible()
end
-- The live-commit half of every mutation (1008's commit law — the header block): the
-- table already moved; persist the set it belongs to.
-- The live commit: saves the set a bind, unbind or reset has just changed.
function KeyBindings_Commit()
SaveBindings(GetCurrentBindingSet())
end
-- The host's capture callback: `chord` is canonical (ALT-CTRL-SHIFT-TOKEN). The body is
-- 1.12's OnKeyDown selected-branch verbatim: steal message, clear the command's keys,
-- rebind in slot order so the pressed slot takes the new chord — then the live commit.
-- The host's capture callback, `chord` canonical (ALT-CTRL-SHIFT-TOKEN): 1.12's OnKeyDown
-- armed branch, then the live commit. A stolen chord names its victim in red only when it
-- was the victim's last key; the command's keys rebind in slot order.
function KeyBindings_OnHostKey(chord)
local page = KeyBindingsPage
if not page.selected then
@@ -449,11 +378,9 @@
KeyBindings_RepaintVisible()
end
-- 1.12 KeyBindingFrame_SetBinding, kept verbatim: a refused bind restores the slot's old
-- key and says why. The reference wrote this arm with the wheel in mind and named its
-- string for it, but the engine refuses only a key string that is not a key
-- (decision 1295), so this fires for a chord the capture seam could not name — never for
-- a wheel notch. The message stays 1.12's; ours is not the place to correct Blizzard.
-- 1.12's KeyBindingFrame_SetBinding: a refused bind restores the slot's old key. SetBinding
-- refuses only a string that is not a key, so this fires for a chord the capture could not
-- name, never for the wheel; the message stays 1.12's wheel text.
function KeyBindings_SetOrRestore(key, command, oldKey)
if not SetBinding(key, command) then
if oldKey then SetBinding(oldKey, command) end
@@ -463,8 +390,8 @@
end
end
-- The Unbind Key button (the Container header band, OptionsFrame.xml): 1.12's body —
-- the selected slot empties, the other slides into slot order — plus the live commit.
-- The Unbind Key button (OptionsFrame.xml): 1.12's body, which empties the selected slot
-- and slides the other into slot order, then the live commit.
function KeyBindings_Unbind()
local page = KeyBindingsPage
local key1, key2 = GetBindingKey(page.selected)
@@ -481,14 +408,12 @@
KeyBindings_RepaintVisible()
end
-- ── The character-specific set (1.12's three-set model under the era checkbox row) ──
-- CHECK: switch to the character set and save it at once — it starts as a copy of what
-- you had (LoadBindings(2) falls back to the account snapshot; SaveBindings(2) makes it
-- exist, which IS the checkbox's persisted truth). UNCHECK: destructive — the era
-- confirm-on-uncheck-only law; the box springs back until the popup decides. The accept
-- order is load-then-save: LoadBindings(1) puts the account set live FIRST, so
-- SaveBindings(1) persists it unchanged while dropping the character set (the confirmed
-- permanent delete — saving live-as-account here would smuggle the character binds in).
-- ── Character-specific set ── 1.12's binding sets under the era's checkbox. Checking
-- saves set 2 at once, a copy of the current binds (LoadBindings(2) falls back to the
-- account set), and that saved set is what the checkbox reads. Unchecking deletes it, so
-- it confirms (the era confirms only on uncheck) and the box stays checked meanwhile.
-- The accept loads set 1 before saving it; saving first would write the character binds
-- into the account set.
function KeyBindings_CharacterToggle(box)
PlaySound("igMainMenuOptionCheckBoxOn")
if box:GetChecked() then
@@ -516,9 +441,8 @@
KeyBindingsPage_Refresh()
end,
}
-- Reset To Default — the page's Defaults button (era: Defaults confirms; the string is
-- ours, 0997's note: the era's lives in its client binary, 1.12 has none). The reset
-- lands in the CURRENT set and commits live like every other mutation.
-- Reset To Default, the page's Defaults button: it confirms, as the era's does, with our
-- own string since 1.12 has none, and resets the current set with a live commit.
StaticPopupDialogs["CONFIRM_RESET_KEYBINDINGS"] = {
text = "Are you sure you want to reset all keybindings to their defaults?",
button1 = KeyBindings_String("OKAY", "Okay"),
@@ -533,12 +457,11 @@
end,
}
-- ── Search (the era redirect law) ── every binding's search tag is its display name
-- uppercased (era AddSearchTags(GetBindingName(action))); matches score by the shared
-- law (OptionsSearch_Score) and surface under the Keybindings group head, whose click
-- jumps to this page (era Blizzard_SettingsPanel.lua l.578: the fake category's results
-- carry `redirectCategory`). The matched rows are LIVE (era reflows real initializers);
-- OptionsFrame.xml paints them onto its static KeybindSearch row pool.
-- ── Search ── a binding's tag is its display name uppercased (the era's
-- AddSearchTags(GetBindingName(action))); matches list under the Keybindings group head,
-- which opens this page (the era's `redirectCategory`, Blizzard_SettingsPanel.lua l.578).
-- OptionsFrame.xml paints them as live rows. Deviation: a static pool of
-- KEYBINDINGS_SEARCH_ROWS rows, where the era's results scroll.
function KeyBindings_SearchMatches(words)
local out = {}
for i = 1, GetNumBindings() do
+51 -120
View File
@@ -1,68 +1,31 @@
<Ui>
<!--
benilla's **script error log** (decision 1495) — the window B293 asked for, and it is
entirely OURS: 1.12 has no counterpart, so every name here wears the `Benilla` prefix (1254's
rule, `tests::reference_surface`'s gate).
## Why it exists beside `ScriptErrors`
`BasicControls.xml`'s red `ScriptErrors` dialog is the reference's whole answer to a Lua
fault, transcribed and installed by 1305 — and it stays exactly as it is. It shows **one**
message, because `_ERRORMESSAGE` guards on `ScriptErrors:IsVisible()`, and it remembers
nothing. Two reporters said so within days of it shipping: Goudy, *minutes after
screenshotting that dialog working*, *"some kind of lua error frame probably needs to be
implemented, as there are a lot of addons that still doesn't work"*; nazriel_0, *"probably we
should start using something like Bug Grabber to report those stack traces better"*.
So: the dialog is the interrupt, this is the log. Additive, no divergence — the reference
simply never had this instrument.
## Shape
`CraftFrame.xml`'s shape, which is the house idiom for "windowed panel, faux-scrolled row
list, detail pane that repaints on selection": 13 fixed 16px rows over a
`FauxScrollFrameTemplate`, and a **real** `<ScrollFrame>` detail pane below (the real kit,
not the faux one — a message is a `mlua` error string with its `stack traceback:` tail
attached, so the detail is genuinely variable-height content, which is what the real kit is
for; `GossipFrame.xml`'s greeting pane is the same wiring).
The window registers itself in `UIPanelWindows` (below; the table is the stock UIParent.lua's
since 1988), which is what seats it in the
left slot and gets it closed by `CloseAllWindows()` — i.e. by ESC — with no code here. Our
own toplevels deliberately stay OUT of `UISpecialFrames`: that table is the addons' (0544's
note, 1206).
## Reading the model
Three engine reads, all from the retained log on the model
(`benilla_ui::script::diagnostics`): `BenillaGetNumScriptErrors()` →
`retained, totalEverRecorded`; `BenillaGetScriptErrorInfo(i)` → `seq, kind, message, count`
(1-based, oldest first, nil past the end); `BenillaClearScriptErrors()`. There is no per-frame
feed and nothing to keep in step — the window polls the log it is a view of.
Benilla's script error log. 1.12 has no such window, so every name here carries the
`Benilla` prefix (`tests::reference_surface` checks it). It sits beside the stock
`ScriptErrors` dialog, which shows one message and keeps none: the dialog interrupts,
this window keeps the log.
Shaped like the reference's CraftFrame: 13 fixed 16px rows over the stock faux-scroll kit
and a detail pane below. It registers in `UIPanelWindows`, which seats it in the left slot
and closes it on ESC, and stays out of `UISpecialFrames`, which belongs to addons.
The engine's log API: `BenillaGetNumScriptErrors()` returns retained and total-ever
counts; `BenillaGetScriptErrorInfo(i)` returns `seq, kind, message, count` (1-based,
oldest first, nil past the end); `BenillaClearScriptErrors()`. The window polls it.
-->
<Script><![CDATA[
-- The slot-manager row (decision 1495): a left-area window one tier above every stock
-- panel's pushable, so opening a game window pushes the log aside rather than the reverse.
-- A left-slot window with a pushable above every stock panel's, so a game window opening
-- pushes the log aside.
UIPanelWindows["BenillaScriptLogFrame"] = { area = "left", pushable = 9, whileDead = 1 }
BENILLA_SCRIPTLOG_ROWS = 13
BENILLA_SCRIPTLOG_ROW_HEIGHT = 16
-- How much of a message a ROW shows. The full text is the detail pane's job; a row is an
-- index, and a row that wraps would break the fixed-height list the faux kit requires.
-- How much of a message a row shows; a wrapped row would break the faux kit's fixed rows.
BENILLA_SCRIPTLOG_ROW_CHARS = 52
-- Which row is selected (a `seq`, not an index — indices shift when the cap evicts, and a
-- selection that silently slides onto a different error is worse than one that goes away).
-- The selected row's `seq`, not its index: indices shift when the cap evicts.
BenillaScriptLogSelected = nil
-- The first line of a message, trimmed to a row's width. A traceback's later lines are
-- indented continuations; on one row they read as noise.
-- `strsub` is BYTE-indexed (Lua 5.0 strings are byte strings, and benilla keeps them that
-- way — decision 1193), so a cut that lands inside a multi-byte character leaves half of
-- one. Every message here is UTF-8 and ours are full of em dashes, so this was not
-- theoretical: the log window's own repaint raised on the first warning it was ever asked
-- to show (2135/2138). Back the cut up over any continuation byte (`10xxxxxx`, i.e.
-- 0x80..0xBF) so it lands on a character boundary.
-- A message's first line, trimmed to a row. `strsub` counts bytes and messages are UTF-8,
-- so the cut backs up over continuation bytes (0x80..0xBF) onto a character boundary.
function BenillaScriptLog_RowText(message)
local cut = strfind(message, "\n", 1, 1)
local head = message
@@ -79,13 +42,10 @@
return head
end
-- The three kinds, coloured by CONSEQUENCE — how much of the addon is broken:
-- `load` red an addon is not running at all (the thing a player can act on)
-- Coloured by how much of the addon is broken:
-- `load` red the addon is not running at all
-- `error` amber code ran and raised; the addon is otherwise up
-- `warn` grey a call was accepted and quietly did not do what it said (2135)
-- Grey rather than a fourth bright colour because a warning is the least urgent of the
-- three and there are more of them: colour has to keep ranking the list, not just label
-- it. Colours come from the shipped tables so this window matches the rest of the UI.
-- `warn` grey a call was accepted and quietly did not do what it said
function BenillaScriptLog_RowColor(kind)
if kind == "load" then return 1.0, 0.3, 0.3 end
if kind == "warn" then return 0.65, 0.65, 0.65 end
@@ -95,12 +55,8 @@
function BenillaScriptLog_Update()
local shown, total = BenillaGetNumScriptErrors()
-- The summary line carries the one number a bug report needs and the window cannot
-- otherwise show: how many were DROPPED. `total` counts distinct failures ever
-- recorded; `shown` is what survived the cap.
-- "problems", not "errors": the log carries three kinds now and two of them are not
-- errors (2135). The window is still `/errors` and still opens on the same key —
-- what changed is that a count of 7 stopped meaning "7 things raised".
-- `total` counts distinct failures ever recorded, `shown` what the cap kept. The
-- summary says "problems" because two of the three kinds are not errors.
local summary
if total == 0 then
summary = "No problems this session."
@@ -126,9 +82,7 @@
if seq then
button.seq = seq
button.message = message
-- The count rides the index, not the text: `#12 x1113` says "this is one
-- failure that happened 1,113 times", which is the whole reason the log
-- deduplicates instead of listing 1,113 rows.
-- `#12 x1113`: one deduplicated failure that happened 1,113 times.
local label = "#" .. seq
if count > 1 then label = label .. " x" .. count end
button.index:SetText(label)
@@ -143,9 +97,8 @@
end
end
-- The moving highlight, seated onto the selected row only when that row is on screen —
-- scrolling the selection away hides the glow rather than stranding it (CraftFrame's
-- own behaviour).
-- The highlight follows the selected row and hides when it scrolls away, as the
-- reference's CraftFrame does.
local highlight = getglobal("BenillaScriptLogHighlightFrame")
if selectedRow then
highlight:SetPoint("TOPLEFT", selectedRow:GetName(), "TOPLEFT", 0, 0)
@@ -157,8 +110,8 @@
BenillaScriptLog_UpdateDetail()
end
-- The detail pane: the FULL message of the selected row, traceback and all. Selection is by
-- `seq`, so this walks the log rather than indexing it.
-- The selected message in full, traceback and all; selection is a `seq`, so this walks
-- the log rather than indexing it.
function BenillaScriptLog_UpdateDetail()
local text = ""
if BenillaScriptLogSelected then
@@ -177,18 +130,14 @@
getglobal("BenillaScriptLogDetailText"):SetText(text)
end
-- Frame-late by nature (the shared kit's own note): text rects resolve after Lua runs, so
-- the child is re-measured from OnUpdate until it stops changing, after which this is a
-- no-op compare per frame. OnUpdate only fires on visible frames, so a closed window costs
-- nothing.
-- Text rects resolve after Lua runs, so the detail child is re-measured every frame; once
-- it settles that is one compare, and OnUpdate fires only while the window is shown.
function BenillaScriptLogFrame_OnUpdate()
BenillaScroll_ResizeChild("BenillaScriptLogDetailScroll", { "BenillaScriptLogDetailText" })
end
function BenillaScriptLogRow_OnClick(button)
if not button.seq then return end
-- Clicking the selected row deselects: the detail pane is a drawer, and a list with no
-- way back to "nothing selected" is a small trap.
if BenillaScriptLogSelected == button.seq then
BenillaScriptLogSelected = nil
else
@@ -198,20 +147,17 @@
end
function BenillaScriptLogFrame_OnLoad(self)
-- The ring's icon, through the portrait verb rather than left at its `file=` — see the
-- texture's own note. MailFrame.xml's open-letter icon is the same fix.
-- Masks the square icon to the portrait ring.
SetPortraitToTexture("BenillaScriptLogFramePortrait",
"Interface\\Icons\\INV_Misc_Note_01")
end
-- The shared helper steps the faux frame's bar, whose OnValueChanged runs the kit's
-- offset handler and thence `updateFunc` — so the repaint is wired once, at OnLoad, and
-- not repeated here.
-- Stepping the bar repaints through the list frame's OnVerticalScroll, so no repaint here.
function BenillaScriptLogFrame_OnMouseWheel(delta)
BenillaFauxScrollFrame_OnMouseWheel(getglobal("BenillaScriptLogListScrollFrame"), delta)
end
-- ── The three entry points the client and the player reach this by ────────────────────
-- ── Entry points ──────────────────────────────────────────────────────────────────────
function BenillaScriptLog_Toggle()
local frame = getglobal("BenillaScriptLogFrame")
@@ -222,10 +168,8 @@
end
end
-- `/errors clear`. Forgets the rows, keeps the numbering (the engine's own rule — a number
-- must never name two different failures across a screenshot). Prints a confirmation
-- because a command whose whole effect is "a window you may not have open is now empty"
-- is otherwise indistinguishable from a typo.
-- `/errors clear`: forgets the rows but keeps the numbering, so a number never names two
-- failures. It confirms in chat because the window may be closed.
function BenillaScriptLog_Clear()
BenillaClearScriptErrors()
BenillaScriptLogSelected = nil
@@ -236,9 +180,8 @@
end
]]></Script>
<!-- One list row: the `#N xCount` index column, then the message head. Fixed columns, 1.12's
own list idiom — not a width limitation: a FontString with a zero-authored axis takes its
measured box into the resolve (`benilla-ui` `script/layout.rs`, decision 1664). -->
<!-- One list row: the `#N xCount` index column, then the message head, in fixed columns as
1.12's lists are. -->
<Button name="BenillaScriptLogRowTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Layers>
@@ -268,18 +211,15 @@
<Size><AbsDimension x="384" y="512"/></Size>
<Layers>
<Layer level="BACKGROUND">
<!-- The same square ITEM icon the mail window's ring holds, and it needs the same
portrait verb for the same reason (`SetPortraitToTexture` in OnLoad below):
`file=` alone draws it unmasked and its corners show through the ring's
transparent ones. -->
<!-- A square item icon: OnLoad masks it with `SetPortraitToTexture`, since `file=`
alone shows its corners through the ring. -->
<Texture name="BenillaScriptLogFramePortrait" file="Interface\Icons\INV_Misc_Note_01">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<!-- The four quadrant slabs. The ClassTrainer family, all four, because it is the one
UNIFORM set in the 1.12 art (CraftFrame's own note) and this window belongs to no
profession — borrowing a themed half would make it look like a window it isn't. -->
<!-- The ClassTrainer quadrants, the one uniform set in the 1.12 art; this window
belongs to no profession. -->
<Layer level="BORDER">
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="TOPLEFT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="TOPRIGHT"/></Anchors></Texture>
@@ -288,22 +228,18 @@
<FontString name="BenillaScriptLogTitle" inherits="GameFontNormal" justifyH="CENTER" text="Script Errors">
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<!-- The summary line, and the ONLY place the dropped-row count can be told. -->
<!-- The summary line, the only place the dropped-row count shows. -->
<FontString name="BenillaScriptLogSummary" inherits="GameFontHighlightSmall" justifyH="LEFT" text="">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="26" y="-76"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The top-right X: the chain's own `UIPanelCloseButton` (seated at benilla.toc's
`Interface\FrameXML\UIPanelTemplates.xml`, 757 lines above this file). Its
32x32, its three minimize-button state textures and its
`HideUIPanel(this:GetParent())` are byte-for-byte what stood here authored. -->
<Button name="BenillaScriptLogCloseButton" inherits="UIPanelCloseButton">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-29" y="-8"/></Offset></Anchor></Anchors>
</Button>
<!-- Declared BEFORE the rows so row text paints over the glow (the house z-tiebreak). -->
<!-- Declared before the rows so their text draws over the glow. -->
<Frame name="BenillaScriptLogHighlightFrame" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
@@ -335,16 +271,14 @@
<Size><AbsDimension x="293" y="208"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-94"/></Offset></Anchor></Anchors>
<Scripts>
<!-- The reference's kit writes `frame.offset` ONLY from here; its
`updateFunc` field does not exist, so without this the bar moves and
the list does not follow (1868). -->
<!-- The stock kit sets the offset only from here and has no `updateFunc`, so
the repaint is wired here too. -->
<OnVerticalScroll>FauxScrollFrame_OnVerticalScroll(BENILLA_SCRIPTLOG_ROW_HEIGHT, BenillaScriptLog_Update)</OnVerticalScroll>
</Scripts>
</ScrollFrame>
<!-- The detail pane: the REAL scroll kit, because a message carries its `stack
traceback:` tail and is genuinely variable-height. GossipFrame's greeting pane is
the same wiring. -->
<!-- The detail pane, on the real-scroll kit: a message with its `stack traceback:` tail
has variable height. -->
<ScrollFrame name="BenillaScriptLogDetailScroll">
<Size><AbsDimension x="293" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-314"/></Offset></Anchor></Anchors>
@@ -367,8 +301,7 @@
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<Layers>
<Layer level="ARTWORK">
<!-- 270 wide so the engine wraps the traceback rather than clipping
it; the pane pans over whatever height that comes to. -->
<!-- 270 wide so the traceback wraps rather than clips. -->
<FontString name="BenillaScriptLogDetailText" inherits="GameFontHighlightSmall" justifyH="LEFT" justifyV="TOP" text="">
<Size><AbsDimension x="270" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="6" y="-6"/></Offset></Anchor></Anchors>
@@ -392,18 +325,16 @@
</Frames>
<Scripts>
<OnLoad>BenillaScriptLogFrame_OnLoad(self)</OnLoad>
<!-- The list wheel lives on the WINDOW: the engine bubbles a spin up the parent chain
(CraftFrame's own note). The detail pane has its own, which wins over its own rect. -->
<!-- The list's wheel handler is on the window, since a spin bubbles up the parent
chain; the detail pane's own handler wins over its rect. -->
<OnMouseWheel>BenillaScriptLogFrame_OnMouseWheel(arg1)</OnMouseWheel>
<OnShow>BenillaScriptLog_Update()</OnShow>
<OnUpdate>BenillaScriptLogFrame_OnUpdate()</OnUpdate>
</Scripts>
</Frame>
<!-- benilla's own commands over the reference's SlashCmdList (decision 1948). The stock
ChatEdit_ParseText walks SlashCmdList after its own built-ins, so each of benilla's commands
(ui_chat::commands — the dev instruments and the three additions) hands its line to
SubmitChatInput, where ui_chat::input's parser still owns it. This file loads after the
reference's ChatFrame.xml, which is what declares the table. -->
<!-- Benilla's slash commands in the stock SlashCmdList, which ChatEdit_ParseText walks after
its built-ins; each hands its line to SubmitChatInput, where ui_chat::input parses it.
This file loads after the stock ChatFrame.xml, which declares the table. -->
<Script><![CDATA[
local BENILLA_COMMANDS = {
{ "CHATTEST", "/chattest" },
+74 -207
View File
@@ -1,54 +1,30 @@
<Ui>
<!--
benilla's reusable scroll kits — TWO of them, sharing one scrollbar, one pair of arrow
helpers and one optional TROUGH (`BenillaScrollTroughTemplate`, at the foot: the recessed
channel a bar rides in — 1.12 authors that art per window, so an owner declares one beside
its bar and the kit shows and hides the two together).
The **faux-scroll** kit (this header) re-binds a fixed set of rows to a moving data
offset; the **real-scroll** kit (second half of the script block) pans and CLIPS a
variable-height child through the engine's `<ScrollFrame>` (decision 0112) — the one a gossip
menu of wrapped options needs, since its content height isn't known until it is laid out.
Only their OnValueChanged differs, and an instance overrides the template's (the loader
SetScripts every `<Scripts>` block in order, so the instance's handler wins).
The faux kit (decisions 0247/0250/0251): a draggable scroll bar over
the `Slider` primitive (0250) plus the fixed-row "faux scroll" Lua layer — the reference's
`FauxScrollFrame`, which never really scrolls a child: it re-binds a fixed set of row buttons to
a moving data offset. First customer: the class trainer's skill-line list (TrainerFrame.xml).
The quest log's chunk-local faux-scroll upvalue is a later migration onto this.
DESIGN — collapsed from the real client's three-part decomposition (0250: FauxScrollFrame =
ScrollFrame + Slider + Lua). The real `FauxScrollFrameTemplate` routes the scrollbar's value
through its parent `ScrollFrame` (`OnValueChanged → parent:SetVerticalScroll(v)`; the
ScrollFrame's `OnVerticalScroll` then derives `offset = floor(v/itemHeight + 0.5)`). That
ScrollFrame adds nothing to the value on its way through: 0251 believed it clamped to the
scroll range and called the clamp a liability, and decision 2017 read the bytes — the
reference's `SetVerticalScroll` stores what the bar hands it. So the relay is a pure
pass-through, and benilla's faux frame is a plain `<Frame>` + a `Slider`: fewer moving parts
and the identical on-screen result (a smooth, pixel-granularity thumb whose position maps
1:1 to the scroll offset). See 0251 for the full rationale + the divergence from 0250's
described (real-client) shape.
VALUE MODEL. The scrollbar's value is in PIXELS: `[0, (numItems − numToDisplay) * itemHeight]`,
`valueStep = itemHeight`. The row offset is `floor(value / itemHeight + 0.5)` — sub-pixel
rounding, so the thumb (drawn at the raw value fraction) and the list (drawn at the rounded
offset) never visibly disagree. The frame carries `frame.offset` (the top visible row, 0-based)
and `frame.updateFunc` (the owner's list-repaint, set by the owner's OnLoad).
Benilla's scroll kits, sharing one `Slider` bar, its arrow helpers and an optional trough:
a faux-scroll kit that re-binds a fixed set of rows to a moving data offset, and a
real-scroll kit that pans and clips a `<ScrollFrame>` child whose height is known only after
layout. A real-scroll bar overrides the template's OnValueChanged on its instance, which
wins because the loader runs every `<Scripts>` block in order.
Faux model: the value is pixels, `[0, (numItems - numToDisplay) * itemHeight]` in steps of
`itemHeight`; the parent holds the 0-based top row in `frame.offset` and its repaint in
`frame.updateFunc`, set by the owner's OnLoad.
Deviation: the faux parent is a plain `<Frame>`, not the reference's ScrollFrame relay,
which adds nothing because the reference's `SetVerticalScroll` stores the bar's value as is.
`FauxScrollFrameTemplate` and the `FauxScrollFrame_*`, `ScrollFrame_*` and
`ScrollingEdit_*` API are the stock `UIPanelTemplates` on the player's chain, which loads
after this file, so its declarations win.
-->
<Script><![CDATA[
-- Step the bar one row (the slider's valueStep) — the sensible step for an item list (the
-- generic ref scrollbar steps half its height; a discrete row list wants one). Silent by
-- design: the ref plays UChatScrollButton only on the arrow *buttons'* OnClick, never on the
-- wheel (ScrollFrameTemplate_OnMouseWheel is soundless). Both the wheel and the arrows call
-- this, so the click lives on the arrow OnClick, keeping wheel-scroll silent.
-- Steps the bar by its valueStep. Deviation: one row on a faux list, where the reference
-- bar's arrows step half its height. Soundless: the reference plays UChatScrollButton on
-- an arrow click, never on the wheel, so the arrows' OnClick plays it.
function BenillaScrollBar_Step(scrollBar, dir)
local step = scrollBar:GetValueStep()
if step <= 0 then step = 1 end
scrollBar:SetValue(scrollBar:GetValue() + dir * step)
end
-- Grey the up arrow at the top and the down arrow at the bottom (ref
-- ScrollFrame_OnScrollRangeChanged's arrow handling), from the slider's own value/range.
-- Greys each arrow at its end of the range (reference: ScrollFrame_OnScrollRangeChanged).
function BenillaScrollBar_UpdateArrows(scrollBar)
local minV, maxV = scrollBar:GetMinMaxValues()
local val = scrollBar:GetValue()
@@ -58,29 +34,11 @@
if val >= maxV then down:Disable() else down:Enable() end
end
-- The scrollbar's OnValueChanged: derive the row offset from the (pixel) value and, only when
-- it actually moved, repaint the owner's list. The change-gate is load-bearing: the owner's
-- repaint calls _Update below, which SetValue()s the thumb back to offset*itemHeight — a value
-- that maps to the SAME offset, so this handler no-ops and the cycle stops after one hop (the
-- Slider's own SetValue change-gate stops the value side, decision 0250).
--
-- THE SECOND HALF IS FOR ADDONS, and it is the reference's own wiring. The ref's
-- `UIPanelScrollBarTemplate` routes its value through the ScrollFrame it rides
-- (`<OnValueChanged>this:GetParent():SetVerticalScroll(arg1)</OnValueChanged>`,
-- ref-UIPanelTemplates.xml l.156-158), and that frame's `<OnVerticalScroll>` is where an
-- addon puts `FauxScrollFrame_OnVerticalScroll(itemHeight, MyUpdate)`. Our OWN faux frames
-- are plain `<Frame>`s (the 0251 divergence in the header above), so they have no
-- SetVerticalScroll and this line never fires for them — but an addon's is declared
-- `<ScrollFrame ... inherits="FauxScrollFrameTemplate">`, and the instance's own tag wins
-- over the template's on inherit (framexml.rs `merge`), so it really is one. Duck-typed for
-- exactly that reason.
--
-- The engine's SetVerticalScroll takes the bar's value as given (decision 2017 — the
-- reference's never reads the scroll range), so an addon frame's `<OnVerticalScroll>`
-- fires with this very value and its `FauxScrollFrame_OnVerticalScroll` derives the offset
-- from it, the reference's path end to end. The offset our own kit computes above is the
-- same number by construction — it comes straight off the Slider, which is the whole point
-- of 0251.
-- The faux bar's OnValueChanged: the top row is the value rounded to the nearest row, so
-- thumb and rows agree, and `updateFunc` runs only when that row changes, which also ends
-- the loop when a repaint re-seats the thumb.
-- A ScrollFrame parent also gets the value through SetVerticalScroll, as the reference's
-- `UIPanelScrollBarTemplate` routes it (UIPanelTemplates.xml l.156-158).
function BenillaFauxScrollBar_OnValueChanged(scrollBar, value)
local frame = scrollBar:GetParent()
local step = scrollBar:GetValueStep()
@@ -94,27 +52,10 @@
if frame.SetVerticalScroll then frame:SetVerticalScroll(value) end
end
-- Seat a trough ON ITS BAR. THE SOCKETS, and why this is not a free choice: the channel art
-- is not a plain tube — each end carries a 16-tall SOCKET that the arrow button drops into,
-- bounded by the frame's outer cap and an inner crossbar. Measured texel-exact off
-- UI-Character-ScrollBar (64x256):
-- top piece (left column) : rows 0-3 CAP · rows 4-19 SOCKET · rows 20-21 crossbar
-- bottom piece (right column): rows 85-86 crossbar · rows 87-102 SOCKET · rows 104-106 CAP
-- Both pieces draw 1:1 anchored to their own end of the trough, and the arrow buttons sit 16
-- outside the slider's ends — so a socket lands on its arrow only when the trough OVERHANGS
-- the bar by 21 above and 20 below (4 rows of cap at the top against 3+1 at the bottom; that
-- 1 is the whole asymmetry). Those are the ref's own numbers, reached from the other side:
-- ReputationFrame hangs its trough at the scroll frame's TOPRIGHT (-2,+5) / BOTTOMRIGHT
-- (-2,-4) against a bar seated (6,-16)/(6,+16) — 21, 20, and the 8-left hang
-- (ref-ReputationFrame.xml l.573-599 against ref-UIPanelTemplates.xml l.166-181). Miss it by
-- even 4 and the arrows ride OUT of their sockets onto the caps with a sliver of bare socket
-- showing beside them, which is exactly what B224 reported.
--
-- Seated HERE, off the bar, rather than restated in each owner's XML: it is one law, and an
-- owner made to re-derive it in offsets is an owner that can get it wrong — which is how
-- B224 happened. So a trough carries NO anchors of its own; declare it before the bar (so it
-- draws beneath) and name it `<barname>Trough`, and that is the whole contract. Lazy,
-- because the bar is declared after the trough and does not exist at the trough's OnLoad.
-- Seats a trough on its bar with the reference's ReputationFrame hang, 8 left, 21 above and
-- 20 below (ReputationFrame.xml l.573-599 against UIPanelTemplates.xml l.166-181), which
-- puts each art socket on its arrow button. Lazy, because the bar is declared after its
-- trough and does not exist at the trough's OnLoad.
function BenillaScrollTrough_Seat(trough, scrollBar)
if trough.seated then return end
trough.seated = true
@@ -123,19 +64,9 @@
trough:SetPoint("BOTTOMRIGHT", scrollBar, "BOTTOMRIGHT", 7, -20)
end
-- Show/hide a bar TOGETHER WITH ITS TROUGH — the recessed channel it rides in, if the
-- owner authored one beside it (a `<barname>Trough` frame on BenillaScrollTroughTemplate
-- below). Every path that shows or hides a bar goes through here, so a window that fits
-- shows neither and one that overflows shows both — and so a trough is seated on its bar
-- before it can ever be visible, whichever way the first call goes.
--
-- Why the trough is OPT-IN rather than part of the bar template: 1.12 authors trough art
-- PER WINDOW, never in the shared scroll kit — `UIPanelScrollBarTemplate` carries only the
-- two arrow buttons and the knob, and the ref's FrameXML then hangs loose trough textures
-- beside 29 individual bars, in two different skins (UI-Character-ScrollBar for the
-- dialog/paperdoll family, UI-ClassTrainer-ScrollBar for the trainer family). The era's own
-- MinimalScrollBar went the other way and made its Track part of the bar; this pair keeps
-- the era's COUPLING (track lives and dies with the bar) on 1.12's per-window SKIN.
-- Shows or hides a bar and its `<barname>Trough` together, seating the trough first; show
-- and hide a bar only through here. The trough is opt-in because 1.12 draws trough art per
-- window: `UIPanelScrollBarTemplate` carries only the arrows and the knob.
function BenillaScrollBar_SetShown(scrollBar, shown)
local trough = getglobal(scrollBar:GetName() .. "Trough")
if trough then BenillaScrollTrough_Seat(trough, scrollBar) end
@@ -148,17 +79,9 @@
end
end
-- **Our trough, following the REFERENCE's bar.** `BenillaScrollBar_SetShown` above is the
-- Benilla kit's own pairing and nothing in the reference's faux kit calls it — the
-- reference has no trough at all, which is exactly why this template stays ours (1837's
-- manifest note). Since the faux frames took the chain's `FauxScrollFrameTemplate` their
-- bar is `UIPanelScrollBarTemplate`, so an owner that wants its trough asks for it here,
-- right after its `FauxScrollFrame_Update`. Seating is idempotent and the visibility
-- simply mirrors the bar's, which the reference's `_Update` has already decided by
-- showing or hiding the scroll FRAME.
--
-- Decision 1860. Named after the bar rather than the frame because the trough is declared
-- `<barname>Trough` — one law, stated once, exactly as BenillaScrollTrough_Seat wants it.
-- The same for a stock `FauxScrollFrameTemplate` frame, whose bar is the reference's: seats
-- `<frame>ScrollBarTrough` and mirrors the frame's shown state. Call it after
-- `FauxScrollFrame_Update`, which shows or hides the frame.
function BenillaScrollTrough_Follow(scrollFrameName)
local sf = getglobal(scrollFrameName)
if not sf then return end
@@ -170,45 +93,30 @@
if sf:IsShown() then trough:Show() else trough:Hide() end
end
-- A wheel spin over the list → one row (WoW convention: +1 up, −1 down). Called from the
-- owner window's OnMouseWheel (the engine bubbles the spin up the parent chain, so the handler
-- lives on the window, not a child — TrainerFrame.xml's own note).
--
-- KEEPS the `Benilla` prefix on purpose: the reference has no faux-scroll wheel function to
-- take the name of. Its `ScrollFrameTemplate_OnMouseWheel` is the REAL-scroll one and moves
-- by half the bar's height; its faux owners each write a wheel body of their own inline.
-- One row per wheel notch (+1 up) on a faux list, called from the owner window's
-- OnMouseWheel since the spin bubbles up the parent chain. Benilla-named: the reference has
-- no faux wheel function (its `ScrollFrameTemplate_OnMouseWheel` moves half the bar).
function BenillaFauxScrollFrame_OnMouseWheel(frame, delta)
local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
BenillaScrollBar_Step(scrollBar, -delta)
end
-- ══════════════════════════ The REAL-scroll kit ══════════════════════════════════════════
--
-- The other half of the file: a true `<ScrollFrame>` (engine mechanism, decision 0112) that
-- pans and CLIPS a variable-height child, as against the faux kit above (fixed rows re-bound
-- to a data offset). Windows whose content height is unknown until it is laid out — a gossip
-- menu of wrapped options, a quest's body text — need this one; a uniform list wants the faux.
--
-- SHAPE, from the reference `UIPanelScrollFrameTemplate` (Interface\FrameXML\
-- UIPanelTemplates.xml l.166-211, extracted via mpqx): the scrollbar is a SLIDER that rides
-- its ScrollFrame as a child, so `bar:GetParent()` IS the frame it scrolls — no name
-- derivation, and `$parentBar` gives SyncBar its handle. The ref's OnVerticalScroll drives
-- the bar's value and greys the two arrows at the ends; ours does the same through the
-- shared arrow helper above.
BENILLA_SCROLL_STEP = 20 -- px per arrow click / wheel notch
BENILLA_SCROLL_PAD = 20 -- breathing room under the last element in the child
-- ══════════════════════════ The real-scroll kit ══════════════════════════════════════════
-- Shaped on the reference `UIPanelScrollFrameTemplate` (UIPanelTemplates.xml l.166-211):
-- the bar is a `Slider` child, `$parentBar`, so `bar:GetParent()` is the frame it pans.
BENILLA_SCROLL_STEP = 20 -- px per arrow click or wheel notch
BENILLA_SCROLL_PAD = 20 -- px of room below the child's last element
-- The bar moved (drag, arrow, or SyncBar): pan the frame it rides. The loop this could form
-- (bar → scroll → OnVerticalScroll → SyncBar → bar) dead-ends in one hop — SyncBar writes the
-- value the bar already holds, and the Slider's SetValue change-gate (decision 0250) drops it.
-- Pans the frame the bar rides. The loop back through OnVerticalScroll and SyncBar ends in
-- one hop: SyncBar writes the value the bar already holds, which Slider:SetValue ignores.
function BenillaScrollBar_OnValueChanged(bar, value)
bar:GetParent():SetVerticalScroll(value)
BenillaScrollBar_UpdateArrows(bar)
end
-- Reconcile a ScrollFrame's bar to its live range (ref ScrollFrame_OnScrollRangeChanged):
-- the bar spans [0, range] in pixels, seats at the current scroll, and hides entirely when
-- the content fits. Wired to both OnScrollRangeChanged and OnVerticalScroll by the owner.
-- Fits the bar to the frame's live range, [0, range] px, and hides it when the content fits
-- (reference: ScrollFrame_OnScrollRangeChanged). Owners call it from OnScrollRangeChanged
-- and OnVerticalScroll.
function BenillaScroll_SyncBar(sf)
local bar = getglobal(sf:GetName() .. "Bar")
if not bar then return end
@@ -220,10 +128,8 @@
BenillaScrollBar_SetShown(bar, range > 0)
end
-- Pan by `delta` px, clamped to [0, range] HERE: the engine's SetVerticalScroll stores
-- what it is given (decision 2017 — the reference's never reads the range), and the
-- reference's own wheel handler bounds itself the same way, through its bar's [min, max]
-- (ScrollFrameTemplate_OnMouseWheel → scrollBar:SetValue).
-- Pans by `delta` px, clamped here: SetVerticalScroll stores any value, as the reference's
-- does; the reference's ScrollFrameTemplate_OnMouseWheel clamps through its bar's range.
function BenillaScroll_Step(sf, delta)
local target = sf:GetVerticalScroll() + delta
local range = sf:GetVerticalScrollRange()
@@ -233,13 +139,12 @@
BenillaScroll_SyncBar(sf)
end
-- WoW wheel convention: +1 per notch up, -1 down — up lifts the content.
-- The wheel's delta is +1 per notch up, and up scrolls toward the top.
function BenillaScroll_OnMouseWheel(sf, delta)
BenillaScroll_Step(sf, -delta * BENILLA_SCROLL_STEP)
end
-- A widget's resolved bottom edge for the content sum — nil when it isn't contributing
-- (hidden frame / blank FontString / not yet resolved this frame).
-- A widget's resolved bottom, or nil if hidden, a blank FontString, or not laid out yet.
function BenillaScroll_WidgetBottom(w)
if not w then return nil end
if w.IsShown then
@@ -251,19 +156,13 @@
return w:GetBottom()
end
-- Size a scroll child to its MEASURED content — the resolved bottom of the lowest visible
-- widget, plus padding — instead of a fixed budget, and tell the frame (which fires
-- OnScrollRangeChanged → the owner's SyncBar). Frame-late by nature: rects resolve after Lua
-- runs, so owners re-run this from an OnUpdate until it stops changing. Returns true when it
-- actually resized (only a real change writes: SetHeight invalidates layout, and an
-- every-frame write would fight the resolver).
-- Sizes the scroll child to its lowest visible widget plus BENILLA_SCROLL_PAD and updates
-- the frame's range. Rects resolve after Lua runs, so owners re-run it from OnUpdate until
-- it returns false; it writes only on a real change, since SetHeight invalidates layout.
function BenillaScroll_ResizeChild(scrollName, widgets)
local sf = getglobal(scrollName)
-- ASK THE FRAME, don't guess a name. This used to be
-- `getglobal(scrollName .. "Child")`, which only ever worked because every caller wore
-- a private name in that exact shape — the reference's own convention is
-- `<X>ScrollFrame` / `<X>ScrollChildFrame`, not a suffix, so the derivation broke the
-- moment QuestFrame took the reference's names. A ScrollFrame knows its own child.
-- The frame's own child, not a derived name: the reference pairs `<X>ScrollFrame` with
-- `<X>ScrollChildFrame`.
local child = sf:GetScrollChild()
if not child then return false end
local top = child:GetTop()
@@ -284,11 +183,9 @@
]]></Script>
<!--
The scroll bar: a `Slider` (0250) 16px wide, its up/down step buttons pinned above/below the
track, and the knob thumb. The value is in pixels (see the header); the thumb draws at the
value fraction along the track (engine-side, decision 0250 §4), draggable (§5). The step-button
+ knob art is the stock 1.12 scrollbar kit, each texture cropped to its centred 16px (the
`0.25..0.75` TexCoords the ref's `UIPanelScrollBarButton` applies).
The bar: a 16px `Slider` with step buttons above and below the track, on the stock 1.12
scrollbar art cropped to its centred 16px (the `0.25..0.75` TexCoords of the reference's
`UIPanelScrollBarButton`).
-->
<Slider name="BenillaScrollBarTemplate" virtual="true">
<Size><AbsDimension x="16" y="0"/></Size>
@@ -322,39 +219,24 @@
</Slider>
<!--
The scroll bar's TROUGH — the recessed channel the bar rides in: the "background and
border" a bare arrows+knob bar is missing. An owner declares one of these BESIDE its bar,
named `<barname>Trough` and WITH NO ANCHORS — it seats itself on the bar the first time
BenillaScrollBar_SetShown touches it (BenillaScrollTrough_Seat above, where the socket law
and the ref citations live). Declare it BEFORE the bar so it draws beneath.
THE ART, and why it is sliced this way. `Interface\PaperDollInfoFrame\UI-Character-ScrollBar`
is a 64x256 sheet holding TWO 31-wide channel pieces, measured texel-exact from the file —
rails at x 2-5 and 25-30 with a 19-wide interior between them, and each piece bearing a
16-tall SOCKET for its arrow button:
· LEFT column (u 0 .. 31/64): rows 0-3 the solid TOP cap, 4-19 the up-arrow socket,
20-21 the crossbar that closes it, then plain channel from row 22 to the bottom edge.
· RIGHT column (u 33/64 .. 1): plain channel, then rows 85-86 the crossbar, 87-102 the
down-arrow socket, 104-106 the solid BOTTOM cap (nothing below 107).
The ref composes a trough from exactly two quads — the whole left column plus the right
column's bottom 108 rows — which reaches 362 units and no further, and only works because
its scroll frames are short enough for the two to OVERLAP. Ours must span a 563-tall page,
so the same art is cut as a THREE-SLICE instead: the two socket ends at their natural
heights and a plain-channel slice stretched between them. The middle is constant down its
whole length, so stretching it is exact — there is no seam to find.
The black `Fill` is the ref's own trough backing, not an invention: ItemTextFrame draws a
0.8-alpha black quad behind its channel art for exactly this (ref-ItemTextFrame.xml's
`Middle`). It sits in BACKGROUND and the channel art in BORDER — two layers, because within
ONE layer the client re-sorts overlapping quads by texture handle and their order is not
ours to depend on (order.rs). The bar's own furniture stays clear above both: button state
textures are ARTWORK, the knob is OVERLAY.
A bar's trough, the recessed channel it rides in; the reference's scroll kit has none.
Declare it before its bar, so it draws beneath, named `<barname>Trough` and with no
anchors: BenillaScrollTrough_Seat seats it on the bar.
`UI-Character-ScrollBar` (64x256) holds two 31-wide channel pieces, the left column
(u 0 .. 31/64) capped at the top and the right (u 33/64 .. 1) at the bottom, each with a
16-tall socket for its arrow button: rows 4-19 at the top, rows 87-102 at the bottom.
Deviation: cut as a three-slice (both socket ends at natural height, the plain channel
stretched between) where the reference overlaps two quads, since two quads reach only 362
units and a page here is 563 tall.
The black fill is the reference's trough backing (ItemTextFrame's 0.8-alpha `Middle`). It
is in BACKGROUND and the art in BORDER because the client orders overlapping quads within
one layer by texture handle; the bar's buttons (ARTWORK) and knob (OVERLAY) draw above both.
-->
<Frame name="BenillaScrollTroughTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="31"/></Size>
<Layers>
<Layer level="BACKGROUND">
<!-- The channel interior: x 6..24 of the 31-wide piece, i.e. inset 6 from each edge. -->
<!-- The channel interior: x 6..24 of the 31-wide piece, inset 6 from each edge. -->
<Texture name="$parentFill">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor>
@@ -374,9 +256,7 @@
<TexCoords left="0" right="0.484375" top="0" bottom="0.08984375"/>
</Texture>
<!-- Crossbar + down-arrow socket + cap: right column, rows 82..107
(v 82/256 .. 108/256) — the mirror of the top's 23, cut to put row 107 on the
trough's bottom edge exactly as ReputationFrame's own 108/0.421875 does, which
is what lands the socket on the arrow (BenillaScrollTrough_Seat's law). -->
(v 82/256 .. 108/256), ending on v 0.421875 as ReputationFrame's does. -->
<Texture name="$parentBottom" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size><AbsDimension y="26"/></Size>
<Anchors>
@@ -386,8 +266,7 @@
<TexCoords left="0.515625" right="1.0" top="0.3203125" bottom="0.421875"/>
</Texture>
<!-- The stretched run between them: left column rows 30..70, plain rails only.
Declared LAST because it anchors to the two above by name, and an anchor whose
target is declared later falls back to the parent (ItemTextFrame's own note). -->
Declared last: an anchor to a later sibling falls back to the parent. -->
<Texture name="$parentMiddle" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTop" relativePoint="BOTTOMLEFT"/>
@@ -399,22 +278,10 @@
</Layers>
</Frame>
<!-- ══ Carried here from UiPanels.xml when that file went stock (decision 1988) ══
The placeholder icon our UIParent.lua counterpart held and the reference's does not,
seated in the earliest file of ours every consumer loads after. The window tab template
and its settle stood here too until 1993 put them on the chain. -->
<Script><![CDATA[
-- The placeholder icon for an item/spell whose texture has not arrived yet (Craft, Quest,
-- QuestLog, Trainer and TradeSkill read it). Was ActionBar.xml's (stock since 1938), then
-- UiPanels.xml's (stock since 1988); this file is the earliest of ours every reader loads
-- after. A benilla name, so nothing stock reads it.
-- Placeholder icon for an item or spell whose texture has not arrived; a benilla global
-- that the reference does not declare, so no stock file reads it.
BENILLA_FALLBACK_ICON = "Interface\\Icons\\INV_Misc_QuestionMark"
]]></Script>
<!-- `FauxScrollFrameTemplate` and the `FauxScrollFrame_*`/`ScrollFrame_*`/`ScrollingEdit_*`
API are the REFERENCE's, off the player's chain (`Interface\FrameXML\UIPanelTemplates`,
seated below this file — so its declarations win). Our transcriptions stood here dead
from the day that entry landed and are gone (1860). What stays is
`BenillaScrollBarTemplate` + `BenillaScrollTroughTemplate` and their helpers, which are
ours: six windows inherit the bar directly and the reference has no trough at all. -->
</Ui>
@@ -1,27 +1,13 @@
<Ui>
<!--
BENILLA_ALLOW_OWN_UI=1 — the director's two LOOK deviations on the pet page's autocast
button, over the reference's own SpellBookFrame.xml (decisions 1392/1393; re-seated by 2014
onto the reference's own Model now that this engine renders it — 2013).
Since 2013 the `$parentAutoCast` <Model> on every SpellButton draws its own file
(Interface\Buttons\UI-AutoCastButton.mdx) as a tile: four bones chasing the corners of a
0.02-unit square, four spline emitters trailing GlowStar sparks off them, on the pane's own
clock. The reference declares it 36x36 at CENTER (1,1) at scale 1.22 — a 0.87x rim that
floats clear of the viewport edge, never clipped, washing the icon; the real 1.12 client
looks the same way (director-checked, 1392), so this is taste, not fidelity, and the
director chose the pet button's proportions instead:
· the shine pane squared on the button — 37x37 at CENTER (0,0), scale 1.48 — so the rim
runs ON the viewport edge (1.024x, the pet button's own ratio) and the tile's cell, the
widget's own scissor, halves every star; and so the glow and the brackets share a centre
(1393's "top and right are off" was the reference's own +1,+1);
· the corner brackets at 71.53 rather than the reference's 60 — the art fills 33/64 of
UI-AutoCastableOverlay.blp, and 71.53 = 37 x 58/30 puts the bracket square on this
button's corners at the pet button's 0.997x.
Reverting the widget to 1.12 exactly is: overlay 60, pane 36x36 CENTER (1,1), scale 1.22 —
i.e. deleting this file. Nothing else here: the reference's file is sourced unedited.
BENILLA_ALLOW_OWN_UI=1. Deviation: over the unedited stock SpellBookFrame.xml, the pet
page's autocast widgets take the pet bar button's proportions.
The `$parentAutoCast` shine (UI-AutoCastButton.mdx) is squared on the button, 37x37 at
CENTER (0,0) and scale 1.48, so its rim runs on the viewport edge at the pet button's
1.024x and shares the brackets' centre; the reference's is 36x36 at CENTER (1,1), 1.22.
The corner brackets are 71.53 rather than 60: the art fills 33/64 of
UI-AutoCastableOverlay.blp, and 71.53 = 37 x 58/30 puts them on the button's corners at
the pet button's 0.997x. Deleting this file restores the reference's look.
-->
<Script><![CDATA[
for i = 1, 12 do