Files
benilla-octo/crates/benilla-app/assets/ui/TrainerFrame.xml
T
SamandClaude Fable 5 462602c44b the world becomes the UI's backdrop, and recipe rows go white on hover
- every translucent UI pixel over the 3D world composited in the wrong space: the world lane and the UI lane each blend in gamma bytes the way the reference's fixed-function device does, but the one blend that mixes them — the UI camera's finished image onto the sRGB swapchain — ran in linear, so a chat tab whose body is black at alpha 102/255 left the scene at 77.5% of its brightness instead of 60%, and the dock's hover glow (an ADD with no plate under it) read as a lozenge floating on grass; the world camera now renders to an off-screen image drawn as the first quad of the UI pass, so every UI blend over the world is the same blend as every UI blend over UI — after, the selected tab plate lands at 0.593–0.600 of the bare scene against 0.60 predicted, and bare-world pixels move at most 1 LSB
- the backdrop image is un-encoded `Rgba16Float`, because the UI arc composites in gamma and takes its one decode at the end — pre-encoding would land a second encode in that chain, and quantizing un-encoded values to 8 bits is the banding collapse measured long ago; and hiding the UI keeps the world: the toggle used to retire every batch in the pass, which would now be a black screen — hidden means the two UI lanes draw nothing while the backdrop, which is the world, still draws, with the no-world glue screens pinned on the old behaviour
- a button's highlighted label is its own font instance: the client swaps which of three embedded instances the label reads — it never merges colour across states — so the `highlight_color.or(normal_color)` fallback is gone, and `LockHighlight()` now enters the label, not just the highlight texture (the reference blanks a recipe row's highlight texture to "" and then locks the row it selected — with nothing left to pin, the lock's only possible effect is the label, and that is the white text on a selected recipe)
- tradeskill, craft and class-trainer rows read white on hover and on select: all three windows carried the row name as a child FontString no per-state font can reach, behind a "no engine method" note that was true when written and stale ever since — the name is now the button's own `<ButtonText>` under the base template's `<NormalFont>`/`<HighlightFont>`/`<DisabledFont>` trio, and the windows call `SetTextColor`/`LockHighlight`/`UnlockHighlight` where the reference calls them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 15:06:05 +07:00

896 lines
61 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>
<!--
benilla's class/profession trainer window (decisions 0237/0247/0251), a faithful port of the
TBC-backported `Blizzard_TrainerUI` addon the director's client runs (0246): a client-sorted,
collapsible **skill-line tree** with a **dropdown** state filter and a real **scroll bar**. The
window art/geometry are the real 1.12 `ClassTrainerFrame` (MerchantFrame.xml's four-quadrant-slab
idiom); the list/sort/collapse/filter logic is transcribed from `Blizzard_TrainerUI.lua` onto
benilla's engine (`crates/benilla-ui/src/script/trainer.rs`, which builds the tree — decision
0247) + the shared faux-scroll kit (ScrollTemplates.xml, decision 0251). This REPLACES the
earlier flat-list placeholder (git has it): flat state-as-category rows + inline sort checkboxes
+ a chunk-local scroll offset, which stood in until the tree/scrollbar/dropdown landed.
Modeled on the two known-good dressed windows for the framing:
- MerchantFrame.xml — the 384×512 four-quadrant window art (no <Backdrop>: the art IS the
frame), the NPC-session lifecycle (RegisterEvent trio + PLAYER_MONEY, SHOW/UPDATE/CLOSED
dispatch, OnShow/OnHide PlaySound with CloseTrainer() on hide), the "npc" model-portrait
binding, and the BenillaMoney_* coin rig (detail cost + player purse). The close X reuses
the minimize-button art (this engine ships no UIPanelCloseButton).
- QuestLogFrame.xml — the list-row Button template shape ($parent-named child regions wired in
<OnLoad>) and the persistent selection-highlight Frame (a child Frame whose ARTWORK Texture
renders when Show()n, moved onto the selected visible row).
WHAT'S NEW vs. the placeholder (decision 0247's tree, 0251's scroll bar):
- THE LIST IS A TREE. GetTrainerServiceInfo(i) now returns interleaved "header" rows (a skill
line's name, +/- fold) and service rows (name/state), 1-based into the VISIBLE tree. Update
renders each row by type; a header click folds its group, a service click selects it.
- THE FILTER IS A DROPDOWN. BenillaTrainerFilterDropDown (over UIDropDownMenu.xml's kit) with
three keepShownOnClick check rows (Available/Unavailable/Already Known) driving the engine's
state-visibility filter — replacing the three inline sort checkboxes. Its three states are
SAVED VARIABLES (decision 1128): defaults 1/1/0 at file scope — "Already Known" starts OFF,
the shipped 1.12.1 Blizzard_TrainerUI.lua:6-9's own values — restored from
benilla/saved-variables.lua at load, and pushed into the engine on show because the list
builder resets its mask on every packet.
- THE SCROLL BAR IS REAL. BenillaTrainerListScrollFrame (FauxScrollFrameTemplate): a
draggable Slider bar + up/down step arrows over the fixed 11 rows (the faux-scroll re-binds
rows to a moving offset — it never really scrolls a child). The wheel still lives on the
WINDOW (the engine bubbles a spin up the parent chain, so a spin over a row reaches here).
- COLLAPSE WORKS. The "All" tab collapses/expands every group (Collapse/ExpandTrainerSkillLine(0));
its +/- icon reflects whether all groups are folded (the backport's numHeaders/notExpanded).
FLAT layout — a CHOICE, and it was written down here as a constraint that does not exist.
This block used to claim "a child <Frame> shown at runtime does not render its own <Layers>
FontStrings in this engine". That is FALSE, and it was propagated from here into two more
windows before anyone probed it (decision 1517): MailFrame's own SendMailTitleText lives in a
runtime-shown pane's Layers and has rendered correctly the whole time. The detail-pane
FontStrings + the title/greeting do live on BenillaTrainerFrame's OWN <Layers>, and that is
fine — it is simply one way to lay this out, not the only one available. Per-row regions live
on the row Button template. The detail is a FIXED (non-scrolling) pane
(our descriptions are empty for now) — no real ScrollFrame, unlike the ref's DetailScrollFrame.
Adaptations / deviations (flagged):
- TITLE: the trainer's NAME rides arg1 of TRAINER_SHOW/UPDATE (the app resolves the creature
name ask-once via the NameCache — the MerchantFrame vendor-name precedent); "Trainer" until
it lands.
- PORTRAIT: the "npc" unit-token model bake (MerchantFrame's binding — the interaction NPC's face).
- GlobalStrings the ref reads (REQUIRES_LABEL, TRAINER_REQ_*, COSTS_LABEL, the labels) are
defined LOCALLY below as the byte-exact 1.12 enUS values (interface.MPQ GlobalStrings.lua,
line-cited); NORMAL/GRAY/HIGHLIGHT_FONT_COLOR are Fonts.xml globals.
- The affordability RED-TINT of the cost coins (ref SetMoneyFrameColor) is BenillaMoney_SetColor.
- The profession-confirm StaticPopup is skipped for v1 (Train buys directly); a later refinement.
Registers at the left UIPanel slot like Gossip/Merchant/Quest/QuestLog (UiPanels.xml) — opening
it competes for/replaces those. It IS an NPC session: CloseTrainer() fires on hide, and the
standardized range guard (crate::ui_session) client-side-closes it when the player walks away.
-->
<Script><![CDATA[
-- ── GlobalStrings / labels the ref reads (byte-exact 1.12 enUS, interface.MPQ) ────────────────
-- Chunk-local upvalues: the global functions below close over them; inline XML handlers (separate
-- chunks) can't, so every handler routes to a function here. `_RED` variants redden only the
-- number/name (met = white, unmet = red), exactly as the real requirements line reads.
local PARENS_TEMPLATE = "(%s)" -- GlobalStrings.lua:2468
local REQUIRES_LABEL = "Requires:" -- REQUIRES_LABEL (2768)
local COSTS_LABEL = "Cost:" -- COSTS_LABEL (667)
local UNKNOWN_LABEL = "Unknown" -- UNKNOWN (3860)
local FILTER_LABEL = "Filter" -- FILTER (1297)
local TRAINER_REQ_LEVEL = "Level |cffffffff%d|r" -- TRAINER_REQ_LEVEL (3739)
local TRAINER_REQ_LEVEL_RED = "Level |cffff2020%d|r" -- TRAINER_REQ_LEVEL_RED (3740)
local TRAINER_REQ_SKILL_RANK = "%s (|cffffffff%d|r)" -- TRAINER_REQ_SKILL_RANK (3741)
local TRAINER_REQ_SKILL_RANK_RED = "%s (|cffff2020%d|r)" -- TRAINER_REQ_SKILL_RANK_RED (3742)
local TRAINER_REQ_ABILITY = "|cffffffff%s|r" -- TRAINER_REQ_ABILITY (3737)
local TRAINER_REQ_ABILITY_RED = "|cffff2020%s|r" -- TRAINER_REQ_ABILITY_RED (3738)
-- ── The state filter's three SAVED VARIABLES (decision 1128) ─────────────────────────────────
-- The reference's own globals, values and mechanism: Blizzard_TrainerUI.lua:6-9 assigns these
-- at FILE SCOPE and its `.toc` declares them `## SavedVariables`; the client executes the
-- saved file over them AFTER this file loads and BEFORE the load event, so a remembered value
-- wins over the default. benilla's ported UI is one FrameXML tree rather than addons, so the
-- declaration rides `RegisterForSave` (FrameXML's own API for its 38 globals) and the same
-- ordering is the app's load seam. File scope is load-bearing: assign these in OnLoad and the
-- default would clobber the restored value every launch.
TRAINER_FILTER_AVAILABLE = 1
TRAINER_FILTER_UNAVAILABLE = 1
TRAINER_FILTER_USED = 0
RegisterForSave("TRAINER_FILTER_AVAILABLE")
RegisterForSave("TRAINER_FILTER_UNAVAILABLE")
RegisterForSave("TRAINER_FILTER_USED")
local LABEL_AVAILABLE = "Available" -- AVAILABLE (84)
local LABEL_UNAVAILABLE = "Unavailable" -- UNAVAILABLE (3840)
local LABEL_USED = "Already Known" -- USED (3868)
-- The font colour codes the dropdown rows wrap their labels in (GlobalStrings.lua: the *_CODE
-- family) — green/red/gray, closed with |r.
local GREEN_CODE = "|cff20ff20"
local RED_CODE = "|cffff2020"
local GRAY_CODE = "|cff808080"
local CODE_CLOSE = "|r"
-- CLASS_TRAINER_SKILLS_DISPLAYED / CLASS_TRAINER_SKILL_HEIGHT (ref Blizzard_TrainerUI.lua:2-3).
local BENILLA_TRAINER_ROWS = 11
local BENILLA_TRAINER_ROW_HEIGHT = 16
-- ── Detail pane ─────────────────────────────────────────────────────────────────────────────
-- Nothing selected → blank every detail region: a FontString "hides" via SetText(""), while the
-- icon Button + coin frames Hide. Predates region Show/Hide (region.rs has the real pair now);
-- equivalent either way, so the blank-text idiom stays.
function BenillaTrainer_HideDetails()
getglobal("BenillaTrainerSkillName"):SetText("")
getglobal("BenillaTrainerSkillRequirements"):SetText("")
getglobal("BenillaTrainerSkillDescription"):SetText("")
getglobal("BenillaTrainerCostLabel"):SetText("")
getglobal("BenillaTrainerSkillIcon"):Hide()
BenillaMoney_Clear("BenillaTrainerDetailMoney")
getglobal("BenillaTrainerTrainButton"):Disable()
end
-- Fill the detail pane for the selected service `id` (1-based visible index) — the transcribed
-- ClassTrainer_SetSelection detail half: name/sub name, the Requires: string (built term-for-term
-- as the ref does), the icon, the cost coins + label, the description, and Train's enable state.
function BenillaTrainer_ShowDetails(id)
local name = GetTrainerServiceInfo(id)
getglobal("BenillaTrainerSkillName"):SetText(name or UNKNOWN_LABEL)
-- The detail header shows the spell NAME only — no "(Rank N)" beside it. A deliberate
-- divergence from the ref (which sets a ClassTrainerSubSkillName subtext here): the rank is
-- already on the spell's list row, so a second copy floated to the right of the name just
-- read as clutter (director's call).
local icon = getglobal("BenillaTrainerSkillIcon")
icon:SetNormalTexture(GetTrainerServiceIcon(id) or BENILLA_FALLBACK_ICON)
icon:Show()
-- Requirements string (ref term-for-term): level gate, skill gate, each prerequisite ability;
-- each red when unmet (hasReq nil). Our feed has no pet-learn / step requirements.
local requirements = ""
local separator = ""
local reqLevel = GetTrainerServiceLevelReq(id)
if reqLevel and reqLevel > 1 then
separator = ", "
if (UnitLevel("player") or 0) >= reqLevel then
requirements = requirements .. format(TRAINER_REQ_LEVEL, reqLevel)
else
requirements = requirements .. format(TRAINER_REQ_LEVEL_RED, reqLevel)
end
end
local skill, rank, hasReq = GetTrainerServiceSkillReq(id)
if skill then
if hasReq then
requirements = requirements .. separator .. format(TRAINER_REQ_SKILL_RANK, skill, rank)
else
requirements = requirements .. separator .. format(TRAINER_REQ_SKILL_RANK_RED, skill, rank)
end
separator = ", "
end
local numReq = GetTrainerServiceNumAbilityReq(id)
for i = 1, numReq do
local ability, abilityHas = GetTrainerServiceAbilityReq(id, i)
if ability then
if abilityHas then
requirements = requirements .. separator .. format(TRAINER_REQ_ABILITY, ability)
else
requirements = requirements .. separator .. format(TRAINER_REQ_ABILITY_RED, ability)
end
separator = ", "
end
end
if requirements ~= "" then
getglobal("BenillaTrainerSkillRequirements"):SetText(REQUIRES_LABEL .. " " .. requirements)
else
getglobal("BenillaTrainerSkillRequirements"):SetText("")
end
-- Cost (ref): a free service shows no coins/label; otherwise the "Cost:" label + coins,
-- reddened when unaffordable (BenillaMoney_SetColor: white when GetMoney() >= cost, else red).
local moneyCost = GetTrainerServiceCost(id)
if moneyCost and moneyCost > 0 then
getglobal("BenillaTrainerCostLabel"):SetText(COSTS_LABEL)
BenillaMoney_Set("BenillaTrainerDetailMoney", moneyCost, true)
if (GetMoney() or 0) >= moneyCost then
BenillaMoney_SetColor("BenillaTrainerDetailMoney", 1.0, 1.0, 1.0)
else
BenillaMoney_SetColor("BenillaTrainerDetailMoney", 1.0, 0.1, 0.1)
end
else
getglobal("BenillaTrainerCostLabel"):SetText("")
BenillaMoney_Clear("BenillaTrainerDetailMoney")
end
getglobal("BenillaTrainerSkillDescription"):SetText(GetTrainerServiceDescription(id))
-- Train enable (ref): only an available AND affordable service. The talent/profession-point
-- gate is dropped (talentPts always 0; the profession-confirm is a later refinement).
local _, _, serviceType = GetTrainerServiceInfo(id)
local affordable = (GetMoney() or 0) >= (moneyCost or 0)
if serviceType == "available" and affordable then
getglobal("BenillaTrainerTrainButton"):Enable()
else
getglobal("BenillaTrainerTrainButton"):Disable()
end
end
-- ── The list page ────────────────────────────────────────────────────────────────────────────
-- Pick the first learnable (available) service, else the first non-header service (ref
-- ClassTrainer_SelectFirstLearnableSkill — the backport hardcodes index 2, the first service
-- under the first header; we find it, robust to which group leads / a leading collapsed group).
local function selectFirstService()
local n = GetNumTrainerServices()
for i = 1, n do
local _, _, serviceType = GetTrainerServiceInfo(i)
if serviceType == "available" then
SelectTrainerService(i)
return
end
end
for i = 1, n do
local _, _, serviceType = GetTrainerServiceInfo(i)
if serviceType ~= "header" then
SelectTrainerService(i)
return
end
end
end
-- Set the collapse-all tab's +/- icon from whether every group is folded (ref
-- ClassTrainerFrame_Update's numHeaders/notExpanded tail).
local function updateCollapseAllIcon(numServices)
local numHeaders, notExpanded = 0, 0
for i = 1, numServices do
local name, _, serviceType, isExpanded = GetTrainerServiceInfo(i)
if name and serviceType == "header" then
numHeaders = numHeaders + 1
if not isExpanded then notExpanded = notExpanded + 1 end
end
end
local b = getglobal("BenillaTrainerCollapseAllButton")
if numHeaders > 0 and notExpanded == numHeaders then
b.collapsed = 1
b:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
else
b.collapsed = nil
b:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
end
end
function BenillaTrainerFrame_Update()
getglobal("BenillaTrainerGreetingText"):SetText(GetTrainerGreetingText())
BenillaMoney_Set("BenillaTrainerMoney", GetMoney())
local numServices = GetNumTrainerServices()
if numServices == 0 then
getglobal("BenillaTrainerCollapseAllButton"):Disable()
else
getglobal("BenillaTrainerCollapseAllButton"):Enable()
end
-- The faux-scroll bar reconciles to the row count, then hands back the top-row offset.
local scrollFrame = getglobal("BenillaTrainerListScrollFrame")
FauxScrollFrame_Update(scrollFrame, numServices, BENILLA_TRAINER_ROWS, BENILLA_TRAINER_ROW_HEIGHT)
local skillOffset = FauxScrollFrame_GetOffset(scrollFrame)
getglobal("BenillaTrainerSkillHighlight"):Hide()
-- Fill the visible rows (ref ClassTrainerFrame_Update's row loop). Header rows show the skill
-- line name + a +/- fold; service rows blank the fold, indent the name two spaces, and colour
-- name+subtext by state (available green / used gray / unavailable red).
for i = 1, BENILLA_TRAINER_ROWS do
local button = getglobal("BenillaTrainerService" .. i)
local index = i + skillOffset
if index <= numServices then
local name, subText, serviceType, isExpanded = GetTrainerServiceInfo(index)
if not name then name = UNKNOWN_LABEL end
-- Every row is unlocked here and the selected one re-locked below (ref l.183/190).
-- The lock is what paints a selected row's name white: the template's
-- `<HighlightFont inherits="GameFontHighlight">` instance wins over the state
-- colour SetTextColor writes, which reaches the NORMAL instance only. It reaches
-- nothing else on a service row — the branch below blanks that row's fold glow.
button:UnlockHighlight()
if serviceType == "header" then
button:SetText(name)
button:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
button.subtext:SetText("")
if isExpanded then
button:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
else
button:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
end
button.foldHighlight:SetTexture("Interface\\Buttons\\UI-PlusButton-Hilight")
else
button:SetNormalTexture("")
button.foldHighlight:SetTexture("")
button:SetText(" " .. name)
if subText and subText ~= "" then
button.subtext:SetText(format(PARENS_TEMPLATE, subText))
else
button.subtext:SetText("")
end
-- The rank rides the NAME's right edge, wherever the name ended (ref l.157,
-- B253's flow row) — re-anchored every repaint because the name's width is
-- the thing that changes.
button.subtext:SetPoint("LEFT", getglobal(button:GetName() .. "Text"), "RIGHT", 10, 0)
if serviceType == "available" then
button:SetTextColor(0, 1.0, 0)
button.subR, button.subG, button.subB = 0, 0.6, 0
elseif serviceType == "used" then
button:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
button.subR, button.subG, button.subB = 0.5, 0.5, 0.5
else
button:SetTextColor(0.9, 0, 0)
button.subR, button.subG, button.subB = 0.6, 0, 0
end
button.subtext:SetTextColor(button.subR, button.subG, button.subB)
end
button.serviceIndex = index
button:Show()
else
button.serviceIndex = nil
button:UnlockHighlight()
button:SetText("")
button.subtext:SetText("")
button:SetNormalTexture("")
button.foldHighlight:SetTexture("")
button:Hide()
end
end
-- Selection highlight + detail (ref l.146-186). The glow rides the selected SERVICE row while
-- it is inside the visible window; a header selection shows no glow. The detail fills only for
-- a selected service.
local selection = GetTrainerSelectionIndex()
local selType
if selection >= 1 and selection <= numServices then
_, _, selType = GetTrainerServiceInfo(selection)
end
if selType and selType ~= "header"
and selection > skillOffset and selection <= skillOffset + BENILLA_TRAINER_ROWS then
local selButton = getglobal("BenillaTrainerService" .. (selection - skillOffset))
local hl = getglobal("BenillaTrainerSkillHighlight")
local tex = getglobal("BenillaTrainerSkillHighlightTexture")
if selType == "available" then
tex:SetVertexColor(0, 1.0, 0)
elseif selType == "used" then
tex:SetVertexColor(0.5, 0.5, 0.5)
else
tex:SetVertexColor(0.9, 0, 0)
end
hl:SetPoint("TOPLEFT", selButton, "TOPLEFT", 0, 0)
hl:Show()
-- The whole selected row goes white, and it is the ref's own pair of moves (l.183-184):
-- LockHighlight for the NAME (the button's label, so the white comes from its
-- HighlightFont instance, not from a repaint that the next Update would undo) and an
-- explicit whiten for the SUBTEXT, which is a child FontString the lock cannot reach.
-- An earlier revision called the white name a deliberate divergence "the ref does not
-- do"; the ref does it, through the lock this window had no engine method for at the
-- time (decision 1605). subR/subG/subB go white too so a hover-then-leave keeps the
-- subtext white.
selButton:LockHighlight()
selButton.subtext:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
selButton.subR, selButton.subG, selButton.subB =
HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b
end
if selType and selType ~= "header" then
BenillaTrainer_ShowDetails(selection)
else
BenillaTrainer_HideDetails()
end
updateCollapseAllIcon(numServices)
end
-- The window's wheel handler (bubbled up the parent chain from a row) → one row via the faux kit.
function BenillaTrainerFrame_OnMouseWheel(delta)
BenillaFauxScrollFrame_OnMouseWheel(getglobal("BenillaTrainerListScrollFrame"), delta)
end
-- A row click: a header folds/unfolds its group (Collapse/ExpandTrainerSkillLine take the header's
-- display index); a service selects (ref ClassTrainerSkillButton_OnClick + SetSelection's header
-- branch). Left-button only.
function BenillaTrainerSkillButton_OnClick(button, mouseButton)
if mouseButton ~= "LeftButton" then return end
local index = button.serviceIndex
if not index then return end
local _, _, serviceType, isExpanded = GetTrainerServiceInfo(index)
if serviceType == "header" then
if isExpanded then
CollapseTrainerSkillLine(index)
else
ExpandTrainerSkillLine(index)
end
else
SelectTrainerService(index)
end
BenillaTrainerFrame_Update()
end
-- Hover recolors a service row's subtext to HIGHLIGHT, restoring its state colour on leave (ref
-- ClassTrainerSkillButton_OnEnter/OnLeave). Harmless over a header (its subtext is blank).
function BenillaTrainerSkillButton_OnEnter(button)
button.subtext:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
end
function BenillaTrainerSkillButton_OnLeave(button)
button.subtext:SetTextColor(button.subR or 0.5, button.subG or 0.5, button.subB or 0.5)
end
-- ── The Train button ─────────────────────────────────────────────────────────────────────────
-- Buy the selected service, then repaint (the row flips to "used" once the app pushes the fresh
-- snapshot). The tradeskill profession-confirm dialog is a later refinement.
function BenillaTrainerTrainButton_OnClick()
BuyTrainerService(GetTrainerSelectionIndex())
BenillaTrainerFrame_Update()
end
-- ── The state-filter dropdown (ref ClassTrainerFrameFilterDropDown_*) ────────────────────────
function BenillaTrainerFilterDropDown_OnLoad()
UIDropDownMenu_Initialize(BenillaTrainerFilterDropDown, BenillaTrainerFilterDropDown_Initialize)
UIDropDownMenu_SetText(FILTER_LABEL, BenillaTrainerFilterDropDown)
UIDropDownMenu_SetWidth(96, BenillaTrainerFilterDropDown)
end
function BenillaTrainerFilterDropDown_Initialize()
local function addRow(kind, label, code)
local info = {}
info.text = code .. label .. CODE_CLOSE
info.value = kind
info.func = BenillaTrainerFilterDropDown_OnClick
info.checked = GetTrainerServiceTypeFilter(kind) and 1 or nil
info.keepShownOnClick = 1
UIDropDownMenu_AddButton(info)
end
addRow("available", LABEL_AVAILABLE, GREEN_CODE)
addRow("unavailable", LABEL_UNAVAILABLE, RED_CODE)
addRow("used", LABEL_USED, GRAY_CODE)
end
-- A row's func fires with the row button as `this`, carrying the PRE-click state (this.value /
-- this.checked): the kit's UIDropDownMenuButton_OnClick runs the func FIRST and only then, for a
-- keepShownOnClick row, flips the check texture + this.checked itself. So the func's whole job is
-- to flip the ENGINE filter to the inverse of the pre-click check, reset scroll, and repaint —
-- the visible check is the kit's to move, and the next open re-derives every row from
-- GetTrainerServiceTypeFilter anyway.
--
-- It must NOT re-Initialize the open menu here. Doing so re-ran AddButton, which sets
-- `button.checked` (and the Check texture) from the fresh engine state — so the kit's flip
-- immediately afterwards inverted the ALREADY-correct row and put it back. The check never
-- appeared to move, and because `this.checked` then read stale-true forever, a filter switched
-- off could never be switched back on.
function BenillaTrainerFilterDropDown_OnClick()
-- The engine filter AND the saved global, together: the global is what survives the
-- restart and what BenillaTrainerFrame_ApplyFilter pushes back after the per-packet
-- reset, so writing only the engine would lose the toggle at the next list (the ref's
-- own setglobal pair, Blizzard_TrainerUI.lua:526-530).
local on = 0
if not this.checked then
on = 1
end
setglobal("TRAINER_FILTER_" .. strupper(this.value), on)
SetTrainerServiceTypeFilter(this.value, on)
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
BenillaTrainerFrame_Update()
end
-- ── The collapse-all tab ─────────────────────────────────────────────────────────────────────
-- Fold / unfold every group (Collapse/ExpandTrainerSkillLine(0), ref
-- ClassTrainerCollapseAllButton_OnClick).
function BenillaTrainerCollapseAllButton_OnClick()
local b = getglobal("BenillaTrainerCollapseAllButton")
if b.collapsed then
b.collapsed = nil
ExpandTrainerSkillLine(0)
else
b.collapsed = 1
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
CollapseTrainerSkillLine(0)
end
BenillaTrainerFrame_Update()
end
-- ── The NPC-session lifecycle (MerchantFrame.xml's own shape) ────────────────────────────────
function BenillaTrainerFrame_OnLoad(frame)
-- The NPC portrait: the "npc" unit token → the trainer's live model bake
-- (crates/benilla/src/portrait, InteractNpc) — MerchantFrame's own binding.
SetPortraitTexture(getglobal(frame:GetName() .. "Portrait"), "npc")
frame:RegisterEvent("TRAINER_SHOW")
frame:RegisterEvent("TRAINER_UPDATE")
frame:RegisterEvent("TRAINER_CLOSED")
frame:RegisterEvent("PLAYER_MONEY")
-- The faux-scroll frame repaints the list through this window's Update.
getglobal("BenillaTrainerListScrollFrame").updateFunc = BenillaTrainerFrame_Update
end
-- Push the three saved globals into the engine — the ref's ClassTrainerFrame <OnShow>
-- (Blizzard_TrainerUI.xml:531-533), and it is NOT redundant: the list builder rewrites the
-- engine's filter mask to its own default on every SMSG_TRAINER_LIST (byte-verified,
-- `0x4d75d9 mov ds:0xb73a1c,3`; decision 1128), so this is what puts the player's remembered
-- choice back. Called on show and at the top of TRAINER_SHOW — our repaint runs before
-- ShowUIPanel, so applying it only in OnShow would leave one frame painted under the reset.
function BenillaTrainerFrame_ApplyFilter()
SetTrainerServiceTypeFilter("available", TRAINER_FILTER_AVAILABLE)
SetTrainerServiceTypeFilter("unavailable", TRAINER_FILTER_UNAVAILABLE)
SetTrainerServiceTypeFilter("used", TRAINER_FILTER_USED)
end
function BenillaTrainerFrame_SetTitle(name)
if name and name ~= "" then
getglobal("BenillaTrainerTitleText"):SetText(name)
else
getglobal("BenillaTrainerTitleText"):SetText("Trainer")
end
end
function BenillaTrainerFrame_OnEvent(frame)
if event == "TRAINER_SHOW" then
-- Fresh session: top of the list, a first selection (ref ClassTrainerFrame_Show).
BenillaTrainerFrame_ApplyFilter()
BenillaTrainerFrame_SetTitle(arg1)
FauxScrollFrame_SetOffset(getglobal("BenillaTrainerListScrollFrame"), 0)
if GetTrainerSelectionIndex() == 0 then
selectFirstService()
end
BenillaTrainerFrame_Update()
ShowUIPanel(frame)
elseif event == "TRAINER_UPDATE" then
BenillaTrainerFrame_SetTitle(arg1)
-- Re-anchor the selection if it fell off the (possibly shorter) list.
if GetTrainerSelectionIndex() == 0 then
selectFirstService()
end
BenillaTrainerFrame_Update()
elseif event == "TRAINER_CLOSED" then
HideUIPanel(frame)
elseif event == "PLAYER_MONEY" then
BenillaMoney_Set("BenillaTrainerMoney", GetMoney())
-- A buy changes affordability without a TRAINER_UPDATE — repaint the detail's cost tint.
if BenillaTrainerFrame:IsVisible() then
BenillaTrainerFrame_Update()
end
end
end
function BenillaTrainerCloseButton_OnClick()
CloseTrainer()
HideUIPanel(getglobal("BenillaTrainerFrame"))
end
]]></Script>
<!--
One row (ref ClassTrainerSkillButtonTemplate): a 293×16 text row — the +/- fold slot at the left
(used for header rows, blanked per-row for services), the name, and the rank subtext beside it.
NO item icon (the list rows carry none; the spell icon is the DETAIL pane's). <OnLoad> wires the
child regions + the fold highlight onto `self` (the QuestLogFrame.xml row-template shape).
THE ROW IS A FLOW, NOT TWO COLUMNS (B253). The name carries **no width**, so it never wraps:
a FontString given an explicit width wraps at it, and a 16px row cannot grow — which is how
"Handstitched Leather Pants" came to draw two lines over the row beneath it. The reference's
own row (`ClassTrainerFrameTemplates.xml`, read off the player's chain, never transcribed)
makes the name the Button's `<ButtonText>` at width **0** and then SetPoints the subtext to
that text's RIGHT + 10 from Lua, per row — so a long name simply runs on and pushes its
"(Rank N)" along. We express the same law declaratively: no size on either string, the
subtext anchored to the name's right edge. The 160/188 fixed columns this replaces were
benilla's invention, and both halves of them were wrong — the cap wrapped, and the column
put the rank at a fixed x the reference never uses.
-->
<Button name="BenillaTrainerServiceTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Layers>
<Layer level="ARTWORK">
<!-- NO <Anchors> — Update SetPoints this to the row's own label every repaint, the
ref's own move (`skillSubText:SetPoint("LEFT", <row>Text, "RIGHT", 10, 0)`,
Blizzard_TrainerUI.lua l.157). A `relativeTo="$parentText"` here CANNOT work and
fails quietly: `<Layers>` is loader step 5 and the `<ButtonText>` is step 5b, so
the label does not exist yet and the anchor falls back to the row (the loader
does warn — `resolve_target`'s "does not resolve" line). The ref anchors it at
runtime for its own reason anyway: the rank must follow a name whose width is
only known once the name is measured (B253). -->
<FontString name="$parentSubText" inherits="GameFontNormalSmall" justifyH="LEFT"/>
</Layer>
</Layers>
<!-- THE SERVICE NAME IS THE BUTTON'S OWN ButtonText (the ref's `ClassTrainerSkill<n>Text`),
under the base's font TRIO — that is what turns a row white under the cursor and holds
the selected row white, with no script involved: `GameFontHighlight` is white, the engine
swaps the highlighted font instance in on hover, and `LockHighlight()` pins it for the
selection. It was a `$parentName` FontString until decision 1605, on a stale note that
`Button:SetTextColor`/`LockHighlight` had no engine method. Carries NO `<Size>` — B253's
flow row: the name measures to its own text and `$parentSubText` follows its right edge,
which is the reference's own `<ButtonText>`-at-width-0 plus per-row
`SetPoint("LEFT", <row>Text, "RIGHT", 10, 0)`. TradeSkillFrame.xml and CraftFrame.xml are
the twins (both fixed-column, so both keep an explicit width). -->
<ButtonText name="$parentText" justifyH="LEFT">
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="22" y="0"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>
<HighlightFont inherits="GameFontHighlight" justifyH="LEFT"/>
<DisabledFont inherits="GameFontDisable" justifyH="LEFT"/>
<!-- The +/- fold slot + its hover glow: meaningful for header rows (Update sets +/- there),
blanked for services. -->
<NormalTexture file="Interface\Buttons\UI-MinusButton-UP">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</NormalTexture>
<HighlightTexture name="$parentHighlight" file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</HighlightTexture>
<Scripts>
<OnLoad>
self.subtext = getglobal(self:GetName() .. "SubText")
self.foldHighlight = self:GetHighlightTexture()
</OnLoad>
<OnClick>BenillaTrainerSkillButton_OnClick(self, arg1)</OnClick>
<OnEnter>BenillaTrainerSkillButton_OnEnter(self)</OnEnter>
<OnLeave>BenillaTrainerSkillButton_OnLeave(self)</OnLeave>
</Scripts>
</Button>
<!--
A money coin slot (copied from MerchantFrame.xml's rig): BenillaMoney_Set fills three,
highest-denomination-first (ltr) or copper-first (rtl), shrinking each to its number + 13px coin.
-->
<Frame name="BenillaTrainerCoinTemplate" virtual="true">
<Size><AbsDimension x="13" y="13"/></Size>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
<!-- A panel action button (Train/Exit): the UIPanelButtonTemplate face, label via text=. -->
<Button name="BenillaTrainerPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="80" y="22"/></Size>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<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>
</Button>
<Frame name="BenillaTrainerFrame" hidden="true">
<!-- The real 384×512 window. No <Anchors> — ShowUIPanel SetPoints this to the left slot. -->
<Size><AbsDimension x="384" y="512"/></Size>
<Layers>
<!-- NPC portrait 60×60 TOPLEFT (7,-6) (ref), BACKGROUND so the window art's ring frames it.
Bound in OnLoad via SetPortraitTexture(region, "npc") — the trainer's live model bake. -->
<Layer level="BACKGROUND">
<Texture name="$parentPortrait">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<!-- The four quadrant slabs that ARE the window art (ref): 256-wide left halves, 128-wide
right halves, each 256 tall, pinned to their corner, whole-texture. -->
<Layer level="BORDER">
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-BotLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"/></Anchors></Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-BotRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"/></Anchors></Texture>
<!-- Title: the NPC's name, fed as arg1 of TRAINER_SHOW/UPDATE via the NameCache; the
static "Trainer" shows until the creature-name query lands. -->
<FontString name="BenillaTrainerTitleText" inherits="GameFontNormal" justifyH="CENTER" text="Trainer">
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
</FontString>
<!-- Greeting (GetTrainerGreetingText), GameFontHighlight LEFT/TOP, 260×26, TOPLEFT (76,-38). -->
<FontString name="BenillaTrainerGreetingText" inherits="GameFontHighlight" justifyH="LEFT" justifyV="TOP">
<Size><AbsDimension x="260" y="26"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="76" y="-38"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
<!-- The horizontal divider between the list and the detail pane (ref): 256+75 wide, 16 tall,
TOPLEFT (15,-275), sampled from UI-ClassTrainer-HorizontalBar by TexCoord. -->
<Layer level="ARTWORK">
<Texture name="BenillaTrainerHorizontalBar" file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Size><AbsDimension x="256" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="15" y="-275"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="1.0" top="0" bottom="0.25"/>
</Texture>
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-HorizontalBar">
<Size><AbsDimension x="75" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerHorizontalBar" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0" right="0.29296875" top="0.25" bottom="0.5"/>
</Texture>
</Layer>
<!-- The FIXED detail-pane FontStrings (ref DetailScrollChildFrame), flattened onto this
window's own Layers (the flat-layout gap). Blanked/filled by Hide/ShowDetails.
THE WIDTHS ARE THE REFERENCE'S, not ours (B253). A FontString wraps at whatever
width it is given, so an invented narrow one is a wrap bug waiting for a long name:
the detail name at 150 broke "Handstitched Leather Pants" over two lines. The
reference's detail child is 296 wide with its name/requirements at **244** and its
description at **290**; our flattened anchors put the name at the same window x (68)
the child's 46 resolves to, so the same numbers land in the same place — and, unlike
the 290/300 they replace, they stay inside the pane instead of running past its
right edge at 358/330. -->
<Layer level="OVERLAY">
<FontString name="BenillaTrainerSkillName" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="244" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="68" y="-293"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaTrainerSkillRequirements" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size><AbsDimension x="244" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerSkillName" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaTrainerCostLabel" inherits="GameFontNormalSmall" justifyH="LEFT">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="30" y="-340"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="BenillaTrainerSkillDescription" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size><AbsDimension x="290" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerCostLabel" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- The scroll-wheel catcher (this file's header note) — MUST be first so every real row
below out-ranks it in hit-testing; it gives the list's EMPTY tail a hit-target whose
spin also bubbles to the window. -->
<Button name="BenillaTrainerWheelCatcher">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"/>
</Anchors>
<Scripts><OnMouseWheel>BenillaTrainerFrame_OnMouseWheel(arg1)</OnMouseWheel></Scripts>
</Button>
<!-- The top-right close X (ref a UIPanelCloseButton at TOPRIGHT (-29,-8); the minimize-button
art stands in). -->
<Button name="BenillaTrainerCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-29" y="-8"/></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>BenillaTrainerCloseButton_OnClick()</OnClick></Scripts>
</Button>
<!-- The expand/collapse-all tab (ref ClassTrainerExpandButtonFrame): a tab-shaped frame
carrying the collapse-all button — now functional (Collapse/ExpandTrainerSkillLine(0)). -->
<Frame name="BenillaTrainerExpandButtonFrame">
<Size><AbsDimension x="54" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="15" y="-70"/></Offset></Anchor></Anchors>
<Layers><Layer level="BACKGROUND">
<Texture name="BenillaTrainerExpandTabLeft" file="Interface\ClassTrainerFrame\UI-ClassTrainer-ExpandTab-Left"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors></Texture>
<Texture name="BenillaTrainerExpandTabMiddle" file="Interface\QuestFrame\UI-QuestLogSortTab-Middle"><Size><AbsDimension x="38" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="6"/></Offset></Anchor></Anchors></Texture>
<Texture file="Interface\QuestFrame\UI-QuestLogSortTab-Right"><Size><AbsDimension x="8" y="32"/></Size><Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerExpandTabMiddle" relativePoint="RIGHT"/></Anchors></Texture>
</Layer></Layers>
<Frames>
<Button name="BenillaTrainerCollapseAllButton" text="All">
<Size><AbsDimension x="40" y="22"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerExpandTabLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="3"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-MinusButton-Up"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors></NormalTexture>
<HighlightTexture file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD"><Size><AbsDimension x="16" y="16"/></Size><Anchors><Anchor point="LEFT" relativePoint="LEFT"/></Anchors></HighlightTexture>
<ButtonText><Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="18" y="0"/></Offset></Anchor></Anchors></ButtonText>
<NormalFont inherits="GameFontNormalSmall" justifyH="LEFT"/>
<Scripts><OnClick>BenillaTrainerCollapseAllButton_OnClick()</OnClick></Scripts>
</Button>
</Frames>
</Frame>
<!-- The state-filter dropdown (ref ClassTrainerFrameFilterDropDown): the UIDropDownMenu capsule
whose three keepShownOnClick check rows drive the engine's state-visibility filter —
replacing the three inline sort checkboxes. TOPRIGHT (-26,-64), the ref's spot. -->
<Frame name="BenillaTrainerFilterDropDown" inherits="UIDropDownMenuTemplate">
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-26" y="-64"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>BenillaTrainerFilterDropDown_OnLoad()</OnLoad>
<OnShow>BenillaTrainerFilterDropDown_OnLoad()</OnShow>
</Scripts>
</Frame>
<!-- The persistent selection highlight (ref ClassTrainerSkillHighlightFrame): 293×16,
ARTWORK-layer UI-Listbox-Highlight2. Declared BEFORE the rows so the row text paints OVER
the glow (same parent + level ⇒ declaration order is the z-tiebreak). Moved onto the
selected visible service row + vertex-colored by state. -->
<Frame name="BenillaTrainerSkillHighlight" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<Layers><Layer level="ARTWORK">
<Texture name="BenillaTrainerSkillHighlightTexture" file="Interface\Buttons\UI-Listbox-Highlight2">
<Anchors><Anchor point="TOPLEFT"/><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer></Layers>
</Frame>
<!-- 11 tree rows (ref ClassTrainerSkill1..11): 293×16, row1 TOPLEFT (22,-100); rows 2-11 chain
TOPLEFT→prior BOTTOMLEFT with no gap. -->
<Button name="BenillaTrainerService1" inherits="BenillaTrainerServiceTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-100"/></Offset></Anchor></Anchors>
</Button>
<Button name="BenillaTrainerService2" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService3" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService4" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService5" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService6" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService7" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService8" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService9" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService10" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaTrainerService11" inherits="BenillaTrainerServiceTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaTrainerService10" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- The faux-scroll frame (FauxScrollFrameTemplate, ScrollTemplates.xml): invisible
container over the 11 rows; only its right-hand Slider scroll bar renders. Sized to the
11-row span so the bar sits in the window's right gutter. updateFunc is set in
BenillaTrainerFrame_OnLoad. -->
<Frame name="BenillaTrainerListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="176"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-100"/></Offset></Anchor></Anchors>
</Frame>
<!-- The detail spell icon (ref ClassTrainerSkillIcon): a 37×37 button over a 64×64 UI-EmptySlot
ring, at the detail area's top-left. Icon via SetNormalTexture; Hide()n when unselected.
The hover is the ref's own, verbatim (Blizzard_TrainerUI.xml:452-459): owner is the icon
button, anchor ANCHOR_RIGHT, subject `ClassTrainerFrame.selectedService` — the SELECTED
row, which is why the tooltip describes the selection no matter which row the mouse
passed over on its way here. GetTrainerSelectionIndex() is our engine-held equivalent of
that field; it reads 0 with nothing selected, and SetTrainerService no-ops on 0. This is
the ONLY SetTrainerService call site in the shipped client — the list rows carry no
tooltip of their own (their template's OnEnter only recolours the subtext). -->
<Button name="BenillaTrainerSkillIcon" hidden="true">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="26" y="-291"/></Offset></Anchor></Anchors>
<Layers><Layer level="BACKGROUND">
<Texture file="Interface\Buttons\UI-EmptySlot"><Size><AbsDimension x="64" y="64"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="-13" y="13"/></Offset></Anchor></Anchors></Texture>
</Layer></Layers>
<Scripts>
<OnEnter>
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetTrainerService(GetTrainerSelectionIndex())
GameTooltip:Show()
</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
</Scripts>
</Button>
<!-- The detail cost coins (ref ClassTrainerDetailMoneyFrame), left-to-right off the "Cost:"
label; BenillaMoney_Set("BenillaTrainerDetailMoney", cost, true) fills them. Coin1's LEFT
pins to the label's RIGHT (not a magic absolute x): each coin auto-shrinks to numW+13 and
its number left-aligns at this LEFT edge, growing rightward — so the number abuts the
":" with a fixed 4px gap regardless of the amount's digit count (the ref's cost-label →
money-frame anchoring; the old absolute x reserved a wide 1-vs-2-digit gap). -->
<Frame name="BenillaTrainerDetailMoneyCoin1" inherits="BenillaTrainerCoinTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerCostLabel" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTrainerDetailMoneyCoin2" inherits="BenillaTrainerCoinTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerDetailMoneyCoin1" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTrainerDetailMoneyCoin3" inherits="BenillaTrainerCoinTemplate">
<Anchors><Anchor point="LEFT" relativeTo="BenillaTrainerDetailMoneyCoin2" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
</Frame>
<!-- The player purse (ref ClassTrainerMoneyFrame): the rightmost coin (Coin1) pins its
BOTTOMRIGHT to the window BOTTOMLEFT + (180, 86); Coin2/Coin3 chain left. Filled
copper-first by BenillaMoney_Set("BenillaTrainerMoney", GetMoney()). -->
<Frame name="BenillaTrainerMoneyCoin1" inherits="BenillaTrainerCoinTemplate">
<Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="180" y="86"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTrainerMoneyCoin2" inherits="BenillaTrainerCoinTemplate">
<Anchors><Anchor point="RIGHT" relativeTo="BenillaTrainerMoneyCoin1" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="BenillaTrainerMoneyCoin3" inherits="BenillaTrainerCoinTemplate">
<Anchors><Anchor point="RIGHT" relativeTo="BenillaTrainerMoneyCoin2" relativePoint="LEFT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor></Anchors>
</Frame>
<!-- The Train button (ref ClassTrainerTrainButton): UIPanelButton face + "Train", 80×22, in
the detail area's bottom-right. Enabled only for an affordable available service. -->
<Button name="BenillaTrainerTrainButton" inherits="BenillaTrainerPanelButtonTemplate" text="Train">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="184" y="-409"/></Offset></Anchor></Anchors>
<Scripts><OnClick>BenillaTrainerTrainButton_OnClick()</OnClick></Scripts>
</Button>
<!-- The Exit button (ref ClassTrainerCancelButton): hides the window; OnHide fires CloseTrainer(). -->
<Button name="BenillaTrainerCancelButton" inherits="BenillaTrainerPanelButtonTemplate" text="Exit">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="265" y="-409"/></Offset></Anchor></Anchors>
<Scripts><OnClick>HideUIPanel(getglobal("BenillaTrainerFrame"))</OnClick></Scripts>
</Button>
</Frames>
<!-- The NPC-session lifecycle (MerchantFrame.xml's shape): OnHide calls CloseTrainer() — it ends
the client-side session whenever this window hides for ANY reason (ESC, close X, Exit,
TRAINER_CLOSED, or another left-slot window displacing it). The list wheel lives on the
WINDOW (the engine bubbles a spin up the parent chain, so a spin over a row reaches here). -->
<Scripts>
<OnLoad>BenillaTrainerFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaTrainerFrame_OnEvent(self)</OnEvent>
<OnMouseWheel>BenillaTrainerFrame_OnMouseWheel(arg1)</OnMouseWheel>
<OnShow>BenillaTrainerFrame_ApplyFilter(); PlaySound("igCharacterInfoOpen")</OnShow>
<OnHide>CloseTrainer(); PlaySound("igCharacterInfoClose")</OnHide>
</Scripts>
</Frame>
</Ui>