Files
benilla-octo/crates/benilla-app/assets/ui/ChatFrame.xml
T
SamandClaude Fable 5 4eeed067a1 landmarks and the Plaguelands towers on the map, no ultrawide crop
- the world map's landmarks: city icons and the Eastern Plaguelands towers, from AreaPOI.dbc through the reference's own three gates (level flags, exploration, world state) — the tower rows are one per (tower, owner, state), so the server flipping a world state swaps which icon exists; and the always-up PvP readout ("Towers Controlled: n") over a new WorldStateUI.dbc loader, the init clearing the table on a zone change as the client does
- the character-select scene holds its authored 4:3 view box: the diagonal-FOV relation was fed the display's aspect, so every extra pixel of width came out of height (1.55× zoom at 21:9, head and feet gone) — now width grows and height never shrinks; the reference does the same and is wrong on the same panel
- a trail particle is stored in world space and drawn raw: Sprint's trail was welded to the chest bone (turn and it swings, run and it wobbles) because the trail was faked by subtracting translation inside a bone-local store; the correction machinery deleted itself, gravity is plain world down, and the emitters' live attach frame is retired
- the spellbook can shrink: `SMSG_REMOVED_SPELL` had no handler, so a talent wipe took the points back and left every rank drawn; and the chat dock's hover driver rides a frame the dock cannot hide, so selecting the Combat Log no longer freezes the tab alphas

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

1429 lines
83 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 chat window, rebuilt on the reference's own structure (decision 0288 P3): the docked
General + Combat Log pair sharing ChatFrame1's rect, the hover-revealed tabs and
background (the classic transparent chat — chat-cache.txt ships window alpha 0, hover
fades to DEFAULT_CHATFRAME_ALPHA 0.25), the outside-left scroll column with the
blinking bottom button, and the reference edit box (three-piece border art + header)
below the frame. Geometry, art paths, timing constants, and behaviors are QUOTED from the
extracted patch-chain FloatingChatFrame.xml/.lua + ChatFrame.xml/.lua (the pin in 0288);
window management (drag/undock/resize/rename, the tab right-click options dropdown) is
chat SETTINGS — deliberately out (0288 §2).
The inbound feed (ui_chat::feed) AddMessages into ChatFrame1; the composer/router phase
(0288 P4) fans CHAT_MSG groups across both windows per the chat-cache defaults. The edit
box submits through SubmitChatInput → ui_chat::input's grammar; its live header machine
(ChatEdit_UpdateHeader) is 0288 P5 — until then the header shows the sticky default
"Say:" with static insets.
-->
<Script><![CDATA[
-- ── NUM_CHAT_WINDOWS — how many chat windows the UI declares ─────────────────────────
-- `NUM_CHAT_WINDOWS = 7` is ChatFrame.lua line 5, verbatim. It is a FrameXML constant, not
-- an engine one, and the number is NOT the engine's: the chat-cache and the per-window
-- struct array `0xb4fe50` (wow-re `chat-color-table.md`, stride 0x98) both hold **10**
-- windows, and FloatingChatFrame.xml duly declares ChatFrame1TabDockRegion..10 — but only
-- seven ChatFrames. 7 is the count an addon iterates.
--
-- Measured demand: **5 corpus addons, all genuinely distinct** (no replicated library —
-- the one clean count in this area). Two idioms, and they need different things from us:
-- getglobal("ChatFrame"..i) Outfitter, _LazyPig — _LazyPig indexes the result
-- UNGUARDED (`ChatFrame:IsVisible()`), so it dies at i=3
-- unless ChatFrame3..7 are real frames. They are, below.
-- GetChatWindowInfo(i) EnhTooltip, MikScrollingBattleText, Enchantrix — the
-- engine getter, `benilla_ui::script::chat_window`.
NUM_CHAT_WINDOWS = 7
-- ── SlashCmdList — the addon slash-command registry (decision 1195) ────────────────────
-- Declared HERE, not in Rust, because the 1.12 client declares it in ChatFrame.lua and the
-- captured `_G` attributes it `table framexml`. An addon's registration is three lines at
-- FILE SCOPE:
-- SLASH_MYADDON1 = "/myaddon"
-- SlashCmdList["MYADDON"] = function(msg) ... end
-- so a missing table is not a missing feature, it is `attempt to index global
-- 'SlashCmdList'` on the addon's very first chunk — 26 of a 218-addon corpus died there.
-- The dispatch walk is `benilla_ui::script::slash`, transcribing ChatEdit_ParseText's.
--
-- It starts EMPTY: benilla's own commands are dispatched host-side (decision 0881's table)
-- where the reference registers them here. That divergence is recorded in `slash.rs`.
SlashCmdList = {}
-- ── ChatTypeGroup / OtherMenuChatTypeGroups — declared, and deliberately EMPTY ────────
-- The reference builds `ChatTypeGroup` at ChatFrame.lua l.115-348: 68 entries mapping a
-- chat TYPE to the list of `CHAT_MSG_*` events that belong to it, which its
-- `ChatFrame_RegisterForMessages` walks to register events on a chat window.
--
-- Declared here for the same reason `SlashCmdList` above is: an addon REGISTERS ITS OWN
-- chat type at file scope, and a missing table is not a missing feature — it is
-- `attempt to index global 'ChatTypeGroup'` on the addon's very first chunk. CT_RaidAssist
-- does exactly that three lines apart (`CT_RaidAssist.lua:59` assigns
-- `ChatTypeGroup["CTRAID"]`, `:63` tinserts into `OtherMenuChatTypeGroups`), and it dies
-- there — taking `CT_RA_OnEvent` with it, which is why FOUR MORE corpus addons
-- (CT_RABossMods, FixCTGroups, FuBar_CTRaid, SmartRes) die on that function being nil.
-- One missing table, five addons.
--
-- **EMPTY, not populated, and that is the honest shape rather than a shortcut.** Two
-- reasons, in order of weight:
-- · The 68 entries are 233 lines of the reference's own Lua. We RUN the reference's
-- files where we can (1234) and write our own where we cannot; what we do not do is
-- paste its source into a repo that mirrors publicly.
-- · Nothing would read them. Measured across the 218-addon corpus with `grep -a`:
-- CT_RaidAssist is the ONLY addon that touches either table, and it only WRITES. Not
-- one addon reads a group out.
--
-- The divergence that leaves, stated rather than discovered: our chat frames do not
-- register through this table at all — `ui_chat` routes host-side, the same split
-- `SlashCmdList` records above. So an addon that adds a group gets no routing from it.
-- That is the `ChatFrame_OnEvent` architecture question, not this table's.
ChatTypeGroup = {}
OtherMenuChatTypeGroups = {}
-- ── ChatTypeInfo — the per-chat-type table addons read colors out of ──────────────────
-- Declared HERE because the 1.12 client declares it at the top of ChatFrame.lua, and it
-- is an ADDON-FACING table before it is ours: `local info = ChatTypeInfo["SAY"]` then
-- `frame:AddMessage(text, info.r, info.g, info.b, info.id)` is the standard way an addon
-- prints a colored line.
--
-- Measured, and the raw count is misleading enough to be worth writing down: 234 corpus
-- sites, but **192 of them are one line** — AceEvent-2.0's
-- `ChatTypeInfo and ChatTypeInfo.WHISPER and ChatTypeInfo.WHISPER.r`, a GUARDED readiness
-- probe that copy-pastes into 64 addons and cannot raise. The reads that actually index
-- the table are 42 sites in 6 addons (ChatLog, oRA2/Participant's RaidWarn, Glory-2.0's
-- two FuBar hosts, FuBar_HeyFu) — those are `attempt to index global 'ChatTypeInfo'`
-- without it, and they are the reason this exists.
--
-- Three columns, three sources:
-- sticky the reference's own literals (ChatFrame.lua l.10-114) — the five sticky types
-- are SAY/PARTY/RAID/GUILD/BATTLEGROUND, which `ui_chat::edit` already quotes.
-- id GetChatTypeIndex's 1-based index into the engine's 94-entry color registry
-- (wow-re `chat-color-table.md`, static table 0x804710) — the ref sets it in
-- ChatFrame_OnLoad, and AddMessage's 5th argument is what consumes it.
-- r,g,b that registry's byte defaults, /255 — the engine hands Lua 0..1 floats
-- (it re-normalizes with the f32 1/255 at 0x8026c8).
--
-- ONE DIVERGENCE, and it is a shortcut through a two-step: the reference's OnLoad writes
-- 1.0/1.0/1.0 into every entry and waits for the engine's UPDATE_CHAT_COLOR storm to fill
-- the real values in (ChatFrame.lua l.1349 is the only writer in the whole default UI).
-- benilla has no chat-color settings surface and nothing that fires that event, so the
-- values are seeded at their END STATE directly. `ui_chat::event::default_color` is the
-- same wow-re table rendered host-side for our own feed, and
-- `chat_type_info_matches_the_host_color_table` fails if the two ever drift.
--
-- REPLY and COMBAT_ERROR are the two keys FrameXML declares that the engine's registry
-- does NOT contain, so GetChatTypeIndex answers 0 for both and no UPDATE_CHAT_COLOR is
-- ever fired for either name. They do NOT share a fate, and the first version of this
-- block got that wrong by generalising:
-- COMBAT_ERROR nothing writes it, ever — id 0, white, on the real client too.
-- REPLY the UPDATE_CHAT_COLOR handler MIRRORS WHISPER INTO IT by hand
-- (ChatFrame.lua l.1357-1365: `if strupper(arg1) == "WHISPER" then
-- info = ChatTypeInfo["REPLY"] ... end`). So its end state is WHISPER's
-- FF80FF, which is what the /r header shows, and seeding it white was a
-- bug the comment above it was busy justifying.
-- CHANNEL1..CHANNEL10 are the boot-seeded extras (indices 95-104), each colored from the
-- live CHANNEL entry — FFC0C0.
ChatTypeInfo = {}
local function chatType(name, sticky, id, r, g, b)
ChatTypeInfo[name] = { sticky = sticky, id = id, r = r / 255, g = g / 255, b = b / 255 }
end
chatType("SAY", 1, 1, 255, 255, 255)
chatType("PARTY", 1, 2, 170, 170, 255)
chatType("RAID", 1, 3, 255, 127, 0)
chatType("GUILD", 1, 4, 64, 255, 64)
chatType("OFFICER", 0, 5, 64, 192, 64)
chatType("YELL", 0, 6, 255, 64, 64)
chatType("WHISPER", 0, 7, 255, 128, 255)
chatType("WHISPER_INFORM", 0, 8, 255, 128, 255)
chatType("REPLY", 0, 0, 255, 128, 255)
chatType("EMOTE", 0, 9, 255, 128, 64)
chatType("TEXT_EMOTE", 0, 10, 255, 128, 64)
chatType("SYSTEM", 0, 11, 255, 255, 0)
chatType("MONSTER_WHISPER", 0, 27, 179, 179, 179)
chatType("MONSTER_SAY", 0, 12, 255, 255, 159)
chatType("MONSTER_YELL", 0, 13, 255, 64, 64)
chatType("MONSTER_EMOTE", 0, 14, 255, 128, 64)
chatType("CHANNEL", 0, 15, 255, 192, 192)
chatType("CHANNEL_JOIN", 0, 16, 192, 128, 128)
chatType("CHANNEL_LEAVE", 0, 17, 192, 128, 128)
chatType("CHANNEL_LIST", 0, 18, 192, 128, 128)
chatType("CHANNEL_NOTICE", 0, 19, 192, 192, 192)
chatType("CHANNEL_NOTICE_USER", 0, 20, 192, 192, 192)
chatType("AFK", 0, 21, 255, 128, 255)
chatType("DND", 0, 22, 255, 128, 255)
chatType("IGNORED", 0, 23, 255, 0, 0)
chatType("SKILL", 0, 24, 85, 85, 255)
chatType("LOOT", 0, 25, 0, 170, 0)
chatType("COMBAT_ERROR", 0, 0, 255, 255, 255)
chatType("COMBAT_MISC_INFO", 0, 26, 128, 128, 255)
chatType("CHANNEL1", 0, 95, 255, 192, 192)
chatType("CHANNEL2", 0, 96, 255, 192, 192)
chatType("CHANNEL3", 0, 97, 255, 192, 192)
chatType("CHANNEL4", 0, 98, 255, 192, 192)
chatType("CHANNEL5", 0, 99, 255, 192, 192)
chatType("CHANNEL6", 0, 100, 255, 192, 192)
chatType("CHANNEL7", 0, 101, 255, 192, 192)
chatType("CHANNEL8", 0, 102, 255, 192, 192)
chatType("CHANNEL9", 0, 103, 255, 192, 192)
chatType("CHANNEL10", 0, 104, 255, 192, 192)
chatType("COMBAT_SELF_HITS", 0, 28, 255, 255, 255)
chatType("COMBAT_SELF_MISSES", 0, 29, 255, 255, 255)
chatType("COMBAT_PET_HITS", 0, 30, 255, 255, 255)
chatType("COMBAT_PET_MISSES", 0, 31, 255, 255, 255)
chatType("COMBAT_PARTY_HITS", 0, 32, 255, 255, 255)
chatType("COMBAT_PARTY_MISSES", 0, 33, 255, 255, 255)
chatType("COMBAT_FRIENDLYPLAYER_HITS", 0, 34, 255, 255, 255)
chatType("COMBAT_FRIENDLYPLAYER_MISSES", 0, 35, 255, 255, 255)
chatType("COMBAT_HOSTILEPLAYER_HITS", 0, 36, 255, 255, 255)
chatType("COMBAT_HOSTILEPLAYER_MISSES", 0, 37, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_SELF_HITS", 0, 38, 255, 47, 47)
chatType("COMBAT_CREATURE_VS_SELF_MISSES", 0, 39, 255, 47, 47)
chatType("COMBAT_CREATURE_VS_PARTY_HITS", 0, 40, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_PARTY_MISSES", 0, 41, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_CREATURE_HITS", 0, 42, 255, 255, 255)
chatType("COMBAT_CREATURE_VS_CREATURE_MISSES", 0, 43, 255, 255, 255)
chatType("COMBAT_FRIENDLY_DEATH", 0, 44, 255, 255, 255)
chatType("COMBAT_HOSTILE_DEATH", 0, 45, 255, 255, 255)
chatType("COMBAT_XP_GAIN", 0, 46, 111, 111, 255)
chatType("COMBAT_HONOR_GAIN", 0, 82, 224, 202, 10)
chatType("SPELL_SELF_DAMAGE", 0, 47, 255, 255, 0)
chatType("SPELL_SELF_BUFF", 0, 48, 255, 255, 0)
chatType("SPELL_PET_DAMAGE", 0, 49, 255, 255, 255)
chatType("SPELL_PET_BUFF", 0, 50, 255, 255, 255)
chatType("SPELL_PARTY_DAMAGE", 0, 51, 255, 255, 255)
chatType("SPELL_PARTY_BUFF", 0, 52, 255, 255, 255)
chatType("SPELL_FRIENDLYPLAYER_DAMAGE", 0, 53, 255, 255, 255)
chatType("SPELL_FRIENDLYPLAYER_BUFF", 0, 54, 255, 255, 255)
chatType("SPELL_HOSTILEPLAYER_DAMAGE", 0, 55, 255, 255, 255)
chatType("SPELL_HOSTILEPLAYER_BUFF", 0, 56, 255, 255, 255)
chatType("SPELL_CREATURE_VS_SELF_DAMAGE", 0, 57, 202, 76, 217)
chatType("SPELL_CREATURE_VS_SELF_BUFF", 0, 58, 255, 255, 255)
chatType("SPELL_CREATURE_VS_PARTY_DAMAGE", 0, 59, 255, 255, 255)
chatType("SPELL_CREATURE_VS_PARTY_BUFF", 0, 60, 255, 255, 255)
chatType("SPELL_CREATURE_VS_CREATURE_DAMAGE", 0, 61, 255, 255, 255)
chatType("SPELL_CREATURE_VS_CREATURE_BUFF", 0, 62, 255, 255, 255)
chatType("SPELL_TRADESKILLS", 0, 63, 255, 255, 255)
chatType("SPELL_DAMAGESHIELDS_ON_SELF", 0, 64, 255, 255, 255)
chatType("SPELL_DAMAGESHIELDS_ON_OTHERS", 0, 65, 255, 255, 255)
chatType("SPELL_AURA_GONE_SELF", 0, 66, 255, 255, 255)
chatType("SPELL_AURA_GONE_PARTY", 0, 67, 255, 255, 255)
chatType("SPELL_AURA_GONE_OTHER", 0, 68, 255, 255, 255)
chatType("SPELL_ITEM_ENCHANTMENTS", 0, 69, 255, 255, 255)
chatType("SPELL_BREAK_AURA", 0, 70, 255, 255, 255)
chatType("SPELL_PERIODIC_SELF_DAMAGE", 0, 71, 255, 255, 255)
chatType("SPELL_PERIODIC_SELF_BUFFS", 0, 72, 255, 255, 255)
chatType("SPELL_PERIODIC_PARTY_DAMAGE", 0, 73, 255, 255, 255)
chatType("SPELL_PERIODIC_PARTY_BUFFS", 0, 74, 255, 255, 255)
chatType("SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE", 0, 75, 255, 255, 255)
chatType("SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS", 0, 76, 255, 255, 255)
chatType("SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", 0, 77, 255, 255, 255)
chatType("SPELL_PERIODIC_HOSTILEPLAYER_BUFFS", 0, 78, 255, 255, 255)
chatType("SPELL_PERIODIC_CREATURE_DAMAGE", 0, 79, 255, 255, 255)
chatType("SPELL_PERIODIC_CREATURE_BUFFS", 0, 80, 255, 255, 255)
chatType("SPELL_FAILED_LOCALPLAYER", 0, 81, 255, 255, 255)
chatType("BG_SYSTEM_NEUTRAL", 0, 83, 255, 120, 10)
chatType("BG_SYSTEM_ALLIANCE", 0, 84, 0, 174, 239)
chatType("BG_SYSTEM_HORDE", 0, 85, 255, 0, 0)
chatType("COMBAT_FACTION_CHANGE", 0, 86, 128, 128, 255)
chatType("MONEY", 0, 87, 255, 255, 0)
chatType("RAID_LEADER", 0, 88, 255, 219, 183)
chatType("RAID_WARNING", 0, 89, 255, 219, 183)
chatType("RAID_BOSS_EMOTE", 0, 91, 255, 219, 183)
chatType("FILTERED", 0, 92, 255, 0, 0)
chatType("BATTLEGROUND", 1, 93, 255, 127, 0)
chatType("BATTLEGROUND_LEADER", 0, 94, 255, 219, 183)
-- ── BenillaFCF: the FloatingChatFrame behaviors, re-expressed (ref FloatingChatFrame.lua)
-- Quoted constants: CHAT_TAB_SHOW_DELAY=0.2, CHAT_FRAME_FADE_TIME=0.15,
-- DEFAULT_CHATFRAME_ALPHA=0.25, CHAT_BUTTON_FLASH_TIME=0.5,
-- MESSAGE_SCROLLBUTTON_INITIAL_DELAY=0, MESSAGE_SCROLLBUTTON_SCROLL_DELAY=0.05.
BenillaFCF = {
selected = 1, -- SELECTED_DOCK_FRAME (1 = General, 2 = Combat Log)
hover = false, -- cursor inside the dock's padded rect
hoverTime = 0, -- stationary-cursor accumulator (ref: moving resets it)
oldX = 0, oldY = 0,
reveal = 0, -- 0..1 master fade the tab/texture alphas scale by
revealTarget = 0,
-- What the apply block below last WROTE, so an unchanged frame writes nothing
-- (decision 1396). -1 is unreachable for both, so the first pass always applies.
appliedReveal = -1,
appliedSelected = -1,
tabsSettled = false, -- both tabs' host-measured label widths have landed
flashTab = 0, -- tab id whose whisper flash runs (0 = none), 0288 P4 wires it
flashLeft = 0, -- remaining flash seconds (ref: 60s, blink 0.25/0.25)
}
-- The per-window texture sets the hover fade drives (ref CHAT_FRAME_TEXTURES: the
-- background + the 8 border pieces; benilla authors the borders as plain textures —
-- resizing is settings, the hover LOOK is not). Global: the load-time tint below needs it.
function BenillaFCF_Textures(id)
local n = "ChatFrame" .. id
local list = {}
for _, suffix in ipairs({
"Background",
"BorderTopLeft", "BorderTopRight", "BorderBottomLeft", "BorderBottomRight",
"BorderTop", "BorderBottom", "BorderLeft", "BorderRight",
}) do
local t = getglobal(n .. suffix)
if t then table.insert(list, t) end
end
return list
end
-- FCF_SelectDockFrame: show the selected frame, hide the other; selected tab full alpha,
-- unselected half (both still scaled by the reveal fade below). Stops the selected tab's
-- whisper flash (ref FCF_SelectDockFrame → UIFrameFlashRemoveFrame). The tab getglobal
-- stays guarded: both dock tabs exist (1575), but the guard also covers ChatFrame3..7,
-- which have no tabs at all and which the error above is what actually keeps out.
function BenillaFCF_SelectDock(id)
BenillaFCF.selected = id
for i = 1, 2 do
local frame = getglobal("ChatFrame" .. i)
if i == id then frame:Show() else frame:Hide() end
end
if BenillaFCF.flashTab == id then
BenillaFCF.flashTab = 0
local flash = getglobal("ChatFrame" .. id .. "TabFlash")
if flash then flash:Hide() end
end
end
-- FCF_SelectDockFrame(frame) — the reference's own name for the operation above, taking a
-- FRAME where ours takes a dock id. Both corpus callers pass DEFAULT_CHAT_FRAME:
--
-- if not DEFAULT_CHAT_FRAME:IsVisible() then
-- FCF_SelectDockFrame(DEFAULT_CHAT_FRAME)
-- end
-- DEFAULT_CHAT_FRAME:AddMessage(out)
--
-- verbatim in both CustomNameplates (`CustomNameplates.lua:69`) and Roid-Macros
-- (`Utility.lua:40`) — "un-hide the chat window before printing into it". Two addons,
-- two independent files, not a replicated library.
--
-- Ours does the dock half and NOT the rest of ref FCF_SelectDockFrame l.1204-1217: the ref
-- also assigns `SELECTED_DOCK_FRAME` (a global no corpus addon reads — 0 hits) and calls
-- `FCF_DockUpdate()` (tab geometry, which FCF_OnUpdate below re-derives every frame).
--
-- An UNDOCKED frame RAISES rather than quietly doing nothing. ChatFrame3..7 exist as real
-- frames but benilla has no undock/dock model at all (0288 §2 — window management is chat
-- settings, deliberately out), so "select ChatFrame5" has no meaning here; answering it
-- with silence would leave the caller's next AddMessage landing in a window that is still
-- hidden, which is the bug the guard was written to prevent.
function FCF_SelectDockFrame(frame)
if not frame then
error("FCF_SelectDockFrame: nil frame", 2)
end
local id = frame:GetID()
if id ~= 1 and id ~= 2 then
error("FCF_SelectDockFrame: " .. (frame:GetName() or "?")
.. " is not docked — benilla docks only ChatFrame1/ChatFrame2 (decision 0288 §2)", 2)
end
BenillaFCF_SelectDock(id)
end
function BenillaFCF_TabClick(id)
-- ref FCF_Tab_OnClick (docked, left click): select; UChatScrollButton per click.
BenillaFCF_SelectDock(id)
PlaySound("UChatScrollButton")
end
-- The whisper tab flash (ref FCF_FlashTab: never the selected tab; 0.25s on/off for 60s,
-- and a flashing tab holds the dock tabs visible). Fired by the app on a received
-- whisper landing in an unselected window (0288 P4). No-tab windows can't flash.
function BenillaFCF_FlashTab(id)
if id == BenillaFCF.selected then return end
if not getglobal("ChatFrame" .. id .. "TabFlash") then return end
BenillaFCF.flashTab = id
BenillaFCF.flashLeft = 60
end
-- PanelTemplates_TabResize(5, tab), re-expressed: the ref sizes every docked tab to its
-- label (FCF_DockUpdate) — middle slice = text width + 5, tab = middle + the two 16px
-- caps; the flash/highlight bands follow the Left/Right pieces by anchor. The label
-- width is host-measured (GetStringWidth), so early frames may see 0. It RETURNS whether
-- the width is known yet — which is what lets FCF_OnUpdate's apply gate keep polling
-- until the measurement lands and then stop for good (decision 1396). Without that
-- answer the gate would have to choose between polling forever and shipping tabs stuck
-- at their authored width on whichever session measured late.
function BenillaFCF_TabResize(tab)
local w = getglobal(tab:GetName() .. "Text"):GetStringWidth()
if w == 0 then return false end
if tab.benillaTabTextW == w then return true end
tab.benillaTabTextW = w
getglobal(tab:GetName() .. "Middle"):SetWidth(w + 5)
tab:SetWidth(w + 5 + 32)
return true
end
-- The hover rect: the dock's rect expanded by the ref's MouseIsOver pads
-- (FCF_OnUpdate: MouseIsOver(chatFrame, 45, -10, -5, 5) — 45 up into the tab zone,
-- 10 down past the edit box seam, 5 out each side).
local function overDock(x, y)
-- **The SELECTED frame, not ChatFrame1.** The two dock windows are mutually exclusive
-- — selecting one hides the other — so reading a fixed frame here meant that the
-- instant the Combat Log was selected the hover test measured a hidden window, went
-- false, and drove the reveal fade to 0: the tabs and the chat background disappeared
-- together (director-reported). The dock's rect is whichever window is up.
local f = getglobal("ChatFrame" .. BenillaFCF.selected) or ChatFrame1
local left, right = f:GetLeft(), f:GetRight()
local top, bottom = f:GetTop(), f:GetBottom()
if not left then return false end
return x >= left - 5 and x <= right + 5 and y >= bottom - 10 and y <= top + 45
end
-- FCF_OnUpdate transcription (the parts that survive with window management out):
-- reveal after a 0.2s STATIONARY hover (a moving cursor keeps resetting the delay),
-- fade over 0.15s, conceal on leave; a flashing tab forces the tabs visible; the
-- selected window's textures ride the same fade toward alpha 0.25 (the chat-cache
-- default window alpha is 0 — the box only exists on hover).
function FCF_OnUpdate(elapsed)
local s = BenillaFCF
local x, y = GetCursorPosition()
if overDock(x, y) then
if not s.hover then
s.hover = true
s.hoverTime = 0
s.oldX, s.oldY = x, y
elseif s.oldX == x and s.oldY == y then
s.hoverTime = s.hoverTime + elapsed
else
s.hoverTime = 0
s.oldX, s.oldY = x, y
end
if s.hoverTime > 0.2 then s.revealTarget = 1 end
else
s.hover = false
s.hoverTime = 0
s.revealTarget = 0
end
if s.flashTab ~= 0 then s.revealTarget = 1 end
-- The 0.15s linear fade toward the target (ref UIFrameFadeIn/Out over
-- CHAT_FRAME_FADE_TIME).
if s.reveal < s.revealTarget then
s.reveal = math.min(s.revealTarget, s.reveal + elapsed / 0.15)
elseif s.reveal > s.revealTarget then
s.reveal = math.max(s.revealTarget, s.reveal - elapsed / 0.15)
end
-- Apply: tabs at 1.0 (selected) / 0.5 (unselected) × reveal; the selected window's
-- background+borders at 0.25 × reveal (ref: fade oldAlpha↔DEFAULT_CHATFRAME_ALPHA).
--
-- GATED on everything the writes below depend on — the reveal, the selected tab, and
-- whether the label measurements have landed (decision 1396). **The reference needs
-- no gate because it does not do this here at all**: ref FCF_OnUpdate hands the fade
-- to UIFrameFadeIn/Out, and UIFrameFade_OnUpdate walks only the frames still fading,
-- so a settled dock costs nothing. This transcription inlined the ramp and lost that
-- property with it, and ~24 getglobals, two table builds and ~20 SetAlpha calls then
-- ran every frame of every session to write the values that were already there —
-- 36 us/frame, the largest single handler in the client (1395 measured it). The
-- guard restores the reference's own behaviour rather than inventing one.
if s.reveal ~= s.appliedReveal or s.selected ~= s.appliedSelected then
for i = 1, 2 do
local tab = getglobal("ChatFrame" .. i .. "Tab")
if tab then
tab:SetAlpha(((i == s.selected) and 1.0 or 0.5) * s.reveal)
end
for _, t in ipairs(BenillaFCF_Textures(i)) do
t:SetAlpha(0.25 * s.reveal)
end
end
s.appliedReveal = s.reveal
s.appliedSelected = s.selected
end
-- The tab widths are a SEPARATE trigger and get their own latch, deliberately: they
-- depend on a host measurement rather than on the fade, and folding them into the
-- gate above would tie the alphas to a width that can legitimately never arrive (an
-- empty label measures 0 forever) — the apply block would then run every frame again
-- for a reason having nothing to do with what it writes. Polls until both land, then
-- stops for good.
if not s.tabsSettled then
local settled = true
for i = 1, 2 do
local tab = getglobal("ChatFrame" .. i .. "Tab")
if tab and not BenillaFCF_TabResize(tab) then settled = false end
end
s.tabsSettled = settled
end
-- The whisper flash blink (0.25 on / 0.25 off, 60s — ref UIFrameFlash args).
if s.flashTab ~= 0 then
s.flashLeft = s.flashLeft - elapsed
local flash = getglobal("ChatFrame" .. s.flashTab .. "TabFlash")
if s.flashLeft <= 0 then
s.flashTab = 0
flash:Hide()
else
local phase = mod(s.flashLeft, 0.5)
if phase > 0.25 then flash:Show() else flash:Hide() end
end
end
end
-- ChatFrame_OnUpdate transcription: the bottom button's blink while scrolled up
-- (CHAT_BUTTON_FLASH_TIME = 0.5s toggle; hidden the moment the view is AtBottom).
function ChatFrame_OnUpdate(elapsed)
local frame = this
local flash = getglobal(frame:GetName() .. "BottomButtonFlash")
if not flash then return end
if frame:AtBottom() then
-- The reference's own gate (ChatFrame.lua): a settled view writes nothing. Our
-- flashTimer reset rides inside it — so a return-to-bottom during a hidden blink
-- phase keeps the residual phase (a divergence that restarts the blink mid-phase
-- next scroll-up, invisible at rest and costing zero writes there).
if flash:IsVisible() then
flash:Hide()
frame.flashTimer = 0
end
return
end
frame.flashTimer = (frame.flashTimer or 0) + elapsed
while frame.flashTimer >= 0.5 do
frame.flashTimer = frame.flashTimer - 0.5
if flash:IsVisible() then flash:Hide() else flash:Show() end
end
end
-- A completed left click on the chat body dismisses a held SPELL/ACTION payload
-- (decision 0843, the director's call — no ref counterpart: the reference keeps a
-- non-item payload on any frame click). Items are deliberately excluded: an item
-- dismissal is a destroy, which only the world-drop popup flow may offer.
function BenillaChatFrame_DismissCursor(button)
if button ~= "LeftButton" then return end
local kind = GetCursorInfo()
if kind == "spell" or kind == "action" then
ClearCursor()
end
end
-- MessageFrameScrollButton_OnLoad/_OnUpdate + the ref OnClick pair: a press arms the
-- hold-repeat (initial delay 0), each 0.05s while held scrolls WITHOUT the sound; the
-- release click scrolls once WITH the sound (ref FloatingChatFrame.xml's DownButton/
-- UpButton scripts — sound only on the non-pushed branch).
function BenillaChatScrollButton_OnClick(btn, down, dir, sound)
if down then
btn.clickDelay = 0
else
PlaySound(sound)
local frame = getglobal("ChatFrame" .. BenillaFCF.selected)
if dir == "up" then frame:ScrollUp() else frame:ScrollDown() end
end
end
function BenillaChatScrollButton_OnUpdate(btn, elapsed, dir)
if btn:GetButtonState() == "PUSHED" then
btn.clickDelay = (btn.clickDelay or 0) - elapsed
if btn.clickDelay < 0 then
local frame = getglobal("ChatFrame" .. BenillaFCF.selected)
if dir == "up" then frame:ScrollUp() else frame:ScrollDown() end
btn.clickDelay = 0.05
end
end
end
-- ── BenillaUIMenu: the 1.12 UIMenu kit, re-expressed (ref UIMenu.lua — the chat menu's
-- row engine: fixed 16px rows in a tooltip-backdrop box, a 2 s no-hover timeout that
-- walks up parentMenu chains, nested submenus opened on row hover). Constants quoted.
-- Scoped here because the chat menu is its only consumer; a second consumer graduates
-- it to its own file.
BENILLA_UIMENU_BUTTON_HEIGHT = 16
BENILLA_UIMENU_BUTTON_WIDTH = 104
BENILLA_UIMENU_BORDER_HEIGHT = 12
BENILLA_UIMENU_BORDER_WIDTH = 12
BENILLA_UIMENU_NUMBUTTONS = 32
BENILLA_UIMENU_TIMEOUT = 2.0
function UIMenu_Initialize()
local menu = this
menu.numButtons = 0
menu.subMenu = ""
local name = menu:GetName()
for i = 1, BENILLA_UIMENU_NUMBUTTONS do
local button = getglobal(name .. "Button" .. i)
button:SetWidth(BENILLA_UIMENU_BUTTON_WIDTH)
button:SetHeight(BENILLA_UIMENU_BUTTON_HEIGHT)
button:Hide()
getglobal(button:GetName() .. "ShortcutText"):Hide()
end
menu:SetWidth(BENILLA_UIMENU_BUTTON_WIDTH + BENILLA_UIMENU_BORDER_WIDTH * 2)
menu:SetHeight(BENILLA_UIMENU_BORDER_HEIGHT * 2)
end
-- Rows past the button pool are dropped (the ref message()s; nothing we author overflows).
function UIMenu_AddButton(text, shortcut, func, nested)
local menu = this
local id = menu.numButtons + 1
if id > BENILLA_UIMENU_NUMBUTTONS then return end
menu.numButtons = id
local button = getglobal(menu:GetName() .. "Button" .. id)
if text then button:SetText(text) end
button.func = func
button.nested = nested
button:Show()
if shortcut then
local s = getglobal(button:GetName() .. "ShortcutText")
s:SetText(shortcut)
s:Show()
end
menu:SetHeight(id * BENILLA_UIMENU_BUTTON_HEIGHT + BENILLA_UIMENU_BORDER_HEIGHT * 2)
end
function UIMenu_OnShow()
this.timeleft = BENILLA_UIMENU_TIMEOUT
this.counting = 1
end
function UIMenu_OnUpdate(elapsed)
if this.counting == 1 then
local timeleft = (this.timeleft or BENILLA_UIMENU_TIMEOUT) - elapsed
if timeleft <= 0 then
this:Hide()
return
end
this.timeleft = timeleft
end
end
function UIMenu_StartCounting(menu)
menu.counting = 1
if menu.parentMenu then UIMenu_StartCounting(getglobal(menu.parentMenu)) end
end
function UIMenu_StopCounting(menu)
menu.counting = 0
menu.timeleft = BENILLA_UIMENU_TIMEOUT
if menu.parentMenu then UIMenu_StopCounting(getglobal(menu.parentMenu)) end
end
function UIMenuButton_OnLoad()
this:SetPoint("TOP", this:GetParent():GetName(), "TOP", 0,
-((this:GetID() - 1) * BENILLA_UIMENU_BUTTON_HEIGHT) - BENILLA_UIMENU_BORDER_HEIGHT)
end
function UIMenuButton_OnClick()
if this.func then this.func() end
this:GetParent():Hide()
PlaySound("UChatScrollButton")
end
function UIMenuButton_OnEnter()
local nested = this.nested
if nested then
local menu = getglobal(nested)
if not menu:IsVisible() then
local oldMenu = getglobal(this:GetParent().subMenu or "")
if oldMenu then oldMenu:Hide() end
this:GetParent().subMenu = nested
menu:SetPoint("BOTTOMLEFT", this:GetName(), "BOTTOMRIGHT", 10, -12)
menu:Show()
end
UIMenu_StopCounting(menu)
else
UIMenu_StopCounting(this:GetParent())
end
end
function UIMenuButton_OnLeave()
UIMenu_StartCounting(this:GetParent())
if this.nested then UIMenu_StartCounting(getglobal(this.nested)) end
end
-- ── The chat menu rows (ref ChatMenu_* in ChatFrame.lua, adapted): every type row
-- prefixes the box with the type's slash and lets the LIVE PARSE convert — the ref's
-- own Whisper row does literally `SetText("/w "..GetText())`, and routing all rows the
-- same way means the menu can never drift from the slash grammar the edit machine
-- owns. Labels/shortcuts quoted from GlobalStrings. Language (benilla has no language
-- model yet) and Macro (no macro frame) are deliberately out; this OnLoad is their
-- return path.
function BenillaChatMenu_Type(prefix)
local eb = ChatFrameEditBox
eb:Show()
eb:SetFocus()
eb:SetText(prefix .. (eb:GetText() or ""))
end
function ChatMenu_OnLoad()
UIMenu_Initialize()
UIMenu_AddButton("Say", "/s", function() BenillaChatMenu_Type("/s ") end)
UIMenu_AddButton("Party Chat", "/p", function() BenillaChatMenu_Type("/p ") end)
UIMenu_AddButton("Guild Chat", "/g", function() BenillaChatMenu_Type("/g ") end)
UIMenu_AddButton("Yell", "/y", function() BenillaChatMenu_Type("/y ") end)
UIMenu_AddButton("Whisper", "/w", function() BenillaChatMenu_Type("/w ") end)
UIMenu_AddButton("Emote", "/e", function() BenillaChatMenu_Type("/e ") end, "EmoteMenu")
UIMenu_AddButton("Reply", "/r", function() BenillaChatMenu_Type("/r ") end)
UIMenu_AddButton("Voice Emote", nil, nil, "VoiceMacroMenu")
end
-- An emote row submits its /command through the REAL chat pipeline (SubmitChatInput →
-- the router's EmotesText resolve + the send-side posture gate) — the ref's DoEmote by
-- token, reached through our one grammar. The lists are the ref's EmoteList /
-- TextEmoteSpeechList tokens as OUR canonical /commands (the EmotesText names the typed
-- grammar resolves), CMD1-sorted like the ref menu. Three ref labels differ
-- (/attacktarget /followme /silly are GlobalStrings CMD aliases the typed grammar
-- doesn't carry yet) — each row shows the command it actually runs.
BenillaEmoteMenuList = {
"/applaud", "/beg", "/bow", "/chicken", "/cry", "/dance", "/eat", "/flex",
"/kiss", "/kneel", "/laugh", "/point", "/roar", "/rude", "/salute", "/shy",
"/sit", "/sleep", "/stand", "/talk", "/wave",
}
BenillaVoiceMacroList = {
"/attackmytarget", "/bye", "/charge", "/cheer", "/congratulate", "/flee",
"/flirt", "/follow", "/healme", "/hello", "/helpme", "/incoming", "/joke",
"/no", "/nod", "/oom", "/openfire", "/rasp", "/thank", "/train", "/wait",
"/welcome",
}
function BenillaEmoteMenu_Click(cmd)
SubmitChatInput(cmd)
ChatMenu:Hide()
end
function BenillaOnEmoteMenuLoad(list)
UIMenu_Initialize()
this.parentMenu = "ChatMenu"
for _, cmd in ipairs(list) do
local c = cmd
UIMenu_AddButton(cmd, nil, function() BenillaEmoteMenu_Click(c) end)
end
end
]]></Script>
<!-- ── The dock: ChatFrame1 (General) + ChatFrame2 (Combat Log) sharing its rect ─────── -->
<!--
**One declaration, two windows.** The dock's two windows are the SAME widget seated
differently, and saying that twice is how three straight rounds of bugs got in: 1575 found
the Combat Log with no tab, 1579 found it with no rect and no border art, and 1588 found it
with no `OnUpdate` and no scroll column — each time because window 2 was a HAND-MIRRORED
partial copy of window 1 and the mirroring missed a part. The reference does not mirror: it
declares `FloatingChatFrameTemplate` `virtual="true"` and every `ChatFrameN` inherits it
(FloatingChatFrame.xml l.211, l.685-832). So do we, and the class of bug goes with it —
a part that exists for one docked window now cannot fail to exist for the other.
Per-instance is only what genuinely differs: the seat (ChatFrame1 is the managed-position
base — `UIPARENT_MANAGED_FRAME_POSITIONS["ChatFrame1"]`, baseY 85 + the bottom-stack
offsets, decision 0272 — and ChatFrame2 takes its rect from ChatFrame1's, which is what
being docked MEANS and why it must never carry a managed row of its own, 1579), the id,
and which of the two ships shown.
The chrome the template carries: displayDuration 120 / maxLines 128 (ref ChatFrameTemplate);
a background stretched -2/+2 x and +3/-6 y past the frame (ref $parentBackground anchors)
and the 8 border slices ringing it (the ref draws those through its resize buttons'
textures — benilla authors them as plain textures, the hover LOOK without the resize
mechanics); the scroll column; and the per-frame scripts. Everything starts alpha 0;
BenillaFCF fades it.
**`EXECUTE_CHAT_LINE` is gated to window 1 on purpose** — see the OnLoad below. It is the
one script here that is genuinely ChatFrame1's rather than the dock's, so the template
carries the gate rather than the registration.
-->
<ScrollingMessageFrame name="BenillaDockedChatFrameTemplate" virtual="true"
frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128">
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBackground" file="Interface\ChatFrame\ChatFrameBackground" alpha="0">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="-2" y="3"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="2" y="-6"/></Offset></Anchor>
</Anchors>
</Texture>
<!-- The ChatFrameBorder ring (ref ChatFrameBorder slices; texcoords quoted from
the ref resize buttons). Corners 16×16, edges stretched between them. -->
<Texture name="$parentBorderTopLeft" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentBackground"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="0.125"/>
</Texture>
<Texture name="$parentBorderTopRight" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativeTo="$parentBackground"><Offset><AbsDimension x="2" y="2"/></Offset></Anchor></Anchors>
<TexCoords left="0.75" right="1.0" top="0" bottom="0.125"/>
</Texture>
<Texture name="$parentBorderBottomLeft" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="BOTTOMLEFT" relativeTo="$parentBackground"><Offset><AbsDimension x="-2" y="-3"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.25" top="0.7265625" bottom="0.8515625"/>
</Texture>
<Texture name="$parentBorderBottomRight" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT" relativeTo="$parentBackground"><Offset><AbsDimension x="2" y="-3"/></Offset></Anchor></Anchors>
<TexCoords left="0.75" right="1.0" top="0.7265625" bottom="0.8515625"/>
</Texture>
<Texture name="$parentBorderTop" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentBorderTopLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentBorderTopRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.25" right="0.75" top="0" bottom="0.125"/>
</Texture>
<Texture name="$parentBorderBottom" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentBorderBottomLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentBorderBottomRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.25" right="0.75" top="0.7265625" bottom="0.8515625"/>
</Texture>
<Texture name="$parentBorderLeft" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="$parentBorderTopLeft" relativePoint="BOTTOM"/>
<Anchor point="BOTTOM" relativeTo="$parentBorderBottomLeft" relativePoint="TOP"/>
</Anchors>
<TexCoords left="0" right="0.25" top="0.125" bottom="0.7265625"/>
</Texture>
<Texture name="$parentBorderRight" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="$parentBorderTopRight" relativePoint="BOTTOM"/>
<Anchor point="BOTTOM" relativeTo="$parentBorderBottomRight" relativePoint="TOP"/>
</Anchors>
<TexCoords left="0.75" right="1.0" top="0.125" bottom="0.7265625"/>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- The scroll column, outside bottom-left (ref: BottomButton at (-32,-4), Down and
Up stacked above it, 2px overlaps). Sounds + hold-repeat per the ref scripts. -->
<Button name="$parentBottomButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-32" y="-4"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentFlash" file="Interface\ChatFrame\UI-ChatIcon-BlinkHilight" hidden="true" setAllPoints="true"/>
</Layer>
</Layers>
<Scripts>
<OnClick>
PlaySound("igChatBottom")
getglobal("ChatFrame" .. BenillaFCF.selected):ScrollToBottom()
</OnClick>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollEnd-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollEnd-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollEnd-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="$parentDownButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativeTo="$parentBottomButton" relativePoint="TOP"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnLoad>
<!-- All four transitions, ref-verbatim (MessageFrameScrollButton_OnLoad,
ChatFrame.lua:1583): the OnClick body reads no button, so a right-click
scrolls exactly like a left one, and the DOWN halves are what arm the
hold-repeat. -->
this:RegisterForClicks("LeftButtonDown", "LeftButtonUp", "RightButtonUp", "RightButtonDown")
</OnLoad>
<OnClick>
BenillaChatScrollButton_OnClick(this, arg2, "down", "igChatScrollDown")
</OnClick>
<OnUpdate>
BenillaChatScrollButton_OnUpdate(this, arg1, "down")
</OnUpdate>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="$parentUpButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativeTo="$parentDownButton" relativePoint="TOP"/>
</Anchors>
<Scripts>
<OnLoad>
<!-- All four, ref-verbatim — see the down button's note. -->
this:RegisterForClicks("LeftButtonDown", "LeftButtonUp", "RightButtonUp", "RightButtonDown")
</OnLoad>
<OnClick>
BenillaChatScrollButton_OnClick(this, arg2, "up", "igChatScrollUp")
</OnClick>
<OnUpdate>
BenillaChatScrollButton_OnUpdate(this, arg1, "up")
</OnUpdate>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollUp-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
</Frames>
<Scripts>
<!-- EXECUTE_CHAT_LINE — how a MACRO line reaches the chat grammar (0996, wow-re
`macro-execution-law.md` §4, VERIFIED). The engine's macro runner (`0x4f14e0`)
names no Lua function and walks no command table: per body line it fires
`FrameScript_SignalEvent(0x188, "%s", line)`, and `0x188` is EXECUTE_CHAT_LINE
(the registry slot `0xbe17b8` is written once, at `0x51b4ff`, with `0x852470`).
ChatFrame.lua:1343 is the handler — `editBox:SetText(arg1)` → `ChatEdit_SendText`
→ `ChatEdit_OnEscapePressed` — and ChatFrame1's OnLoad
(FloatingChatFrame.xml:700) is its ONLY registrant in the whole default UI, so
every macro line runs through THIS frame's box whichever one has focus. benilla's
SubmitChatInput stands in for ChatEdit_SendText: the grammar behind it is Rust
(0881), which is the only part of that chain we replace. -->
<OnLoad>
if this:GetID() == 1 then this:RegisterEvent("EXECUTE_CHAT_LINE") end
</OnLoad>
<OnEvent>
if event == "EXECUTE_CHAT_LINE" then SubmitChatInput(arg1) end
</OnEvent>
<OnUpdate>
ChatFrame_OnUpdate(arg1)
</OnUpdate>
<OnMouseWheel>
-- WoW wheel convention: +1 per notch up, -1 down. Up = older messages.
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnClick>
BenillaChatFrame_DismissCursor(arg1)
</OnClick>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame1" inherits="BenillaDockedChatFrameTemplate" id="1">
<Size><AbsDimension x="430" y="120"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="32" y="85"/></Offset>
</Anchor>
</Anchors>
</ScrollingMessageFrame>
<!-- ChatFrame2 "Combat Log": docked — it takes ChatFrame1's rect from these two anchors and
nothing else, ships hidden, and its tab (ChatFrame2Tab, below) selects it. Its message
groups are the chat-cache combat set, registered app-side (`ui_chat::frames`, 1571). -->
<ScrollingMessageFrame name="ChatFrame2" inherits="BenillaDockedChatFrameTemplate" hidden="true" id="2">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ChatFrame1" relativePoint="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="ChatFrame1" relativePoint="BOTTOMRIGHT"/>
</Anchors>
</ScrollingMessageFrame>
<!-- The dock's DRIVER. `FCF_OnUpdate` is dock-GLOBAL — one `reveal`, one selected tab, both
windows' tabs and textures — and it used to ride ChatFrame1's own OnUpdate. Selecting the
Combat Log hides ChatFrame1, a hidden frame gets no OnUpdate, and so the whole dock froze
the instant it was used: the tab alphas stuck showing General as selected (what the
director saw), the hover fade stuck at whatever it was, the tab-width settle stopped, and
the whisper flash stopped blinking. A driver must not be one of the things it drives.
So it rides a frame the dock cannot hide. Mounting it on BOTH windows instead would work
only while exactly one of them is ever visible — an invariant the dock happens to hold
today and that a second visible window would turn into a double-speed fade. The reference
offers no shape to copy here: 1.12.1 ships `FCF_OnUpdate` DEFINED and never referenced
(0 hits across all of FrameXML), because vanilla's tabs do not hover-fade at all — the
reveal is benilla's (0288), so its mounting point is ours to get right.
`ChatFrame_OnUpdate` stays on the windows, where it belongs: it reads `this` and blinks
that frame's own bottom button. -->
<Frame name="BenillaChatDockDriver" parent="UIParent">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnUpdate>
FCF_OnUpdate(arg1)
</OnUpdate>
</Scripts>
</Frame>
<!-- ── ChatFrame3..ChatFrame7: the five undocked windows ────────────────────────────────
The reference declares seven ScrollingMessageFrames (FloatingChatFrame.xml l.822-832,
`inherits="FloatingChatFrameTemplate"`), of which 3..7 ship HIDDEN and UNDOCKED — the
shipped chat-cache says so in as many words, and it is the state that matters here:
WINDOW 3 … 10: SIZE 0 / COLOR 0 0 0 0 / LOCKED 1 / DOCKED 0 / SHOWN 0
MESSAGES <empty> CHANNELS <empty> ZONECHANNELS 0
(`wow-5875-re/WoW/WTF/.../chat-cache.txt`, a real 1.12 character). So these are not
placeholders standing in for something richer — hidden, empty, unsubscribed IS what
ChatFrame3 is on a stock client, and an `AddMessage` into one lands in a ring nobody is
looking at, exactly as it does there.
**They are load-bearing for two different reasons, both measured:**
1. `_LazyPig/LazyPig.lua:1992` walks `for i = 1, NUM_CHAT_WINDOWS` and calls
`getglobal("ChatFrame"..i):IsVisible()` with NO nil guard — it dies at i=3 the moment
NUM_CHAT_WINDOWS exists without the frames behind it. Declaring the constant alone
would have *created* that crash.
2. `ChatFrame3` and `ChatFrame5` are named directly as debug sinks by five distinct
addons: IgniteStatus (7 unguarded `ChatFrame3:AddMessage` sites), TipBuddy, Radar
(the one guarded read), and FuBar_CorkFu + FuBar_oRA2CooldownFu, which store the frame
as AceDebug's `debugFrame`. A sixth "consumer", RosterLib-2.0's
`local function print(text) ChatFrame3:AddMessage(text) end`, is dead code replicated
into four folders — the count that looks like 12 files is 7 real sites.
No tabs (`ChatFrame3Tab`..`ChatFrame7Tab`) and no dock regions. The reference builds
both; here they would be furniture for a dock model we do not have (0288 §2), and
nothing reads them while these frames stay hidden and undocked. Outfitter.lua:3099 is
the one addon that would touch a tab — `local vTab = getglobal("ChatFrame"..i.."Tab")`
then `vTab:GetText()`, unguarded — but only INSIDE `if vChatFrame:IsVisible() or
vChatFrame.isDocked`, so a hidden, `isDocked`-less window never reaches it.
`isDocked` is not set on ChatFrame2 either, and the reason has CHANGED: it used to be
that window 2 had no tab for Outfitter to read, so claiming dockedness would have walked
it into a nil. 1575 built the tab, so that hazard is gone — what is left is that
`isDocked` belongs to a dock MODEL (dock/undock, FCF_DockUpdate's re-anchoring) that
0288 §2 holds out. Setting the flag would advertise a model we do not implement; our two
windows are docked by their anchors, which is the part that matters.
No background/border art: the ref's own defaults put every one of these at window alpha
0 (COLOR's fourth byte), and BenillaFCF only fades the dock's two. A shown ChatFrame5
renders its text over the world with no box — which is what the reference gives you. ── -->
<ScrollingMessageFrame name="ChatFrame3" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="3">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame4" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="4">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame5" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="5">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame6" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="6">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame7" frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" hidden="true" id="7">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
<Scripts>
<OnMouseWheel>
if arg1 > 0 then this:ScrollUp() else this:ScrollDown() end
</OnMouseWheel>
<OnHyperlinkClick>
SetItemRef(arg1, arg2, arg3)
</OnHyperlinkClick>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</ScrollingMessageFrame>
<!-- ── The tabs (ref ChatTabTemplate: 64×32, ChatFrameTab 3-slice quarters 16/44/16, label
at the left slice's right edge y −5, GameFontNormalSmall; the flash band at y −7; the
ref's ButtonText also carries a vestigial 50×8 Size that TabResize immediately
shrink-wraps — omitted). The middle slice hugs the label live (BenillaFCF_TabResize —
the ref's PanelTemplates_TabResize(5, tab) from FCF_DockUpdate). Hidden by alpha
(hover reveals).
ChatFrame2Tab ("Combat Log") is deliberately NOT built: the combat log renders nothing
real yet (window 2 gets its chat-cache groups from the router, but the combat-event →
line pipeline doesn't exist), so the tab just selected an empty window. ChatFrame2
itself stays above (the router keeps feeding it) so the tab can return whole with the
combat-log arc — director's call, 2026-07-11. The FCF loops guard the missing tab. ── -->
<Button name="ChatFrame1Tab" frameStrata="LOW" alpha="0">
<Size><AbsDimension x="70" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrame1Background" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0.0" right="0.25" top="0.0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="38" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.75" top="0.0" bottom="1.0"/>
</Texture>
<Texture name="$parentRight" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddle" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.75" right="1.0" top="0.0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Frame name="$parentFlash" hidden="true">
<Size><AbsDimension x="5" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="$parentRight"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD" setAllPoints="true"/>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnClick>
BenillaFCF_TabClick(1)
</OnClick>
</Scripts>
<ButtonText name="$parentText" text="GENERAL">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormalSmall"/>
<!-- The ref leaves this texture unsized and leans on the BLP's NATURAL height (32) to
seat the glow at the anchors' y −7 — centered on the tab's visible body, which sits
in the BOTTOM 22 rows of the ChatFrameTab canvas. benilla's v1 region model has no
natural-size law (an unpinned axis inherits the owner's edges, decision 0068), which
parked the glow 7 units high; the explicit height states what the ref reads from the
file. -->
<HighlightTexture file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD">
<Size><AbsDimension x="0" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="$parentRight"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
</Anchors>
</HighlightTexture>
</Button>
<!--
ChatFrame2Tab — the "Combat Log" dock tab. **This is the tab that was missing**: every
piece of machinery around it already existed and was written to expect it — `BenillaFCF`'s
fade, resize and flash loops all run `for i = 1, 2` over
`getglobal("ChatFrame"..i.."Tab")`, `selected = 1` is commented "2 = Combat Log", and
`FCF_SelectDockFrame` errors on any id that is not 1 or 2 — but the button itself was never
authored, and each of those loops has an `if tab then` guard that swallowed its absence
without a word. ChatFrame2 was therefore reachable only from Lua: it is `hidden`, as a
non-selected dock frame should be, and nothing on screen could select it.
Labels are the GlobalString KEYS (`GENERAL`, `COMBAT_LOG`), resolved out of the player's own
`GlobalStrings.lua` by the loader's ALL-CAPS key rule — the same shape the combat log's own
format strings use (1571), and the reason neither word is quoted in this file.
Anchored LEFT-to-RIGHT off tab 1; `BenillaFCF_TabResize` sizes both to their measured label
widths, so the abutment survives a locale whose words are longer.
-->
<Button name="ChatFrame2Tab" frameStrata="LOW" alpha="0">
<Size><AbsDimension x="70" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="ChatFrame1Tab" relativePoint="RIGHT"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0.0" right="0.25" top="0.0" bottom="1.0"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="38" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.25" right="0.75" top="0.0" bottom="1.0"/>
</Texture>
<Texture name="$parentRight" file="Interface\ChatFrame\ChatFrameTab">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddle" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.75" right="1.0" top="0.0" bottom="1.0"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Frame name="$parentFlash" hidden="true">
<Size><AbsDimension x="5" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="$parentRight"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD" setAllPoints="true"/>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnClick>
BenillaFCF_TabClick(2)
</OnClick>
</Scripts>
<ButtonText name="$parentText" text="COMBAT_LOG">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormalSmall"/>
<HighlightTexture file="Interface\PaperDollInfoFrame\UI-Character-Tab-Highlight" alphaMode="ADD">
<Size><AbsDimension x="0" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
<Anchor point="RIGHT" relativeTo="$parentRight"><Offset><AbsDimension x="0" y="-7"/></Offset></Anchor>
</Anchors>
</HighlightTexture>
</Button>
<!-- ── The UIMenu kit (ref UIMenu.xml, names kept for the addon surface): a row button
with a right-aligned shortcut string, and the tooltip-backdrop menu holding the ref's
fixed pool of 32 rows (hidden until AddButton). The label is authored LEFT-anchored:
the ref leans on the 1.12 ButtonText default rect spanning the button, which our lazy
label slot doesn't reproduce. ─────────────────────────────────────────────────── -->
<Button name="UIMenuButtonTemplate" virtual="true">
<Size><AbsDimension x="128" y="16"/></Size>
<Scripts>
<OnLoad>
UIMenuButton_OnLoad()
</OnLoad>
<OnClick>
UIMenuButton_OnClick()
</OnClick>
<OnEnter>
UIMenuButton_OnEnter()
</OnEnter>
<OnLeave>
UIMenuButton_OnLeave()
</OnLeave>
</Scripts>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentShortcutText" inherits="GameFontHighlight" hidden="true">
<Anchors><Anchor point="RIGHT"/></Anchors>
</FontString>
</Layer>
</Layers>
<ButtonText name="$parentText">
<Anchors><Anchor point="LEFT"/></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>
<HighlightFont inherits="GameFontHighlight" justifyH="LEFT"/>
<DisabledFont inherits="GameFontDisable" justifyH="LEFT"/>
</Button>
<Frame name="UIMenuTemplate" frameStrata="DIALOG" virtual="true">
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<BackgroundInsets><AbsInset left="4" right="4" top="4" bottom="4"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="13"/></EdgeSize>
</Backdrop>
<Frames>
<Button name="$parentButton1" inherits="UIMenuButtonTemplate" id="1"/>
<Button name="$parentButton2" inherits="UIMenuButtonTemplate" id="2"/>
<Button name="$parentButton3" inherits="UIMenuButtonTemplate" id="3"/>
<Button name="$parentButton4" inherits="UIMenuButtonTemplate" id="4"/>
<Button name="$parentButton5" inherits="UIMenuButtonTemplate" id="5"/>
<Button name="$parentButton6" inherits="UIMenuButtonTemplate" id="6"/>
<Button name="$parentButton7" inherits="UIMenuButtonTemplate" id="7"/>
<Button name="$parentButton8" inherits="UIMenuButtonTemplate" id="8"/>
<Button name="$parentButton9" inherits="UIMenuButtonTemplate" id="9"/>
<Button name="$parentButton10" inherits="UIMenuButtonTemplate" id="10"/>
<Button name="$parentButton11" inherits="UIMenuButtonTemplate" id="11"/>
<Button name="$parentButton12" inherits="UIMenuButtonTemplate" id="12"/>
<Button name="$parentButton13" inherits="UIMenuButtonTemplate" id="13"/>
<Button name="$parentButton14" inherits="UIMenuButtonTemplate" id="14"/>
<Button name="$parentButton15" inherits="UIMenuButtonTemplate" id="15"/>
<Button name="$parentButton16" inherits="UIMenuButtonTemplate" id="16"/>
<Button name="$parentButton17" inherits="UIMenuButtonTemplate" id="17"/>
<Button name="$parentButton18" inherits="UIMenuButtonTemplate" id="18"/>
<Button name="$parentButton19" inherits="UIMenuButtonTemplate" id="19"/>
<Button name="$parentButton20" inherits="UIMenuButtonTemplate" id="20"/>
<Button name="$parentButton21" inherits="UIMenuButtonTemplate" id="21"/>
<Button name="$parentButton22" inherits="UIMenuButtonTemplate" id="22"/>
<Button name="$parentButton23" inherits="UIMenuButtonTemplate" id="23"/>
<Button name="$parentButton24" inherits="UIMenuButtonTemplate" id="24"/>
<Button name="$parentButton25" inherits="UIMenuButtonTemplate" id="25"/>
<Button name="$parentButton26" inherits="UIMenuButtonTemplate" id="26"/>
<Button name="$parentButton27" inherits="UIMenuButtonTemplate" id="27"/>
<Button name="$parentButton28" inherits="UIMenuButtonTemplate" id="28"/>
<Button name="$parentButton29" inherits="UIMenuButtonTemplate" id="29"/>
<Button name="$parentButton30" inherits="UIMenuButtonTemplate" id="30"/>
<Button name="$parentButton31" inherits="UIMenuButtonTemplate" id="31"/>
<Button name="$parentButton32" inherits="UIMenuButtonTemplate" id="32"/>
</Frames>
<Scripts>
<OnShow>
UIMenu_OnShow()
PlaySound("UChatScrollButton")
this:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
this:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
</OnShow>
<OnUpdate>
UIMenu_OnUpdate(arg1)
</OnUpdate>
</Scripts>
</Frame>
<!-- ── The chat menu button (ref ChatFrameMenuButton): the bubble icon atop the scroll
column; click opens ChatMenu (ref ChatFrame_OpenMenu). ──────────────────────────── -->
<Button name="ChatFrameMenuButton" frameStrata="LOW">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="BOTTOM" relativeTo="ChatFrame1UpButton" relativePoint="TOP"/>
</Anchors>
<Scripts>
<OnClick>
PlaySound("igChatEmoteButton")
ChatMenu:Show()
</OnClick>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-Chat-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-Chat-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-Chat-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<!-- ChatMenu + its two nested menus (ref FloatingChatFrame.xml; the ref's own OnShow
hides only EmoteMenu — hiding both submenus is the same intent without the ref's
Voice-menu-left-open corner). The submenus are authored as children (the ref's
parent="ChatMenu" attr — this engine attaches by nesting, CharacterFrame.xml's note),
so hiding ChatMenu hides them with it. -->
<Frame name="ChatMenu" inherits="UIMenuTemplate" hidden="true">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrameMenuButton" relativePoint="TOPRIGHT"/>
</Anchors>
<Frames>
<Frame name="EmoteMenu" inherits="UIMenuTemplate" hidden="true">
<Scripts>
<OnLoad>
BenillaOnEmoteMenuLoad(BenillaEmoteMenuList)
</OnLoad>
</Scripts>
</Frame>
<Frame name="VoiceMacroMenu" inherits="UIMenuTemplate" hidden="true">
<Scripts>
<OnLoad>
BenillaOnEmoteMenuLoad(BenillaVoiceMacroList)
</OnLoad>
</Scripts>
</Frame>
</Frames>
<Scripts>
<OnLoad>
ChatMenu_OnLoad()
</OnLoad>
<OnShow>
UIMenu_OnShow()
EmoteMenu:Hide()
VoiceMacroMenu:Hide()
this:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b)
this:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
</OnShow>
</Scripts>
</Frame>
<!-- ── The edit box (ref ChatFrameEditBox): below ChatFrame1, -5/+5 wide, 32 tall, DIALOG
strata, hidden until ENTER opens it (ui_chat::input). The three-piece
UI-ChatInputBorder art; the header ("Say:" until the P5 edit machine drives it) at
LEFT+13; typed text starts past it (static insets, P5 makes them live). ─────────── -->
<!-- The ref template ships ignoreArrows="true": plain arrows fall through to CHARACTER
MOVEMENT while typing (Ctrl bypasses), because 1.12 lets you keep arrow-steering with
the box open. benilla has no typing-while-moving arrow scheme, so the attr would leave
the keys dead; the director's call is the modern law — plain arrows move the caret,
Up/Down recall history (decision 0301). The engine still honors the attr faithfully
for any box that sets it. -->
<!-- historyLines: the ref template ships 32; 64 at the director's call ("at least 50",
2026-07-26) — the recall walk is the modern law already (decision 0301). -->
<EditBox name="ChatFrameEditBox" frameStrata="DIALOG" hidden="true" historyLines="64" letters="255">
<Size><AbsDimension x="0" y="32"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="ChatFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="-5" y="-2"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="ChatFrame1" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="5" y="-2"/></Offset></Anchor>
</Anchors>
<TextInsets><AbsInset left="47" right="13" top="0" bottom="0"/></TextInsets>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentLeft" file="Interface\ChatFrame\UI-ChatInputBorder-Left">
<Size><AbsDimension x="256" y="32"/></Size>
<Anchors><Anchor point="LEFT"/></Anchors>
</Texture>
<Texture name="$parentRight" file="Interface\ChatFrame\UI-ChatInputBorder-Right">
<Size><AbsDimension x="16" y="32"/></Size>
<Anchors><Anchor point="RIGHT"/></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="1.0"/>
</Texture>
<Texture name="$parentMid" file="Interface\ChatFrame\UI-ChatInputBorder-Right">
<Size><AbsDimension x="1" y="32"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentRight" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0" right="0.9375" top="0" bottom="1.0"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentHeader" inherits="ChatFontNormal" text="Say:">
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="13" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnterPressed>
SubmitChatInput(this:GetText())
this:SetText("")
this:ClearFocus()
this:Hide()
</OnEnterPressed>
<OnEscapePressed>
this:SetText("")
this:ClearFocus()
this:Hide()
</OnEscapePressed>
<OnTabPressed>
BenillaChatTabPressed()
</OnTabPressed>
</Scripts>
<FontString inherits="ChatFontNormal" justifyH="LEFT"/>
</EditBox>
<!-- The load-order hack, like the ref's own trailing FCF_DockFrame(ChatFrame1, 1): select
the General tab once everything above exists — and tint the window chrome BLACK
(FCF_SetWindowColor: the ref vertex-colors every CHAT_FRAME_TEXTURES piece with the
chat-cache window COLOR, which ships 0 0 0; the ChatFrameBackground art itself is
white, so untinted it reads as a white wash over the world). -->
<Script><![CDATA[
-- `DEFAULT_CHAT_FRAME = ChatFrame1` — the reference's own line (ChatFrame.lua), and the
-- name half of FrameXML actually prints through. It sits here rather than in the header
-- script because a <Script> block runs where it stands: at the top of this file
-- ChatFrame1 does not exist yet. Measured demand: 3 of the 218 vanilla-corpus addons
-- die on `attempt to index global 'DEFAULT_CHAT_FRAME'` without it, and many more reach
-- it on a later path.
DEFAULT_CHAT_FRAME = ChatFrame1
BenillaFCF_SelectDock(1)
for i = 1, 2 do
for _, t in ipairs(BenillaFCF_Textures(i)) do
t:SetVertexColor(0, 0, 0)
end
end
]]></Script>
</Ui>