Files
benilla-octo/crates/benilla/assets/ui/OptionsFrame.xml
T
SamandClaude Fable 5 a03cb5d5d2 Chain Lightning draws its chain, and the Controls page lands
- the beam is on screen: Chain Lightning arcs outward hop by hop and Drain Life ropes from hand to victim — a write-order edge was erasing the finished geometry every frame, and a tripwire now guards the whole class
- the Controls page lands (sticky targeting, auto loot, invert mouse), UI scale waits for the era Apply button, and the terrain-distance slider retires
- with auto-loot in effect a lootable corpse shows the triple-pouch cursor, the real client's tell
- the character-screen freeze before a loading screen is gone: the shader warm-up hides behind a fully presented cover and tears down only its own roots

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 18:54:58 +07:00

1278 lines
82 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Ui>
<!--
The OPTIONS window (decision 0950) — the shell (slice A) and the real PAGES: Audio
(0957), Graphics (0959), Controls (0961). Opened from the game menu's Options button.
Setting rows ride the CVar store (0954): rows read GetCVar on show, write SetCVar on
interaction, and Defaults resets the visible page's CVars to their registered defaults.
Pages without rows are still empty and keep Defaults disabled. One row defers — uiScale
stages a pending value and commits through the era Apply button (0961), because a live
write would rescale the very slider being dragged.
This window is the one deliberate era-1.15.9 skin in a 1.12 shell (0950): the 1.12
OptionsFrame family (four separate panels) collapses into the era SettingsPanel look —
one window, a category list, one page area. Ground truth is the era dump
(wow-ui-source-era @ 1.15.9.68940), transcribed via the session's reference spec:
· **Root** — Blizzard_SettingsPanel.xml `SettingsPanel`: 920x724, CENTER, strata HIGH,
hidden. Skin per `SettingsFrameTemplate` (Classic/Blizzard_SettingsPanelTemplates.xml):
FlatPanelBackgroundTemplate at TOPLEFT (7,-18) → BOTTOMRIGHT (-3,3), the
`ButtonFrameTemplateNoPortrait` nine-slice (Classic/NineSliceLayouts.lua l.132 — right
edge x=-8/x1=8), the `_UI-Frame-TitleTileBg` title band (256x17, TexCoords
.2890625/.421875, TOPLEFT (8,-3) → TOPRIGHT (0,-3)), title GameFontNormal TOP (0,-5).
· **Category list** — Blizzard_CategoryList.xml + .lua: rows/headers STRETCH to the
scroll target's width (199, no scroll bar); header 30 tall (label 14px white at
LEFT (20,-1), Options_CategoryHeader_<n> wash at TOPLEFT at atlas size, n cycling
per header); row 20 tall (label GameFontNormal TOPLEFT (36,+1); bg CENTER —
options_list_active selected / options_list_hover hover, at atlas size); 2 between
elements, 18-tall spacer between groups (so 22 row-to-header).
· **Page area** — Blizzard_SettingsList.xml: title 20px white TOPLEFT (7,-22), the
options_horizontaldivider at TOP (0,-50), Defaults 96x22 at TOPRIGHT (-36,-16).
· **Tabs** — SharedXML Shared/Tabs/MinimalTab.{xml,lua}: a bottom-anchored three-slice
of Options_Tab[_Active]_{Left,Middle,Right}, GameTab TOPLEFT (32,-27) h37, AddOnsTab
5 right of it; width text+40; selected = white small text at BOTTOM+6, unselected =
gold small at BOTTOM+4.
· **Search box** — SharedXML SearchBoxTemplate → InputBoxVisualTemplate
(SecureUIPanelTemplates.xml l.42-67): the common-search-border three-slice (8x20 caps,
left hung 5 outside, middle stretched, all centered on the 22 box), 10x10 magnifier at
LEFT (1,-1), 17x17 clear button at RIGHT (-3), TextInsets 16/20, instructions label
GameFontDisableSmall tinted 0.35 at TOPLEFT (16,0) → BOTTOMRIGHT (-20,0).
Atlas members resolve by name through WoW-era/_extracted_ui/manifest.json (the 0950
seam); era-only FILE textures ride the same extraction as `era:textures/<fdid>.blp`.
SCOPE / divergences (noted, not bugs):
- **horizTile/vertTile are unsupported**, so every repeating tile is STRETCHED for now:
the `_UI-Frame-TitleTile` top band (a 256x28 repeat), the title-bg band under it, the
`!UI-Frame-LeftTile`/`!UI-Frame-RightTile` columns and the `_UI-Frame-Bot` strip. At
this window's spans the stretch reads as a soft smear of the tile — the gap to close
when tiling lands.
- **FlatPanelBackgroundTemplate is ONE solid fill here.** The era template is four
textures (two rounded-corner atlas pieces + two fills) all tinted the engine-side
global PANEL_BACKGROUND_COLOR = (0.122, 0.118, 0.129, 0.8) — the constant is defined in
the client binary, not the Lua dump; value read off the GlobalColor db2 mirror
(warcraft.wiki.gg/wiki/ColorMixin). One flat quad loses only the 16px rounded bottom
corners. The "lighter inner panel" on top of it is not a second fill — it is the real
`Options_InnerFrame` atlas member, placed below.
- **Close X wears the 1.12 UI-Panel-MinimizeButton art at the era seat** — which IS the
era's own close button: 1.15's UIPanelCloseButtonNoScripts names the same four
Interface\Buttons\UI-Panel-MinimizeButton-* files (Classic SharedUIPanelTemplates.xml
l.172-180), TOPRIGHT (+4,+4), 32x32. No divergence here at all.
- **Tabs are STATIC MinimalTab plates** (slice B): Game wears the selected state
permanently, AddOns the unselected gold — the era swaps these in code
(MinimalTabMixin), and with nothing behind AddOns the selection can never change, so
the two states are authored rather than driven. The mixin port comes with the addon
list. Width is era's text+40 law via an OnUpdate measure poll; the era's
measure-at-the-medium-font quirk is deliberately dropped (~6 units of padding).
- **Search filters nothing yet** — the box takes text, shows/hides its "Search"
placeholder and clear-X; wiring it to the category/row filter comes with the pages
slice.
- **No scroll bars**: the static category tree (9 rows, 2 headers) fits its 199px column
outright, and the deepest page (Audio, 7 rows) fits its area. MinimalScrollBar arrives
when a page outgrows the space.
- **Setting-page cuts (0957 Audio, 0959 Graphics)**: setting-row tooltips (the era
OPTION_TOOLTIP_* hovers) are not wired; Defaults resets the VISIBLE page only (the
era confirm dialog is not modeled); track clicks do nothing (thumb drag + steppers
only — the engine slider's scrollbar-faithful posture); era rows with no 1.12 CVar +
benilla knob behind them (output device, Dialog volume, loop music, channels, cache;
the whole era Graphics dropdown zoo) don't ship. A slider shows the 1.12 panel's
range even where the benilla knob's clamp is wider (uiScale's env clamp is 0.5..1.5):
a config value outside the page range displays clamped — thumb and readout agree on
the nearest expressible value, the engine keeps running the config's truth, and
nothing writes back until the player actually moves the row.
- **Apply-flow cuts (0961)**: the era's close-with-pending 3-button confirm dialog
(GAME_SETTINGS_CONFIRM_DISCARD) is not modeled — hiding the window silently discards
pending edits (the era's own hide path wipes without committing too); the 8-second
"keep these settings?" revert timer (CommitFlag.Revertable) is not modeled either —
Apply commits outright.
- Era rows this slice does not ship: the Accessibility group, and Languages/Network under
System — pages with no benilla knobs behind them yet (0950's "only rows backed by real
knobs" rule). Nameplates joined in slice B (real nameplate knobs exist to back its
page).
- **The window rides a scale knob** (slice B): fonts ride frame scale engine-wide now
(the 0219 §2 divergence closed), so ERA_WINDOW_SCALE × the era checkFit shrink
(UIPanelUpdateScaleForFit, transcribed in this file) sizes the whole window; 0.78 is
the director's-eye starting point, live-tunable. The knob is SHARED: it stands in for
the era client's UIParent scale (smaller than our 1.12 768-canvas on modern screens),
so every era-shaped window wears it — GameMenuFrame.xml reads it in its OnShow.
-->
<!-- The two era fonts this window needs beyond Fonts.xml's 1.12 set (the era
GameFontHighlightMedium / GameFontHighlightHuge — Blizzard_Fonts_Shared: FRIZQT 14 and
20, white). Named Options* rather than the era names on purpose: Fonts.xml is the
COMPLETE 1.12 set and stays that way; these are this window's own, defined with
MasterFont's exact shadow inlined (its only content — Fonts.xml l.62). -->
<Font name="OptionsFontHighlightMedium" font="Fonts\FRIZQT__.TTF" virtual="true">
<Shadow>
<Offset><AbsDimension x="1" y="-1"/></Offset>
<Color r="0" g="0" b="0"/>
</Shadow>
<FontHeight><AbsValue val="14"/></FontHeight>
<Color r="1.0" g="1.0" b="1.0"/>
</Font>
<Font name="OptionsFontHighlightHuge" font="Fonts\FRIZQT__.TTF" virtual="true">
<Shadow>
<Offset><AbsDimension x="1" y="-1"/></Offset>
<Color r="0" g="0" b="0"/>
</Shadow>
<FontHeight><AbsValue val="20"/></FontHeight>
<Color r="1.0" g="1.0" b="1.0"/>
</Font>
<Script><![CDATA[
-- The static category tree (era Game tab, Classic tocs), in list order. Keys are the row
-- name suffixes; the visible label doubles as the page title (the era does the same —
-- Settings.RegisterCategory names both from one string).
OPTIONS_CATEGORY_KEYS = {
"Controls", "Interface", "ActionBars", "Combat", "Social", "Keybindings", "Nameplates",
"Graphics", "Audio",
}
-- The era-window scale (shared, director-owned — the 0219 WORLDMAP_WINDOWED_SCALE
-- pattern): the era client's UIParent draws smaller against the screen than our 1.12
-- 768-canvas does on modern displays, and this knob is that mapping — every era-shaped
-- window wears it (this window here; GameMenuFrame in its OnShow — in the era both draw
-- at the SAME UIParent px-per-unit, the parity the director's eye checked). At 1.0 the
-- 724-tall window fills ~85% of the screen's height in our 768-virtual space, where the
-- reference at typical resolutions reads nearer two-thirds.
ERA_WINDOW_SCALE = 0.78
function OptionsFrame_UpdateScale()
-- The era panel is registered `checkFit = 1`: the panel manager also shrinks it to
-- fit a small screen (UIPanelUpdateScaleForFit: min over width/height ratios
-- against the panel + 20 units of air each way, capped at 1) — transcribed here.
local k = ERA_WINDOW_SCALE
local wFit = GetScreenWidth() / 940
local hFit = GetScreenHeight() / 744
if wFit < k then k = wFit end
if hFit < k then k = hFit end
OptionsFrame:SetScale(k)
end
-- The era tab width law (MinimalTab.lua l.7): label width + 40. Era measures at OnLoad;
-- our measure round-trip answers a frame late, so OnUpdate polls until the width is real
-- (the chat-header converge idiom), then latches. Deliberate divergence: the era
-- measures with the MEDIUM font it is about to swap away (an accident of its code
-- order, ~6 units of extra padding); we measure the small font the label actually
-- wears.
function OptionsTab_FitWidth(tab)
if tab.fitted then return end
local w = getglobal(tab:GetName() .. "Text"):GetStringWidth()
if w and w > 0 then
tab:SetWidth(w + 40)
tab.fitted = true
end
end
-- Each row's OnLoad: remember the key + title, paint the label. The whole row identity
-- lives here so adding a category is one XML row + one key in the table above.
function OptionsCategoryRow_OnLoad(row, key, label)
row.categoryKey = key
row.pageTitle = label
row:SetText(label)
end
-- The selection: exactly one row wears options_list_active + a white label; everyone
-- else drops back to bare + gold (the era CategoryList mixin's SetCategoryActive, cut to
-- our static list). Also writes the page title and swaps the page BODY: a category with
-- rows (OPTIONS_PAGE_ROWS) shows its body refreshed from the CVars and Defaults goes
-- live with it; every other page is still empty, Defaults disabled.
function OptionsFrame_SelectCategory(key)
OptionsFrame.selectedCategory = key
for _, k in ipairs(OPTIONS_CATEGORY_KEYS) do
local row = getglobal("OptionsFrameCategoryListRow" .. k)
local bg = getglobal("OptionsFrameCategoryListRow" .. k .. "Bg")
if k == key then
bg:SetAtlas("Options_List_Active", true)
row:SetTextFontObject("GameFontHighlight")
OptionsFrameContainerTitle:SetText(row.pageTitle)
else
bg:SetTexture(nil)
row:SetTextFontObject("GameFontNormal")
end
end
for k in pairs(OPTIONS_PAGE_ROWS) do
local body = getglobal("OptionsFrameContainerBody" .. k)
if k == key then body:Show() else body:Hide() end
end
if OPTIONS_PAGE_ROWS[key] then
OptionsPage_Refresh(key)
OptionsFrameContainerDefaults:Enable()
else
OptionsFrameContainerDefaults:Disable()
end
end
-- ── The setting pages: 1.12 rows in the era Settings grammar ────────────────────────
-- One machinery, every page (Audio 0957, the first). Semantics (rows, CVars, labels,
-- dependencies, the click-sound quirk) are the 1.12 client's own panels, quoted
-- verbatim (SoundOptionsFrame.lua / OptionsFrame.lua + GlobalStrings.lua); geometry is
-- the era settings list (Blizzard_SettingControls.xml/.lua + Blizzard_SettingsList.lua
-- @1.15.9 — 26-tall rows on a 9 spacing, label LEFT+37 → CENTER-85, control at
-- CENTER-80, child rows +15 indent in the small font). Era rows with no 1.12 CVar +
-- benilla knob behind them (Enable Sound=Sound_EnableAllSound, Dialog, output device,
-- channels…) do not ship — the 0950 honest-tree rule applied to settings.
OPTIONS_PAGE_ROWS = {
Controls = { "RowStickyTarget", "RowAutoLoot", "RowInvertMouse" },
Audio = {
"RowEnableAll", "RowMaster", "RowSound", "RowMusic", "RowAmbience",
"RowEnableMusic", "RowEnableAmbience",
},
Graphics = { "RowUiScale" },
}
function OptionsRow_OnLoad(row, cvar, label, child)
row.cvar = cvar
local l = getglobal(row:GetName() .. "Label")
l:SetText(label)
if child then
-- era child rows: +15 label indent and the small font
-- (SettingControls.lua l.319-322); the control itself does not indent.
l:SetFontObject("GameFontNormalSmall")
l:ClearAllPoints()
l:SetPoint("LEFT", row:GetName(), "LEFT", 52, 0)
l:SetPoint("RIGHT", row:GetName(), "CENTER", -85, 0)
end
end
-- A slider row's own range and readout, when the template's 0..1 / 5% / percent (the
-- volume shape) isn't it — called after OptionsRow_OnLoad in the row's OnLoad. The
-- refreshing guard: SetMinMaxValues re-clamps the held value and fires OnValueChanged,
-- and that clamp is wiring, not a player gesture — without the guard every load would
-- SetCVar the template's 0 clamped into the new range.
function OptionsSliderRow_Setup(row, min, max, step, fmt)
local slider = getglobal(row:GetName() .. "ControlSlider")
row.fmt = fmt
row.refreshing = 1
slider:SetMinMaxValues(min, max)
slider:SetValueStep(step)
row.refreshing = nil
end
-- The right-hand readout text (era MinimalSlider's RIGHT label seat): rounded percent
-- (era FormatPercentage, Audio.lua l.406-411) unless the row asked for the raw number.
function OptionsRow_FormatValue(row, value)
if row.fmt == "raw" then
return string.format("%d", math.floor(value + 0.5))
end
return string.format("%d%%", math.floor(value * 100 + 0.5))
end
-- The era row-wide hover wash (HoverBackgroundTemplate, white a=0.1): the row and every
-- control on it show the SAME texture, so crossing from label to slider doesn't blink.
function OptionsRow_Hover(row, on)
local hover = getglobal(row:GetName() .. "Hover")
if on then hover:Show() else hover:Hide() end
end
function OptionsCheckbox_OnClick(box)
local row = box:GetParent()
local checked = box:GetChecked()
-- Click kits: the 1.12 panels disagree with EACH OTHER — the interface panel's
-- PlayClickSound maps checked→CheckBoxOn (OptionsFrame.lua l.509), while the sound
-- panel's own OnClick maps checked→CheckBoxOff (SoundOptionsFrame.lua, the 0957
-- quirk) — so each page keeps its own panel's mapping (soundQuirk on Audio rows).
local kitOn = checked
if row.soundQuirk then kitOn = not kitOn end
if kitOn then
PlaySound("igMainMenuOptionCheckBoxOn")
else
PlaySound("igMainMenuOptionCheckBoxOff")
end
-- The write: checked = 1 — unless the row is value-INVERTED (Sticky Targeting:
-- 1.12 checks it when deselectOnClick is "0", era wraps the same CVar in
-- NegateBoolean; both references agree).
local on = checked
if row.invert then on = not on end
if on then
SetCVar(row.cvar, 1)
else
SetCVar(row.cvar, 0)
end
OptionsPage_UpdateDependencies(OptionsFrame.selectedCategory)
end
function OptionsSlider_OnValueChanged(slider, value)
local control = slider:GetParent()
local row = control:GetParent()
if not row.refreshing then
-- era obeyStepOnDrag (MinimalSlider.xml l.5-7): user moves snap to the step
-- grid — anchored at the range MINIMUM, the reference slider's own anchor
-- (farclip's 1.12 grid is 177+n*60, not the multiples of 60). A refresh
-- writes the stored value raw (era SetValue does not snap), so an off-grid
-- config value still lands where it said.
local step = slider:GetValueStep()
if step and step > 0 then
local minv = slider:GetMinMaxValues()
local snapped = minv + math.floor((value - minv) / step + 0.5) * step
if math.abs(snapped - value) > 0.00001 then
slider:SetValue(snapped) -- re-enters once with the snapped value
return
end
value = snapped -- the clean grid number, not the f32-rounded echo
end
end
getglobal(control:GetName() .. "Value"):SetText(OptionsRow_FormatValue(row, value))
if row.cvar and not row.refreshing then
-- Two decimals cover every page's grid (the 5% volumes, uiScale's 0.01, whole
-- yards); trailing zeros stripped so a value at the registered default
-- compares equal to it as a STRING and stays out of the config file ("0.40"
-- would read as moved-off-"0.4").
local str = string.gsub(string.format("%.2f", value), "%.?0+$", "")
if row.deferred then
-- era CommitFlag.Apply, transcribed (Blizzard_Setting.lua's staged
-- SetValue half): the control shows the new value, the ENGINE keeps the
-- old one, and the CVar moves only when Apply commits — dragging a
-- slider that rescales itself mid-drag is exactly what this flag
-- exists to prevent. Dragging back onto the committed value clears the
-- pending (era's IsModified: a pending EQUAL to current isn't one).
if str == GetCVar(row.cvar) then
row.pending = nil
else
row.pending = str
end
OptionsApply_Update()
else
SetCVar(row.cvar, str)
end
end
end
function OptionsStepper_OnClick(btn, dir)
local slider = getglobal(btn:GetParent():GetName() .. "Slider")
-- era steppers: one step per click, the checkbox-on kit (MinimalSlider.lua l.53-57).
PlaySound("igMainMenuOptionCheckBoxOn")
slider:SetValue(slider:GetValue() + dir * slider:GetValueStep())
end
function OptionsPage_Refresh(key)
local base = "OptionsFrameContainerBody" .. key
for _, rkey in ipairs(OPTIONS_PAGE_ROWS[key]) do
local row = getglobal(base .. rkey)
local box = getglobal(base .. rkey .. "Check")
if box then
-- SetChecked takes the CVar string exactly like the 1.12 panel: the numeric
-- read means "0" unchecks (button-check-and-state-texture.md). A
-- value-inverted row checks on "0" instead — UIOptionsFrame.lua's own
-- GAMEFIELD_DESELECT arm (`checked = 1` when the CVar reads "0").
if row.invert then
local checked
if GetCVar(row.cvar) == "0" then checked = 1 end
box:SetChecked(checked)
else
box:SetChecked(GetCVar(row.cvar))
end
else
local slider = getglobal(base .. rkey .. "ControlSlider")
row.refreshing = 1
-- Pending first (era GetValue returns the pendingValue while one is
-- staged): coming back to a page with an un-applied edit shows the edit.
slider:SetValue(tonumber(row.pending or GetCVar(row.cvar) or "0") or 0)
row.refreshing = nil
-- SetValue fires only on a CHANGE — write the readout unconditionally,
-- from the slider's OWN value: in range that IS the CVar (SetValue stores
-- raw), out of range the thumb and the number at least agree on the
-- nearest expressible value.
getglobal(base .. rkey .. "ControlValue"):SetText(
OptionsRow_FormatValue(row, slider:GetValue()))
end
end
OptionsPage_UpdateDependencies(key)
OptionsApply_Update()
end
-- Per-page dependency rules. Audio's is the 1.12 rule, verbatim: Enable All Sound off
-- greys Enable Ambience — and only it, of our rows
-- (SoundOptionsFrame_UpdateDependencies disables buttons 2 and 4, Ambience and Error
-- Speech; Enable Music stays live, the panel's own quirk).
function OptionsPage_UpdateDependencies(key)
if key ~= "Audio" then return end
local on = GetCVar("MasterSoundEffects") ~= "0"
local box = OptionsFrameContainerBodyAudioRowEnableAmbienceCheck
local label = OptionsFrameContainerBodyAudioRowEnableAmbienceLabel
if on then
box:Enable()
label:SetFontObject("GameFontNormal")
else
box:Disable()
label:SetFontObject("GameFontDisable")
end
end
-- Defaults, live on any page with rows: every row's CVar back to its registered
-- default (era resets per-setting the same way; the era all-pages confirm dialog is
-- not modeled — Defaults resets the VISIBLE page). A pending edit dies with it — the
-- default write supersedes what Apply would have committed.
function OptionsPage_Defaults(key)
local base = "OptionsFrameContainerBody" .. key
for _, rkey in ipairs(OPTIONS_PAGE_ROWS[key]) do
local row = getglobal(base .. rkey)
row.pending = nil
SetCVar(row.cvar, GetCVarDefault(row.cvar))
end
OptionsPage_Refresh(key)
end
-- ── The Apply flow (0961): era CommitFlag.Apply, cut to our rows ────────────────────
-- Pending edits are PANEL-wide like era's modified table: they survive a category
-- switch (the row redisplays its pending on return) and commit together; only hiding
-- the window discards them (era wipes on hide too — its close-time 3-button confirm
-- dialog and the 8-second revert timer are not modeled, SCOPE note).
-- The era Apply gate (HasUnappliedSettings + SetApplyButtonEnabled): the button is
-- enabled AND shown together — it EXISTS only while something is pending.
function OptionsApply_Update()
local pending
for key, rows in pairs(OPTIONS_PAGE_ROWS) do
local base = "OptionsFrameContainerBody" .. key
for _, rkey in ipairs(rows) do
if getglobal(base .. rkey).pending then pending = 1 end
end
end
if pending then
OptionsFrameApplyButton:Enable()
OptionsFrameApplyButton:Show()
else
OptionsFrameApplyButton:Disable()
OptionsFrameApplyButton:Hide()
end
end
-- Commit (era SettingsPanelMixin:CommitSettings, cut to our rows): every pending
-- value everywhere becomes a real SetCVar, and the button goes away.
function OptionsPage_Apply()
for key, rows in pairs(OPTIONS_PAGE_ROWS) do
local base = "OptionsFrameContainerBody" .. key
for _, rkey in ipairs(rows) do
local row = getglobal(base .. rkey)
if row.pending then
SetCVar(row.cvar, row.pending)
row.pending = nil
end
end
end
OptionsApply_Update()
end
-- Hide = discard (era's WipeModifiedTable-on-hide; the confirm dialog is cut): the
-- next open re-reads the committed CVars, so the sliders land back on the truth.
function OptionsPage_DiscardPending()
for key, rows in pairs(OPTIONS_PAGE_ROWS) do
local base = "OptionsFrameContainerBody" .. key
for _, rkey in ipairs(rows) do
getglobal(base .. rkey).pending = nil
end
end
OptionsApply_Update()
end
]]></Script>
<!-- The red panel button (era UIPanelButtonTemplate seat, 96x22): GameMenuButtonTemplate's
four-state UI-Panel-Button art + white/white/grey label trio, copied at this window's
size (GameMenuFrame.xml l.145 is the shared original; templates don't cross files). -->
<Button name="OptionsRedButtonTemplate" virtual="true">
<Size><AbsDimension x="96" y="22"/></Size>
<ButtonText name="$parentText"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
<NormalFont inherits="GameFontHighlight"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
</Button>
<!-- One category row (era Blizzard_CategoryList.xml's category button): the template's
175 width is never rendered in the era either — the ScrollBox stretches every row to
the scroll target's width, 199 with no scroll bar (CategoryList.lua l.176-197 +
ScrollBoxViewUtil), and ours is the no-bar case. Gold label centered in the row (the
full-row box below); a
CENTER bg that is EMPTY until the hover/selected atlas lands on it (options_list_hover
/ options_list_active at atlas size — 187x21, centered: 6 units in from either edge of
the 199 row, the alignment the first cut's 175-wide rows broke). Selection is
OptionsFrame_SelectCategory's; hover only ever paints a row that is NOT the selection. -->
<Button name="OptionsCategoryRowTemplate" virtual="true">
<Size><AbsDimension x="199" y="20"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBg">
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
</Layer>
</Layers>
<!-- The era label is a box spanning the WHOLE row — TOPLEFT (36,+1) AND BOTTOMRIGHT
(0,+1), era CategoryList.xml l.72-77 — so the default MIDDLE vertical justify
CENTERS the text in the 20-tall row (1 unit high of true center, the era's own
nudge). The first cut had only the TOPLEFT corner: the auto-sized label hung from
the row TOP, ~4 units high of center ("text not vertically centered" — director,
2026-08-04). justifyH LEFT is the era's own attribute; a full-row box without it
would center horizontally. -->
<ButtonText name="$parentText" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="36" y="1"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontNormal"/>
<DisabledFont inherits="GameFontDisable"/>
<Scripts>
<OnClick>OptionsFrame_SelectCategory(this.categoryKey)</OnClick>
<OnEnter>
if OptionsFrame.selectedCategory ~= this.categoryKey then
getglobal(this:GetName() .. "Bg"):SetAtlas("Options_List_Hover", true)
end
</OnEnter>
<OnLeave>
if OptionsFrame.selectedCategory ~= this.categoryKey then
getglobal(this:GetName() .. "Bg"):SetTexture(nil)
end
</OnLeave>
</Scripts>
</Button>
<!-- A category group header (era SettingsCategoryListHeaderTemplate, stretched to the 199
row width like the rows): a 14px white label at LEFT (20,-1) over the group's
BACKGROUND WASH — the Options_CategoryHeader_<n> atlas the era Init pins at the
header's TOPLEFT at atlas size (199x144: a header band whose gradient runs down over
the group's own rows; the rows' hover/selected plates draw above it, being later
siblings). The <n> cycles per header (CategoryList.lua l.378-384) — each instance's
OnLoad passes its own. -->
<Frame name="OptionsCategoryHeaderTemplate" virtual="true">
<Size><AbsDimension x="199" y="30"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentBg">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentLabel" inherits="OptionsFontHighlightMedium">
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="20" y="-1"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- One SETTING row, checkbox flavor (era SettingsCheckboxControlTemplate over
SettingsListElementTemplate, Blizzard_SettingControls.xml l.50-112): 26 tall, stretched
to the page width by its instance anchors; label GameFontNormal in a LEFT(37) →
CENTER(-85) box (the era mixin's runtime anchors, SettingControls.lua l.322-323); the
30x29 box at LEFT = row CENTER-80 (l.473). The checkbox art is the era minimal set at
its state seats (SettingControls.xml l.92-95): checkbox-minimal normal AND pushed (no
distinct pressed art), checkmark-minimal checked, checkmark-minimal-disabled
disabled+checked; no HighlightTexture — hover feedback is the row-wide Hover wash
(HoverBackgroundTemplate: white a=0.1, hung 10 left / 5 right of the row). -->
<Frame name="OptionsCheckboxRowTemplate" virtual="true" enableMouse="true">
<Size><AbsDimension y="26"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentHover" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor>
</Anchors>
<Color r="1" g="1" b="1" a="0.1"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentLabel" inherits="GameFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="37" y="0"/></Offset></Anchor>
<Anchor point="RIGHT" relativePoint="CENTER"><Offset><AbsDimension x="-85" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<CheckButton name="$parentCheck">
<Size><AbsDimension x="30" y="29"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="CENTER"><Offset><AbsDimension x="-80" y="0"/></Offset></Anchor></Anchors>
<NormalTexture atlas="checkbox-minimal"/>
<PushedTexture atlas="checkbox-minimal"/>
<CheckedTexture atlas="checkmark-minimal"/>
<DisabledCheckedTexture atlas="checkmark-minimal-disabled"/>
<Scripts>
<OnClick>OptionsCheckbox_OnClick(this)</OnClick>
<OnEnter>OptionsRow_Hover(this:GetParent(), 1)</OnEnter>
<OnLeave>OptionsRow_Hover(this:GetParent(), nil)</OnLeave>
</Scripts>
</CheckButton>
</Frames>
<Scripts>
<OnEnter>OptionsRow_Hover(this, 1)</OnEnter>
<OnLeave>OptionsRow_Hover(this, nil)</OnLeave>
</Scripts>
</Frame>
<!-- One SETTING row, slider flavor (era SettingsSliderControlTemplate →
MinimalSliderWithSteppersTemplate, SettingControls.lua l.550-552 + MinimalSlider.xml):
the same 26-tall row and label box; the 250x40 control at LEFT = row CENTER(-80,+3)
(its 40 height overrides the row symmetrically). Inside: the Slider inset 19 each side
(hit area the full height, the way era's is), wearing the minimal_sliderbar three-slice
at atlas size on the LEFT/RIGHT points (17-tall track centered) with the middle
stretched between the caps, and the 20x19 button thumb. Steppers hang 4 outside the
slider: 11x19 left, 9x18 right (the era's own asymmetry, MinimalSlider.xml l.43-64).
The readout is era's RIGHT label seat — 25 right of the slider edge = 6 right of the
control (MinimalSlider.xml l.73-77), rounded-percent formatted. -->
<Frame name="OptionsSliderRowTemplate" virtual="true" enableMouse="true">
<Size><AbsDimension y="26"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentHover" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor>
</Anchors>
<Color r="1" g="1" b="1" a="0.1"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentLabel" inherits="GameFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="37" y="0"/></Offset></Anchor>
<Anchor point="RIGHT" relativePoint="CENTER"><Offset><AbsDimension x="-85" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Frame name="$parentControl">
<Size><AbsDimension x="250" y="40"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="CENTER"><Offset><AbsDimension x="-80" y="3"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString name="$parentValue" inherits="GameFontNormal">
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Slider name="$parentSlider" orientation="HORIZONTAL" minValue="0" maxValue="1" valueStep="0.05">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="19" y="0"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-19" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentTrackLeft" atlas="minimal_sliderbar_left" useAtlasSize="true">
<Anchors><Anchor point="LEFT"/></Anchors>
</Texture>
<Texture name="$parentTrackRight" atlas="minimal_sliderbar_right" useAtlasSize="true">
<Anchors><Anchor point="RIGHT"/></Anchors>
</Texture>
<Texture name="$parentTrackMiddle" atlas="_minimal_sliderbar_middle">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTrackLeft" relativePoint="TOPRIGHT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentTrackRight" relativePoint="BOTTOMLEFT"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<ThumbTexture atlas="minimal_sliderbar_button" useAtlasSize="true"/>
<Scripts>
<OnValueChanged>OptionsSlider_OnValueChanged(this, arg1)</OnValueChanged>
<OnEnter>OptionsRow_Hover(this:GetParent():GetParent(), 1)</OnEnter>
<OnLeave>OptionsRow_Hover(this:GetParent():GetParent(), nil)</OnLeave>
</Scripts>
</Slider>
<Button name="$parentBack">
<Size><AbsDimension x="11" y="19"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="$parentSlider" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
<NormalTexture atlas="minimal_sliderbar_button_left"/>
<Scripts>
<OnClick>OptionsStepper_OnClick(this, -1)</OnClick>
<OnEnter>OptionsRow_Hover(this:GetParent():GetParent(), 1)</OnEnter>
<OnLeave>OptionsRow_Hover(this:GetParent():GetParent(), nil)</OnLeave>
</Scripts>
</Button>
<Button name="$parentForward">
<Size><AbsDimension x="9" y="18"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentSlider" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<NormalTexture atlas="minimal_sliderbar_button_right"/>
<Scripts>
<OnClick>OptionsStepper_OnClick(this, 1)</OnClick>
<OnEnter>OptionsRow_Hover(this:GetParent():GetParent(), 1)</OnEnter>
<OnLeave>OptionsRow_Hover(this:GetParent():GetParent(), nil)</OnLeave>
</Scripts>
</Button>
</Frames>
</Frame>
</Frames>
<Scripts>
<OnEnter>OptionsRow_Hover(this, 1)</OnEnter>
<OnLeave>OptionsRow_Hover(this, nil)</OnLeave>
</Scripts>
</Frame>
<Frame name="OptionsFrame" frameStrata="HIGH" enableMouse="true" hidden="true">
<Size><AbsDimension x="920" y="724"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<!-- FlatPanelBackgroundTemplate as one fill (header SCOPE note: the era tints
rounded-corner pieces with the binary-side PANEL_BACKGROUND_COLOR; this is
that color at the template's exact insets). -->
<Texture name="$parentFlatBg">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-18"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-3" y="3"/></Offset></Anchor>
</Anchors>
<Color r="0.122" g="0.118" b="0.129" a="0.8"/>
</Texture>
<!-- The title band behind the top tile (era `_UI-Frame-TitleTileBg`, SharedXML
Classic/SharedUIPanelTemplates.xml l.78: Interface\FrameGeneral\_UI-Frame —
era-only, manifest fdid 374157 — 256x17 horizTile). STRETCHED (SCOPE note). -->
<Texture name="$parentTitleBg" file="era:textures/374157.blp">
<Size><AbsDimension y="17"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-3"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor>
</Anchors>
<TexCoords left="0" right="1" top="0.2890625" bottom="0.421875"/>
</Texture>
</Layer>
<Layer level="BORDER">
<!-- The ButtonFrameTemplateNoPortrait nine-slice, unrolled (NineSliceLayouts.lua
l.132 + NineSlice.lua's edge anchoring): corners at their corners at atlas
size; each edge pinned between its two corners on the anchored axis, its
other axis staying the atlas size. The three tile edges + the top band are
STRETCHED (SCOPE note). -->
<Texture name="$parentTopLeftCorner" atlas="UI-Frame-TopLeftCornerNoPortrait" useAtlasSize="true">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
<Texture name="$parentTopRightCorner" atlas="UI-Frame-TopCornerRight" useAtlasSize="true">
<Anchors><Anchor point="TOPRIGHT"/></Anchors>
</Texture>
<Texture name="$parentBotLeftCorner" atlas="UI-Frame-BotCornerLeft" useAtlasSize="true">
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture name="$parentBotRightCorner" atlas="UI-Frame-BotCornerRight" useAtlasSize="true">
<Anchors><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
<Texture name="$parentTopEdge" atlas="_UI-Frame-TitleTile" useAtlasSize="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTopLeftCorner" relativePoint="TOPRIGHT"/>
<Anchor point="TOPRIGHT" relativeTo="$parentTopRightCorner" relativePoint="TOPLEFT"/>
</Anchors>
</Texture>
<Texture name="$parentBotEdge" atlas="_UI-Frame-Bot" useAtlasSize="true">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentBotLeftCorner" relativePoint="BOTTOMRIGHT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBotRightCorner" relativePoint="BOTTOMLEFT"/>
</Anchors>
</Texture>
<Texture name="$parentLeftEdge" atlas="!UI-Frame-LeftTile" useAtlasSize="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTopLeftCorner" relativePoint="BOTTOMLEFT"/>
<Anchor point="BOTTOMLEFT" relativeTo="$parentBotLeftCorner" relativePoint="TOPLEFT"/>
</Anchors>
</Texture>
<!-- The right edge. The era layout's odd x=-8 / x1=8 pair (NineSliceLayouts
l.139) puts DIFFERENT x on the tile's two corner anchors — the reference
engine draws the quad through its anchor points, so the era tile is a
barely-visible lean (top 8 left, bottom 8 right; mean = flush at the frame
edge, which is exactly how the live window reads — director's capture,
2026-08-04). This engine's regions are axis-aligned rects, so we author the
MEAN: both corners flush (x=0). The first cut gave the whole tile to the
-8 anchor instead — the border floated 8 units into the panel with a dark
gap at the true edge, the director's "right border looks off". -->
<Texture name="$parentRightEdge" atlas="!UI-Frame-RightTile" useAtlasSize="true">
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="$parentTopRightCorner" relativePoint="BOTTOMRIGHT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBotRightCorner" relativePoint="TOPRIGHT"/>
</Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The inner dark panel — the one big era piece, at its full atlas size. -->
<Texture name="$parentInnerFrame" atlas="Options_InnerFrame" useAtlasSize="true">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="17" y="-64"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentTitleText" inherits="GameFontNormal" text="Options">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- ── Tabs — the era MinimalTabTemplate plates (SharedXML Shared/Tabs/MinimalTab
l.3-46 + the mixin), authored STATIC: Game is the only tab with anything
behind it, so it wears the selected state permanently (Options_Tab_Active_*
plates, white small text lifted to BOTTOM+6) and AddOns wears the unselected
one (Options_Tab_* plates, gold small text at BOTTOM+4 — era's unselected
look, per the director's capture; the button is still dead). The three-slice:
Left/Right caps BOTTOM-anchored at atlas size (7x26 active / 7x23 not, so the
selected plate stands 3 taller), Middle (a 1px column) stretched between. The
era's width law is text + 40, measured at OnLoad with the MEDIUM font before
the small font lands (MinimalTab.lua l.7-9, the quirk kept verbatim) — our
measure round-trip answers a frame late, so OnUpdate polls until the width is
real, the chat-header idiom. -->
<Button name="$parentGameTab">
<Size><AbsDimension x="72" y="37"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="32" y="-27"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentLeft" atlas="Options_Tab_Active_Left" useAtlasSize="true">
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture name="$parentRight" atlas="Options_Tab_Active_Right" useAtlasSize="true">
<Anchors><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
<Texture name="$parentMiddle" atlas="Options_Tab_Active_Middle" useAtlasSize="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentLeft" relativePoint="TOPRIGHT"/>
<Anchor point="TOPRIGHT" relativeTo="$parentRight" relativePoint="TOPLEFT"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<ButtonText name="$parentText" text="Game">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="6"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontHighlightSmall"/>
<HighlightFont inherits="GameFontHighlightSmall"/>
<DisabledFont inherits="GameFontHighlightSmall"/>
<Scripts>
<OnUpdate>OptionsTab_FitWidth(this)</OnUpdate>
</Scripts>
</Button>
<Button name="$parentAddOnsTab">
<Size><AbsDimension x="87" y="37"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentGameTab" relativePoint="TOPRIGHT"><Offset><AbsDimension x="5" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentLeft" atlas="Options_Tab_Left" useAtlasSize="true">
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
</Texture>
<Texture name="$parentRight" atlas="Options_Tab_Right" useAtlasSize="true">
<Anchors><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
<Texture name="$parentMiddle" atlas="Options_Tab_Middle" useAtlasSize="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentLeft" relativePoint="TOPRIGHT"/>
<Anchor point="TOPRIGHT" relativeTo="$parentRight" relativePoint="TOPLEFT"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<ButtonText name="$parentText" text="AddOns">
<Anchors><Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="4"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormalSmall"/>
<HighlightFont inherits="GameFontNormalSmall"/>
<DisabledFont inherits="GameFontNormalSmall"/>
<!-- No addon list behind it: dead to clicks, but wearing the era's unselected
GOLD (not disabled grey) — the director's capture is the call. -->
<Scripts>
<OnLoad>self:Disable()</OnLoad>
<OnUpdate>OptionsTab_FitWidth(this)</OnUpdate>
</Scripts>
</Button>
<!-- ── The category list (era CategoryList, static — it fits) ──────────────────── -->
<Frame name="$parentCategoryList">
<Size><AbsDimension x="199"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="18" y="-76"/></Offset></Anchor>
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="18" y="46"/></Offset></Anchor>
</Anchors>
<!-- The era list rhythm (CreateScrollBoxListLinearView spacing=2 + the 18-tall
SettingsCategoryListSpacerTemplate between groups): 2 between every
element, 2+18+2 = 22 from a group's last row to the next header. -->
<Frames>
<Frame name="$parentHeaderGameplay" inherits="OptionsCategoryHeaderTemplate">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
getglobal(self:GetName() .. "Label"):SetText("Gameplay")
getglobal(self:GetName() .. "Bg"):SetAtlas("Options_CategoryHeader_1", true)
</OnLoad>
</Scripts>
</Frame>
<Button name="$parentRowControls" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentHeaderGameplay" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Controls", "Controls")</OnLoad></Scripts>
</Button>
<Button name="$parentRowInterface" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowControls" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Interface", "Interface")</OnLoad></Scripts>
</Button>
<Button name="$parentRowActionBars" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowInterface" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "ActionBars", "Action Bars")</OnLoad></Scripts>
</Button>
<Button name="$parentRowCombat" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowActionBars" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Combat", "Combat")</OnLoad></Scripts>
</Button>
<Button name="$parentRowSocial" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowCombat" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Social", "Social")</OnLoad></Scripts>
</Button>
<Button name="$parentRowKeybindings" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowSocial" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Keybindings", "Keybindings")</OnLoad></Scripts>
</Button>
<!-- Nameplates — the era Gameplay group's own last row (NAMEPLATE_OPTIONS_LABEL,
order 15), backed by benilla's real nameplate knobs when the pages land. -->
<Button name="$parentRowNameplates" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowKeybindings" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Nameplates", "Nameplates")</OnLoad></Scripts>
</Button>
<Frame name="$parentHeaderSystem" inherits="OptionsCategoryHeaderTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowNameplates" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-22"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>
getglobal(self:GetName() .. "Label"):SetText("System")
getglobal(self:GetName() .. "Bg"):SetAtlas("Options_CategoryHeader_2", true)
</OnLoad>
</Scripts>
</Frame>
<Button name="$parentRowGraphics" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentHeaderSystem" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Graphics", "Graphics")</OnLoad></Scripts>
</Button>
<Button name="$parentRowAudio" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowGraphics" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Audio", "Audio")</OnLoad></Scripts>
</Button>
</Frames>
</Frame>
<!-- ── The page area (era SettingsList's header; the body is next slice's) ─────── -->
<Frame name="$parentContainer">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentCategoryList" relativePoint="TOPRIGHT"><Offset><AbsDimension x="16" y="0"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-22" y="47"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentDivider" atlas="Options_HorizontalDivider" useAtlasSize="true">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-50"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- The page title — OptionsFrame_SelectCategory writes it. -->
<FontString name="$parentTitle" inherits="OptionsFontHighlightHuge">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-22"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- Live on any page with rows (OPTIONS_PAGE_ROWS);
OptionsFrame_SelectCategory enables/disables it with the page swap. -->
<Button name="$parentDefaults" inherits="OptionsRedButtonTemplate" text="Defaults">
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-36" y="-16"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:Disable()</OnLoad>
<OnClick>OptionsPage_Defaults(OptionsFrame.selectedCategory)</OnClick>
</Scripts>
</Button>
<!-- The page body: one child frame per category with rows, shown by
OptionsFrame_SelectCategory (Audio 0957, Graphics 0959). -->
<Frame name="$parentBody">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-50"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<!-- The Controls page (0961): the gameplay checkboxes with benilla
knobs behind them, in the era Controls page's own row order
(Controls.lua @1.15.9: Sticky Targeting #1, Auto Loot #6,
Invert Mouse #12 — its Mouse section header is cut while one
row survives under it). Sticky Targeting is value-INVERTED in
BOTH references (1.12 UIOptionsFrame checks it on
deselectOnClick=="0"; era wraps the CVar in NegateBoolean);
labels are 1.12 GlobalStrings where the row exists there
(GAMEFIELD_DESELECT_TEXT, INVERT_MOUSE), era's for Auto Loot
(no 1.12 row exists). -->
<Frame name="$parentControls" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<Frame name="$parentRowStickyTarget" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-12"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-20" y="-12"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "deselectOnClick", "Sticky Targeting")
self.invert = 1
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowAutoLoot" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowStickyTarget" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowStickyTarget" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "autoLootDefault", "Auto Loot")</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowInvertMouse" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowAutoLoot" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowAutoLoot" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "mouseInvertPitch", "Invert Mouse")</OnLoad></Scripts>
</Frame>
</Frames>
</Frame>
<!-- The Audio page. Row seats transcribe the era settings list
(Blizzard_SettingsList.lua l.43-47 + .xml: scroll box hung
-15/-20 off the header block with padLeft 25 / padRight 0 and
verticalPad 10 → first row at (10,-12); spacing 9 → 35 pitch).
Row ORDER is the era Audio page's grammar (volume block first,
then the enables) over the 1.12 row set: the three category
volumes ride as CHILDREN of Master Volume exactly like era's,
in the 1.12 panel's own slider order (Sound, Music, Ambience —
SoundOptionsFrame.lua's index order). -->
<Frame name="$parentAudio" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<Frame name="$parentRowEnableAll" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-12"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-20" y="-12"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "MasterSoundEffects", "Enable All Sound")
self.soundQuirk = 1
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowMaster" inherits="OptionsSliderRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowEnableAll" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowEnableAll" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "MasterVolume", "Master Volume")</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowSound" inherits="OptionsSliderRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowMaster" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowMaster" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "SoundVolume", "Sound Volume", 1)</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowMusic" inherits="OptionsSliderRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowSound" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowSound" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "MusicVolume", "Music Volume", 1)</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowAmbience" inherits="OptionsSliderRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowMusic" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowMusic" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>OptionsRow_OnLoad(self, "AmbienceVolume", "Ambience Volume", 1)</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowEnableMusic" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowAmbience" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowAmbience" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "EnableMusic", "Enable Music")
self.soundQuirk = 1
</OnLoad></Scripts>
</Frame>
<Frame name="$parentRowEnableAmbience" inherits="OptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowEnableMusic" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowEnableMusic" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "EnableAmbience", "Enable Ambience")
self.soundQuirk = 1
</OnLoad></Scripts>
</Frame>
</Frames>
</Frame>
<!-- The Graphics page (0959; farclip row retired 0961 — the
director's call: the page holds UI Scale alone for now, the
farclip CVar/knob/env/debug-panel plumbing all stay). Range/
step/label are the 1.12 OptionsFrameSliders table verbatim
(OptionsFrame.lua l.25): uiscale 0.64..1.0 step 0.01 (UI_SCALE
"UI Scale", percent readout — era's own
FormatPercentageRounded on its uiscale row). 1.12's useUiScale
gate has no benilla knob (our scale always applies; off ≡ 1.0)
so the checkbox doesn't ship. -->
<Frame name="$parentGraphics" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<Frame name="$parentRowUiScale" inherits="OptionsSliderRowTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-12"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-20" y="-12"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "uiScale", "UI Scale")
OptionsSliderRow_Setup(self, 0.64, 1.0, 0.01)
-- era registers its uiscale row CommitFlag.Apply
-- (Graphics.lua l.684): the one row whose live
-- write would rescale the very slider mid-drag —
-- it stages and commits through the Apply button.
self.deferred = 1
</OnLoad></Scripts>
</Frame>
</Frames>
</Frame>
</Frames>
</Frame>
</Frames>
</Frame>
<!-- ── The search box (era SearchBoxTemplate → InputBoxVisualTemplate,
SecureUIPanelTemplates.xml l.42-67; filters NOTHING yet — SCOPE note): a 20-tall
THREE-slice of common-search-border atlases, vertically centered on the 22 box —
left cap 8x20 hung 5 outside, right cap 8x20 flush, middle stretched between —
plus the 10x10 magnifier at LEFT (1,-1) and a 17x17 clear button at RIGHT (-3)
that appears with text (ClearButtonMixin, cut to our placeholder machinery).
The first cut transcribed the LEGACY nine-piece Common-Input-Border instead —
its ink drew ~27 units tall against the reference's 18 and rode 4 units high:
the director's "search bar is too high" (capture-measured, 2026-08-04). -->
<EditBox name="$parentSearchBox" autoFocus="false" letters="64">
<Size><AbsDimension x="350" y="22"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentContainer" relativePoint="TOPRIGHT"><Offset><AbsDimension x="4" y="20"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBorderLeft" atlas="common-search-border-left">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="$parentBorderRight" atlas="common-search-border-right">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="$parentBorderMiddle" atlas="common-search-border-middle">
<Size><AbsDimension y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentBorderLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentBorderRight" relativePoint="LEFT"/>
</Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- The "Search" placeholder (era instructions label: GameFontDisableSmall
tinted 0.35). OnTextChanged below toggles it on first text. -->
<FontString name="$parentInstructions" inherits="GameFontDisableSmall" justifyH="LEFT" text="Search">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="16" y="0"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-20" y="0"/></Offset></Anchor>
</Anchors>
<Color r="0.35" g="0.35" b="0.35"/>
</FontString>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentSearchIcon" atlas="common-search-magnifyingglass">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="1" y="-1"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- The clear X (era ClearButtonMixin: shown while text exists, half-alpha
icon, click wipes). Shown/hidden by the box's OnTextChanged below. -->
<Button name="$parentClearButton" hidden="true">
<Size><AbsDimension x="17" y="17"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-3" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon" atlas="common-search-clearbutton" alpha="0.5">
<Size><AbsDimension x="10" y="10"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="3" y="-3"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnClick>
OptionsFrameSearchBox:SetText("")
OptionsFrameSearchBox:ClearFocus()
</OnClick>
</Scripts>
</Button>
</Frames>
<TextInsets><AbsInset left="16" right="20" top="0" bottom="0"/></TextInsets>
<Scripts>
<!-- Placeholder + clear-button toggle only — no filtering behind it yet
(SCOPE note). -->
<OnTextChanged>
if this:GetText() == "" then
OptionsFrameSearchBoxInstructions:Show()
OptionsFrameSearchBoxClearButton:Hide()
else
OptionsFrameSearchBoxInstructions:Hide()
OptionsFrameSearchBoxClearButton:Show()
end
</OnTextChanged>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
</Scripts>
<FontString inherits="GameFontHighlightSmall"/>
</EditBox>
<!-- ── Close, both spellings ───────────────────────────────────────────────────── -->
<!-- The X — hung past the top-right corner like the era's ClosePanelButton, on the
1.12 minimize-ball art (header SCOPE note; the LootFrame close idiom). -->
<Button name="$parentClosePanelButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="4" y="4"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts>
<OnClick>
PlaySound("igMainMenuClose")
HideUIPanel(OptionsFrame)
</OnClick>
</Scripts>
</Button>
<!-- The red Close (era SettingsPanel.CloseButton). -->
<Button name="$parentCloseButton" inherits="OptionsRedButtonTemplate" text="Close">
<Anchors><Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-16" y="16"/></Offset></Anchor></Anchors>
<Scripts>
<OnClick>
PlaySound("igMainMenuClose")
HideUIPanel(OptionsFrame)
</OnClick>
</Scripts>
</Button>
<!-- The era Apply (SettingsPanel.ApplyButton: 96x22, RIGHT of Close's LEFT at -2,
Blizzard_SettingsPanel.xml l.52-57): exists only while a deferred row holds a
pending value — era enables and SHOWS it together (SetApplyButtonEnabled) —
and commits every pending edit on click. -->
<Button name="$parentApplyButton" inherits="OptionsRedButtonTemplate" text="Apply" hidden="true">
<Anchors><Anchor point="RIGHT" relativeTo="$parentCloseButton" relativePoint="LEFT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:Disable()</OnLoad>
<OnClick>OptionsPage_Apply()</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<!-- Controls is the default page; a re-opened window keeps its last selection (the
era remembers too — Settings.OpenToCategory's default seat). The scale re-fits
on every show — the era panel manager re-runs checkFit the same way. -->
<OnShow>
OptionsFrame_UpdateScale()
OptionsFrame_SelectCategory(this.selectedCategory or "Controls")
</OnShow>
<!-- Hiding discards pending edits (the era wipes its modified table on hide; its
close-time confirm dialog is cut — SCOPE note). -->
<OnHide>OptionsPage_DiscardPending()</OnHide>
</Scripts>
</Frame>
</Ui>