Files
benilla-octo/crates/benilla-app/assets/ui/ChatFrame.xml
T
SamandClaude Fable 5 027a2a14b4 clicks find their target, the booth eases off, and the map's lines go
- the world camera's pick regression: its off-screen render target carried `scale_factor: 1.0`, so on a Retina display the camera reported physical pixels as logical and a centred cursor cast its pick ray into the upper-left quadrant — unit and GameObject picks, the occlusion ray, chat bubbles and `world_to_viewport` all broke at once; the target now carries the window's real scale factor, re-stamped when dragging between displays changes it mid-session — and the perf pill's overlap dodge had the same disease in miniature (`GetBottom()` answers in WoW UI units, the pill lays out in window px), so it now answers a fraction of the screen and works at any window height
- `boothHalfRate` defaults back to half-rate: a body pane's off-screen booth pass rendered every frame reads +7.6 ms of GPU at 4K (~1.6 ms at 1600×900), on exactly the weak GPUs that felt the last snapshot — the doll's item effects keep full speed, since they key on the pacing regime rather than the camera's active bit, so only the 30 fps animation cadence returns, and full-rate stays one `SetCVar` away
- a spell cast by a GameObject arrives with caster guid 0 — vmangos writes the caster slot from a `Unit*` its GameObject constructor never sets — and the whole visual body was dropped at the guid lookup; the decode now reads an empty caster slot as the packet's first guid, which with no cast item IS the caster, so the Lightwell's click plays its impact kit (sound 3155 and the head flash) instead of only the aura glow that hid the miss, and the object stops being mislabelled a cast item
- the world map's black hairlines: a `SetTexCoord` crop is not a texture, and a POI icon magnified to 2.08 px per texel sampled half a texel past its crop into the neighbouring atlas cell — an opaque black row bleeding over a cell that is fully transparent (the empty cell is the reference's own zone-level substitution); `UiQuad::uv_clamp` now insets the sampling window half a texel per axis, so a crop renders as if its sub-rect were its own clamped texture, and `WOW_UI_PICK=x,y` names every quad under a pixel — it found this one in a single run

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

2227 lines
129 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)
-- ── The FloatingChatFrame constants, as the GLOBALS the reference declares ───────────
-- These were comments here until decision 1589 (B246): the Chat options page's *Remove
-- Chat Hover Delay* row writes the first two through `SetChatMouseOverDelay`, so they
-- have to be values something can move rather than numbers inlined in the fade. Names,
-- spellings and values are the reference's own (FloatingChatFrame.lua l.8-12), and two of
-- them — `CHAT_FRAME_FADE_TIME` and `CHAT_FRAME_TEXTURES` — are in the captured 1.12 `_G`
-- as `framexml` globals, so an addon can read them.
CHAT_TAB_SHOW_DELAY = 0.2
CHAT_FRAME_FADE_TIME = 0.15
DEFAULT_CHATFRAME_ALPHA = 0.25
DEFAULT_CHATFRAME_COLOR = { r = 0, g = 0, b = 0 }
-- Still quoted rather than declared (nothing writes them): CHAT_BUTTON_FLASH_TIME=0.5,
-- MESSAGE_SCROLLBUTTON_INITIAL_DELAY=0, MESSAGE_SCROLLBUTTON_SCROLL_DELAY=0.05.
-- REMOVE_CHAT_DELAY — the Chat options page's *Remove Chat Hover Delay* row (decision
-- 1589), a saved variable exactly as the reference has it (`UIOptionsFrameCheckButtons`
-- CheckButton18, `uvar = "REMOVE_CHAT_DELAY"`, `default = "0"`). Declared HERE because
-- this is the file that owns what it changes — a row whose defining file was not loaded
-- gets a nil `applyFunc` and is skipped, which is what keeps a partial harness honest
-- (OptionsFrame.xml's `OptionsFrame_ApplySavedSettings`). The `RegisterForSave` sits on
-- the line under the assignment, per 1128's load order.
REMOVE_CHAT_DELAY = "0"
RegisterForSave("REMOVE_CHAT_DELAY")
-- CHAT_LOCKED — the master "no chat window may be moved or resized" switch, the reference's
-- own saved variable. The reference declares it in `UIOptionsFrame.lua` l.96-97 (`"0"` plus
-- `RegisterForSave`) because that is where its *Lock Chat Settings* checkbox lives; ours is
-- declared here for the reason the row above is — the file that owns what it changes owns
-- the variable, and a row whose defining file was not loaded gets a nil `applyFunc` and is
-- skipped rather than half-applied.
--
-- **`"0"` — off.** Note the reference's own two-value disagreement: its checkbox table says
-- `default = "1"` (`UIOptionsFrame.lua:44`) while the variable is initialised `"0"` two
-- lines later, and the initialiser is what a fresh client actually runs. `"0"` here too.
-- The per-window lock (`FCF_SetLocked`, the tab menu's row) is the one that ships ON; this
-- one is the blanket override on top of it.
CHAT_LOCKED = "0"
RegisterForSave("CHAT_LOCKED")
-- The tab menu's lock row labels (GlobalStrings l.2590 / l.4448). Live text comes off the
-- player's own `GlobalStrings.lua`; these are the headless-test fallback this file already
-- uses everywhere else.
LOCK_WINDOW = LOCK_WINDOW or "Lock Window"
UNLOCK_WINDOW = UNLOCK_WINDOW or "Unlock Window"
-- The row's applyFunc. The reference has no such function because it calls
-- `SetChatMouseOverDelay(REMOVE_CHAT_DELAY)` inline from two places (its options load,
-- l.216, and the checkbox's own commit); ours needs a zero-argument name to hand the row,
-- and this is it — one line, in the file that owns both halves.
function ChatFrame_ApplyMouseOverDelay()
SetChatMouseOverDelay(REMOVE_CHAT_DELAY)
end
-- SetChatMouseOverDelay(noDelay) — ref FloatingChatFrame.lua l.1456-1464, verbatim in
-- effect: "1" collapses BOTH the stationary-hover delay and the fade to zero, so the box
-- appears the instant the cursor crosses it. The options row's `applyFunc` is the only
-- caller, and `UIOptionsFrame_Load` calls it at login for the same reason ours does.
function SetChatMouseOverDelay(noDelay)
if noDelay == "1" then
CHAT_TAB_SHOW_DELAY = 0
CHAT_FRAME_FADE_TIME = 0
else
CHAT_TAB_SHOW_DELAY = 0.2
CHAT_FRAME_FADE_TIME = 0.15
end
end
-- ── BenillaFCF: the FloatingChatFrame behaviors, re-expressed (ref FloatingChatFrame.lua)
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)
}
-- CHAT_FRAME_TEXTURES — the reference's own list (FloatingChatFrame.lua l.14-24), verbatim
-- in content and in spelling: the background plus the eight RESIZE GRIPS' textures. The
-- window's border art IS the grips, which is why one list drives both the hover fade
-- (`FCF_OnUpdate`) and the tint/alpha setters (`FCF_SetWindowColor`/`FCF_SetWindowAlpha`) —
-- the ring appears with the box and is invisible with it.
--
-- benilla used to author these as plain `$parentBorder*` textures ("the hover LOOK without
-- the resize mechanics"); the mechanics landed with the move/resize arc, so the suffixes
-- are the reference's again and the pieces are its buttons' children.
CHAT_FRAME_TEXTURES = {
"Background",
"ResizeTopLeftTexture", "ResizeTopRightTexture",
"ResizeBottomLeftTexture", "ResizeBottomRightTexture",
"ResizeTopTexture", "ResizeBottomTexture",
"ResizeLeftTexture", "ResizeRightTexture",
}
-- The live texture objects of one window's set. Global: the load-time tint below needs it.
function BenillaFCF_Textures(id)
local n = "ChatFrame" .. id
local list = {}
for _, suffix in ipairs(CHAT_FRAME_TEXTURES) do
local t = getglobal(n .. suffix)
if t then table.insert(list, t) end
end
return list
end
-- ── The per-window LOOK: the tint and the base alpha (decision 1589, B246) ───────────
-- `frame.oldAlpha` and the CHAT_FRAME_TEXTURES vertex colour in the reference; a pair of
-- constants here (alpha 0, black) until B246 asked for a way to change them. The store is
-- the engine's — `GetChatWindowInfo`/`SetChatWindowColor`/`SetChatWindowAlpha`, which is
-- what an addon reads too — and this table is the per-frame cache the fade reads each
-- frame instead of re-crossing that seam.
BenillaFCF_Look = {}
local function lookFor(id)
local l = BenillaFCF_Look[id]
if not l then
l = { r = 0, g = 0, b = 0, a = 0 }
BenillaFCF_Look[id] = l
end
return l
end
-- FCF_SetWindowColor(frame, r, g, b, doNotSave) — ref FloatingChatFrame.lua l.696-704.
-- The tint is applied here and now (a vertex colour is not part of the fade); the save is
-- what makes it outlive the session, and `doNotSave` is the reference's own flag for
-- "this value CAME from the store" (its `FloatingChatFrame_Update` passes it).
function FCF_SetWindowColor(frame, r, g, b, doNotSave)
local id = frame:GetID()
local l = lookFor(id)
l.r, l.g, l.b = r, g, b
for _, t in ipairs(BenillaFCF_Textures(id)) do
t:SetVertexColor(r, g, b)
end
if not doNotSave then
SetChatWindowColor(id, r, g, b)
end
end
-- FCF_SetWindowAlpha(frame, alpha, doNotSave) — ref l.706-716.
--
-- The reference writes the nine textures' alpha straight here; ours cannot, because the
-- hover fade below is the single writer of those alphas (decision 1396's latch is what
-- keeps an idle dock from rewriting them every frame). So this moves the BASE the fade
-- interpolates from and invalidates that latch, which makes the next frame's apply the
-- one write — the reference's own `frame.oldAlpha` bookkeeping, minus the double write.
function FCF_SetWindowAlpha(frame, alpha, doNotSave)
local id = frame:GetID()
lookFor(id).a = alpha
frame.oldAlpha = alpha -- ref: "Remember the alpha"
BenillaFCF.appliedReveal = -1
if not doNotSave then
SetChatWindowAlpha(id, alpha)
end
end
-- The alpha a window's chrome actually wears at a given point in the hover fade.
--
-- **The reference only fades a window that is DIMMER than the hover value**: both the fade
-- in and the fade out are guarded by `chatFrame.oldAlpha < DEFAULT_CHATFRAME_ALPHA`
-- (FloatingChatFrame.lua l.871 / l.913). So a player who has made their chat box solid
-- keeps it solid when the cursor leaves — the box does not dim itself back down — while
-- the shipped default (alpha 0) rides the full 0 → 0.25 ramp exactly as before.
local function windowAlpha(id, reveal)
local base = lookFor(id).a
if base >= DEFAULT_CHATFRAME_ALPHA then
return base
end
return base + (DEFAULT_CHATFRAME_ALPHA - base) * reveal
end
-- ── The lock, the move and the resize (ref FloatingChatFrame.lua) ────────────────────
--
-- Three gates decide whether a window may be dragged or resized, and the reference applies
-- all three at both verbs (`FCF_Resize` l.583-601, `FCF_Tab_OnClick`'s move arm l.1247-1256):
-- 1 · `FCF_Get_ChatLocked()` — the blanket `CHAT_LOCKED` saved variable;
-- 2 · `chatFrame.isLocked` — this window's own lock, the tab menu's row;
-- 3 · docked-and-not-the-default — a docked window's rect belongs to the DOCK, so only
-- `DEFAULT_CHAT_FRAME` may move it. ChatFrame2's anchors ARE ChatFrame1's corners:
-- resizing it directly would stretch it off the window it follows.
-- The eight grips' name suffixes, the reference's own (see `CHAT_FRAME_TEXTURES`).
local RESIZE_GRIPS = {
"TopLeft", "TopRight", "BottomLeft", "BottomRight",
"Top", "Bottom", "Left", "Right",
}
-- May this window be moved/resized at all? The three gates above, in one place because
-- both verbs and the lock's own plumbing ask the same question.
function FCF_CanMove(chatFrame)
if FCF_Get_ChatLocked() then return nil end
if chatFrame.isLocked then return nil end
if chatFrame.isDocked and chatFrame ~= DEFAULT_CHAT_FRAME then return nil end
return 1
end
-- Hand the window's EDGES and its BODY to the mouse, or take them back. benilla's own —
-- the reference needs nothing like it because its chat window is `enableMouse="false"` and
-- so competes for no click. Ours does (`OnClick`, `OnHyperlinkClick`), so a locked window
-- has to be exactly as click-through as it was before this arc: eight live grip buttons
-- would shadow the outer 16px of the window, and the bottom strip is where the newest chat
-- line — the one with the item link in it — is drawn.
function BenillaFCF_ApplyLock(chatFrame)
local free = FCF_CanMove(chatFrame)
local name = chatFrame:GetName()
for _, grip in ipairs(RESIZE_GRIPS) do
local b = getglobal(name .. "Resize" .. grip)
if b then b:EnableMouse(free and true or false) end
end
local tab = getglobal(name .. "Tab")
if free then
chatFrame:RegisterForDrag("LeftButton")
if tab then tab:RegisterForDrag("LeftButton") end
else
chatFrame:RegisterForDrag()
if tab then tab:RegisterForDrag() end
end
end
-- ref l.802-805, verbatim: the frame field and the engine store, together.
-- `BenillaFCF_ApplyLock` is ours, and it is what makes the flag do something structurally
-- rather than only in the two early returns below.
function FCF_SetLocked(chatFrame, isLocked)
chatFrame.isLocked = isLocked
SetChatWindowLocked(chatFrame:GetID(), isLocked)
BenillaFCF_ApplyLock(chatFrame)
end
-- ref l.785-800. **Its undock branch is deliberately absent**: the reference, on unlocking
-- a docked frame that is not the default one, calls `FCF_UnDockFrame`, re-centres it on the
-- screen and re-seats its tab. benilla has no undock (0288 §2), so that arm has nothing to
-- run — named here rather than stubbed (1134 §4), and reachable only from the Combat Log's
-- tab, whose own gate above already refuses to move it.
function FCF_ToggleLock()
local chatFrame = FCF_GetCurrentChatFrame()
if chatFrame.isLocked then
FCF_SetLocked(chatFrame, nil)
else
FCF_SetLocked(chatFrame, 1)
end
end
-- ref l.1428-1441, verbatim. `CHAT_LOCKED` is the blanket switch over every window; the
-- re-apply is ours, for the reason `FCF_SetLocked`'s is.
function FCF_Set_ChatLocked(isLocked)
-- The saved variable is a STRING, not a boolean — the uvar contract every options row
-- in this client shares, and what `RegisterForSave` writes out.
CHAT_LOCKED = isLocked and "1" or "0"
for i = 1, 2 do
local frame = getglobal("ChatFrame" .. i)
if frame then BenillaFCF_ApplyLock(frame) end
end
end
function FCF_Get_ChatLocked()
-- `1`/nil rather than a boolean, because every caller here is an `if` and the
-- reference's own callers pass the result around as one.
if CHAT_LOCKED == "1" then return 1 end
return nil
end
-- ref l.583-601. `this` is the grip button; the window is its parent. The commented-out
-- `DEFAULT_CHAT_FRAME.resizing = 1` block in the reference is left out with it.
function FCF_Resize(anchorPoint)
local chatFrame = this:GetParent()
if not FCF_CanMove(chatFrame) then return end
chatFrame.resizing = 1
chatFrame:StartSizing(anchorPoint)
end
-- ref l.603-609. The reference also calls `FCF_DockUpdate()` when the parent is the default
-- frame; ours has no such function and needs none — the tab is anchored to the window's own
-- background, so it follows a resize by anchor rather than by a re-layout pass.
function FCF_StopResize()
local chatFrame = this:GetParent()
chatFrame:StopMovingOrSizing()
chatFrame.resizing = nil
end
-- The move half. The reference reaches `StartMoving`/`StopMovingOrSizing` from
-- `FCF_Tab_OnClick`'s press/release arms; ours reaches them from the drag trio, for the
-- reasons written at the template's `OnDragStart`. Same two engine verbs, same three gates.
--
-- `MOVING_CHATFRAME` (the reference's global) is not set: its only readers there are
-- `FCF_IsValidChatFrame` and the dock-region hit test, neither of which exists here, and a
-- global nothing reads is state that starts lying the first time a path forgets to clear it.
function FCF_StartMove(chatFrame)
if not FCF_CanMove(chatFrame) then return end
chatFrame.moving = 1
chatFrame:StartMoving()
end
function FCF_StopMove(chatFrame)
chatFrame:StopMovingOrSizing()
chatFrame.moving = nil
end
-- Is either dock window mid-drag? The hover fade holds the chrome visible while it is —
-- ref FCF_OnUpdate's own `(MouseIsOver(...) or chatFrame.resizing)`. Without it the box
-- fades out under a moving cursor and takes the grip out of the hand holding it.
function BenillaFCF_Dragging()
for i = 1, 2 do
local f = getglobal("ChatFrame" .. i)
if f and (f.resizing or f.moving) then return 1 end
end
return nil
end
-- FloatingChatFrame_Update's look half (ref l.49-57): pull every window's stored tint and
-- alpha out of the engine and seat them, WITHOUT saving them back (they came from there).
-- Runs at load and on `UPDATE_CHAT_WINDOWS`, which is the event the host fires after it
-- restores the player's file.
function BenillaFCF_LoadWindowLooks()
for i = 1, 2 do
local frame = getglobal("ChatFrame" .. i)
if frame then
local _, fontSize, r, g, b, a, _, locked = GetChatWindowInfo(i)
FCF_SetWindowColor(frame, r, g, b, 1)
FCF_SetWindowAlpha(frame, a, 1)
-- ref l.56, in the same block and for the same reason as the two above: the
-- window's stored LOCKED is seated onto the frame. It ships `1` (the stock
-- chat-cache row), so both dock windows start locked exactly as a fresh 1.12
-- client's do — and the write back through `SetChatWindowLocked` is idempotent,
-- so seating what was just read dirties nothing.
FCF_SetLocked(frame, locked)
-- SIZE 0 is the stock "use the font's own height" and applies nothing.
if fontSize and fontSize > 0 then
FCF_SetChatWindowFontSize(frame, fontSize, 1)
end
end
end
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, button)
-- ref FCF_Tab_OnClick l.1219-1226: a RIGHT click opens the window's options menu and
-- returns — it does not select the tab, and it plays no click sound (the ref's sound
-- is on the OnClick *after* the call, which the early return skips). The ref also
-- calls `chatFrame:StopMovingOrSizing()` first; benilla has no drag, so there is
-- nothing in flight to stop.
if button == "RightButton" then
ToggleDropDownMenu(1, nil, getglobal("ChatFrame" .. id .. "TabDropDown"),
"ChatFrame" .. id .. "Tab", 0, 0)
return
end
-- ref FCF_Tab_OnClick (docked, left click): close any open menu, then select.
CloseDropDownMenus()
BenillaFCF_SelectDock(id)
PlaySound("UChatScrollButton")
end
-- ── The chat tab's options menu (decision 1589, B246) ────────────────────────────────
-- ref `FCFOptionsDropDown_Initialize` (FloatingChatFrame.lua l.87-393). The reference's
-- level-1 menu has four blocks: the window verbs (Lock/Unlock · Rename · Create New ·
-- Remove), a **DISPLAY** header over Font Size and Background, a **FILTERS** header over
-- seven message-group submenus, and Join New Channel.
--
-- benilla ships the Lock/Unlock row (1594 built the move, the resize and both lock
-- scopes under it) and the DISPLAY block, and that is the honest tree rather than a first
-- pass: the OTHER three window verbs still need the window management 0288 §2 left out
-- (there is no rename, no undock, no create/close), and the FILTERS block needs a runtime
-- message-group editor over `ui_chat::frames`' registration, which is its own feature and
-- not the one B246 reports missing. Each block lands with the machinery under it.
--
-- The menu is per tab (`ChatFrame<n>TabDropDown`), because `FCF_GetCurrentChatFrameID`
-- reads the open menu's PARENT id — that is the reference's contract and the reason the
-- capsule is declared inside the tab rather than shared.
-- ref l.81-85 is `UIDropDownMenu_Initialize(this, …, "MENU")` plus two width setters.
-- Ours assigns the two fields that call would have set, and skips the widths — both
-- departures are about OUR kit, not about the reference:
-- · `UIDropDownMenu_Initialize` clears every row of both shared lists, which is work at
-- LOAD time for a menu nobody has opened — and it would make this file's load order
-- depend on the dropdown kit's, and through it on GameTooltip's
-- `TOOLTIP_DEFAULT_COLOR`. `ToggleDropDownMenu` runs that init on every open anyway,
-- which is where the reference's own re-init happens too.
-- · `UIDropDownMenu_SetWidth(50)` means something DIFFERENT here: our kit reads a
-- declared width as `noResize` + a fixed list width (UIDropDownMenu.xml l.672-679),
-- so transcribing it would pin the menu at 50 px and cut "Background" in half. In the
-- reference, MENU mode never draws the capsule at all, so its width is inert there.
-- `BenillaFriendsDropDown` (FriendsFrame.xml) is already this same shape.
function FCFOptionsDropDown_OnLoad()
this.initialize = FCFOptionsDropDown_Initialize
this.displayMode = "MENU"
end
function FCFOptionsDropDown_Initialize()
local _, _, r, g, b, a = GetChatWindowInfo(FCF_GetCurrentChatFrameID())
local info
-- Level 2: the font-height list.
if UIDROPDOWNMENU_MENU_LEVEL == 2 then
if UIDROPDOWNMENU_MENU_VALUE == FONT_SIZE then
local _, fontHeight = FCF_GetCurrentChatFrame():GetFont()
for _, value in ipairs(CHAT_FONT_HEIGHTS) do
info = {}
info.text = format(FONT_SIZE_TEMPLATE, value)
info.value = value
info.func = FCF_SetChatWindowFontSize
-- ref: `value == floor(fontHeight+0.5)` — the tick follows the font the
-- frame is actually wearing, not the stored SIZE (which is 0 until the
-- player picks one).
if fontHeight and value == floor(fontHeight + 0.5) then
info.checked = 1
end
UIDropDownMenu_AddButton(info, UIDROPDOWNMENU_MENU_LEVEL)
end
end
return
end
-- Window options — the reference's FIRST row (ref l.236-246), and the only one of its
-- four window verbs benilla can honour. Rename, Create New and Remove each need the
-- window management 0288 §2 holds out (there is no rename, no undock, no create/close),
-- so they are named here rather than stubbed: a row that looks like a setting and
-- changes nothing cannot be told from a working one by looking (1134 §4).
local current = FCF_GetCurrentChatFrame()
info = {}
info.text = (current and current.isLocked) and UNLOCK_WINDOW or LOCK_WINDOW
info.func = FCF_ToggleLock
info.notCheckable = 1
UIDropDownMenu_AddButton(info)
-- Display header (ref l.273-279).
info = {}
info.text = DISPLAY
info.notClickable = 1
info.isTitle = 1
info.notCheckable = 1
UIDropDownMenu_AddButton(info)
-- Font size (ref l.281-288).
info = {}
info.text = FONT_SIZE
info.hasArrow = 1
info.func = nil
info.notCheckable = 1
UIDropDownMenu_AddButton(info)
-- Background — the colour swatch plus the opacity slider (ref l.290-307). This row is
-- B246's actual fix: `hasOpacity` is what puts `OpacitySliderFrame` beside the colour
-- wheel, and `opacityFunc` is what makes the chat box follow the thumb live.
--
-- **`info.opacity = 1 - a` is the reference's own line**, comment included ("Done
-- because the slider is reversed"): the opacity slider reads 0 at the TOP, so the
-- value it is seeded with and the value read back off it are both complements. The
-- read-back half is `FCF_SetChatWindowOpacity` below.
info = {}
info.text = BACKGROUND
info.hasColorSwatch = 1
info.r, info.g, info.b = r, g, b
if a then
a = 1 - a
end
info.opacity = a
info.swatchFunc = FCF_SetChatWindowBackGroundColor
info.func = UIDropDownMenuButton_OpenColorPicker
info.hasOpacity = 1
info.opacityFunc = FCF_SetChatWindowOpacity
info.cancelFunc = FCF_CancelWindowColorSettings
UIDropDownMenu_AddButton(info)
end
-- ref l.723-733: the id/frame the open menu belongs to. The menu's capsule is a child of
-- the tab, and the tab's id is its window's — which is why the tabs carry `id=` at all.
function FCF_GetCurrentChatFrameID()
return UIDropDownMenu_GetCurrentDropDown():GetParent():GetID()
end
-- ref l.729-735, fallback included: `UIDropDownMenu_GetCurrentDropDown` answers `this`
-- when no menu has ever been opened, so the lookup can miss — and then the caller IS a
-- row of the menu, whose parent is the capsule.
function FCF_GetCurrentChatFrame()
local current = getglobal("ChatFrame"
.. UIDropDownMenu_GetCurrentDropDown():GetParent():GetID())
if not current then
current = getglobal("ChatFrame" .. this:GetParent():GetID())
end
return current
end
-- ref l.740-744. The picker drives this on every drag of the colour wheel.
function FCF_SetChatWindowBackGroundColor()
local r, g, b = ColorPickerFrame:GetColorRGB()
FCF_SetWindowColor(FCF_GetCurrentChatFrame(), r, g, b)
end
-- ref l.746-749, the reversed slider's other half (see the Background row above).
function FCF_SetChatWindowOpacity()
FCF_SetWindowAlpha(FCF_GetCurrentChatFrame(), 1.0 - OpacitySliderFrame:GetValue())
end
-- Cancel restores what the row was opened with. The reference names this
-- `FCF_CancelWindowColorSettings` and hands it the pre-open r/g/b/opacity as arguments;
-- ours reads them off the button the picker was opened from, which is where
-- `UIDropDownMenuButton_OpenColorPicker` already stashes them (UIDropDownMenu.xml) — the
-- same values, one hop closer, and no argument contract to get wrong.
function FCF_CancelWindowColorSettings(prev)
local frame = FCF_GetCurrentChatFrame()
if prev and prev.r then
FCF_SetWindowColor(frame, prev.r, prev.g, prev.b)
end
if prev and prev.opacity then
FCF_SetWindowAlpha(frame, 1.0 - prev.opacity)
end
end
-- ref l.751-763: the visible half is the frame's own font, the stored half is SIZE.
-- `doNotSave` is ours, for the load path (the value already came from the store).
function FCF_SetChatWindowFontSize(chatFrame, fontSize, doNotSave)
if not chatFrame then
chatFrame = FCF_GetCurrentChatFrame()
end
if not fontSize then
fontSize = this.value
end
local fontFile, unused, fontFlags = chatFrame:GetFont()
chatFrame:SetFont(fontFile, fontSize, fontFlags)
if not doNotSave then
SetChatWindowSize(chatFrame:GetID(), fontSize)
end
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 > CHAT_TAB_SHOW_DELAY 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
-- ref FCF_OnUpdate's `(MouseIsOver(...) or chatFrame.resizing)`: a drag in flight pins
-- the chrome open. The cursor is moving during a resize, which would otherwise keep
-- resetting the stationary-hover delay and fade the grips out from under it.
if BenillaFCF_Dragging() then s.revealTarget = 1 end
-- The linear fade toward the target (ref UIFrameFadeIn/Out over CHAT_FRAME_FADE_TIME).
-- **Zero is a real setting, not a guard against nothing**: the Chat page's *Remove
-- Chat Hover Delay* row sets both this and the delay to 0 (ref SetChatMouseOverDelay),
-- and the division would be 0/0 on the frame `elapsed` is also zero. The reference
-- lands in the same place by a different road — `UIFrameFadeIn` with timeToFade 0
-- finishes on its first tick.
local fade = CHAT_FRAME_FADE_TIME
if fade <= 0 then
s.reveal = s.revealTarget
elseif s.reveal < s.revealTarget then
s.reveal = math.min(s.revealTarget, s.reveal + elapsed / fade)
elseif s.reveal > s.revealTarget then
s.reveal = math.max(s.revealTarget, s.reveal - elapsed / fade)
end
-- Apply: tabs at 1.0 (selected) / 0.5 (unselected) × reveal; each window's
-- background+borders at its own `windowAlpha` (ref: fade oldAlpha↔DEFAULT_CHATFRAME_
-- ALPHA). Until 1589 that second term was the constant `0.25 * reveal`, which is what
-- `windowAlpha` still computes for the shipped base of 0 — the default look is
-- unchanged, it is now just one case of a rule instead of the only case.
--
-- 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
local a = windowAlpha(i, s.reveal)
for _, t in ipairs(BenillaFCF_Textures(i)) do
t:SetAlpha(a)
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 resize grips ringing it, which are the border art AND the handles (ref
CHAT_FRAME_TEXTURES — see the block below); the scroll column; and the per-frame scripts.
Everything starts alpha 0; BenillaFCF fades it.
`movable`/`resizable`/`<ResizeBounds>` are the reference's own attributes at the reference's
own numbers (FloatingChatFrame.xml l.211, l.222-230): min 296×75, max 608×400. The
`<TitleRegion setAllPoints="true"/>` that sits beside them THERE is deliberately absent
here — see the drag scripts at the foot of this template for why, and for what replaces 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"
movable="true" resizable="true">
<!-- ref FloatingChatFrame.xml l.222-230, verbatim. The only `<ResizeBounds>` in the whole
of 1.12.1's FrameXML, and the numbers are the window's whole size domain: nothing else
clamps a chat window. -->
<ResizeBounds>
<minResize><AbsDimension x="296" y="75"/></minResize>
<maxResize><AbsDimension x="608" y="400"/></maxResize>
</ResizeBounds>
<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>
</Layer>
</Layers>
<Frames>
<!-- ── The eight RESIZE GRIPS (ref FloatingChatFrame.xml l.260-540) ─────────────────
The window's border art and its resize handles are the same eight objects: four
16×16 corners and four strips stretched between them, each a Button whose
`$parentTexture` is one slice of `ChatFrameBorder`. `CHAT_FRAME_TEXTURES` names
them, so they ride the hover fade and the tab menu's tint/opacity exactly as the
background does — they appear when the box appears and are invisible otherwise.
They were plain `$parentBorder*` textures until this arc (the look without the
mechanics); the anchors and TexCoords below are unchanged from those, so the ring
draws pixel-for-pixel where it did.
**Two of the reference's clauses are deliberately NOT transcribed, and both are
downstream of ONE divergence: its chat window is `enableMouse="false"` and ours is
not** (a `<ScrollingMessageFrame>` takes the mouse by construction here, and our
window uses it — `OnClick` dismisses a held spell, 0843, and `OnHyperlinkClick`
opens an item link, 0288 P2). In the reference the window competes for nothing, so:
· **No `this:SetFrameLevel(this:GetFrameLevel()-1)` OnLoad.** There it only sinks
the border art under the background wash. Here it would sink the grips under a
mouse-enabled parent, and every grip STRADDLES the window's edge (the top-left
corner overlaps its interior by 12×11 px, the edge strips by most of their
width) — so the overlapping half would stop hitting the grip and the handle
would be the outer sliver only. Level unchanged also keeps the ring drawing
over the wash, which is where benilla's border textures already were (they sat
after `$parentBackground` in the same layer).
· **The grips take the mouse only while the window is UNLOCKED**
(`BenillaFCF_ApplyLock`). Eight always-live buttons would shadow the outer
16px of the window — including the bottom strip, where the newest chat line
is — so an item link on the last line would stop being clickable. Locked is
the shipped state, so out of the box the window behaves exactly as it did;
unlocking it hands the edges over, which is what unlocking means. This is
benilla's stand-in for the click-through the reference gets for free.
`FCF_Resize`/`FCF_StopResize` still carry the reference's own three gates on top —
they are what `CHAT_LOCKED` and the docked-but-not-default case need. ── -->
<Button name="$parentResizeTopLeft">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentBackground"><Offset><AbsDimension x="-2" y="2"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0" right="0.25" top="0" bottom="0.125"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>FCF_Resize("TOPLEFT")</OnMouseDown>
<OnMouseUp>FCF_StopResize()</OnMouseUp>
</Scripts>
</Button>
<Button name="$parentResizeTopRight">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="$parentBackground"><Offset><AbsDimension x="2" y="2"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0.75" right="1.0" top="0" bottom="0.125"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>FCF_Resize("TOPRIGHT")</OnMouseDown>
<OnMouseUp>FCF_StopResize()</OnMouseUp>
</Scripts>
</Button>
<Button name="$parentResizeBottomLeft">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentBackground"><Offset><AbsDimension x="-2" y="-3"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0" right="0.25" top="0.7265625" bottom="0.8515625"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>FCF_Resize("BOTTOMLEFT")</OnMouseDown>
<OnMouseUp>FCF_StopResize()</OnMouseUp>
</Scripts>
</Button>
<Button name="$parentResizeBottomRight">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBackground"><Offset><AbsDimension x="2" y="-3"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<TexCoords left="0.75" right="1.0" top="0.7265625" bottom="0.8515625"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>FCF_Resize("BOTTOMRIGHT")</OnMouseDown>
<OnMouseUp>FCF_StopResize()</OnMouseUp>
</Scripts>
</Button>
<Button name="$parentResizeTop">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentResizeTopLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentResizeTopRight" relativePoint="LEFT"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT"/>
<Anchor point="RIGHT"/>
</Anchors>
<TexCoords left="0.25" right="0.75" top="0" bottom="0.125"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>FCF_Resize("TOP")</OnMouseDown>
<OnMouseUp>FCF_StopResize()</OnMouseUp>
</Scripts>
</Button>
<Button name="$parentResizeBottom">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentResizeBottomLeft" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeTo="$parentResizeBottomRight" relativePoint="LEFT"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="LEFT"/>
<Anchor point="RIGHT"/>
</Anchors>
<TexCoords left="0.25" right="0.75" top="0.7265625" bottom="0.8515625"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>FCF_Resize("BOTTOM")</OnMouseDown>
<OnMouseUp>FCF_StopResize()</OnMouseUp>
</Scripts>
</Button>
<Button name="$parentResizeLeft">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="$parentResizeTopLeft" relativePoint="BOTTOM"/>
<Anchor point="BOTTOM" relativeTo="$parentResizeBottomLeft" relativePoint="TOP"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOP"/>
<Anchor point="BOTTOM"/>
</Anchors>
<TexCoords left="0" right="0.25" top="0.125" bottom="0.7265625"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>FCF_Resize("LEFT")</OnMouseDown>
<OnMouseUp>FCF_StopResize()</OnMouseUp>
</Scripts>
</Button>
<Button name="$parentResizeRight">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOP" relativeTo="$parentResizeTopRight" relativePoint="BOTTOM"/>
<Anchor point="BOTTOM" relativeTo="$parentResizeBottomRight" relativePoint="TOP"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\ChatFrame\ChatFrameBorder" alpha="0">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors>
<Anchor point="TOP"/>
<Anchor point="BOTTOM"/>
</Anchors>
<TexCoords left="0.75" right="1.0" top="0.125" bottom="0.7265625"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnMouseDown>FCF_Resize("RIGHT")</OnMouseDown>
<OnMouseUp>FCF_StopResize()</OnMouseUp>
</Scripts>
</Button>
<!-- 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>
<!-- UPDATE_CHAT_WINDOWS — the reference's own event for "a window's stored
settings moved", and the one FloatingChatFrame_OnEvent listens for
(FloatingChatFrame.lua l.34-38) to re-read GetChatWindowInfo. Ours is fired
by the host once it has restored the player's per-character file
(ui_chat::settings), which lands after this file has already run. It rides
window 1's registration for the same reason EXECUTE_CHAT_LINE does: the
template is shared by all seven, the handler re-reads EVERY window, and seven
registrations would run it seven times per restore. -->
if this:GetID() == 1 then
this:RegisterEvent("EXECUTE_CHAT_LINE")
this:RegisterEvent("UPDATE_CHAT_WINDOWS")
end
-- `isDocked` — the reference's own flag, and it is LOAD-BEARING now rather than an
-- advertisement. Only this template is inherited, and only ChatFrame1/ChatFrame2
-- inherit it, so every frame that reaches this line is docked: they share one rect
-- (window 2's anchors are window 1's corners) and one tab strip.
--
-- This file used to withhold the flag on the grounds that it "belongs to a dock
-- MODEL (dock/undock, FCF_DockUpdate's re-anchoring) that 0288 §2 holds out", i.e.
-- setting it would advertise verbs we do not have. What changed is that the
-- reference's move and resize gates READ it — `chatFrame.isDocked and chatFrame ~=
-- DEFAULT_CHAT_FRAME` is the clause that keeps the Combat Log from being resized
-- out of the dock it is anchored into — so withholding it now means the gate that
-- protects window 2 silently never fires. The flag is a fact about our two windows;
-- the verbs remain out.
this.isDocked = 1
</OnLoad>
<OnEvent>
if event == "EXECUTE_CHAT_LINE" then
SubmitChatInput(arg1)
elseif event == "UPDATE_CHAT_WINDOWS" then
BenillaFCF_LoadWindowLooks()
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>
<!-- ── Dragging the window ──────────────────────────────────────────────────────────
**What the reference does, and why we cannot copy the declaration.** Its template
carries `<TitleRegion setAllPoints="true"/>` — the engine drag where a mouse-down
anywhere in the frame moves it — and that region is INERT there, because the same
line declares `enableMouse="false"`. So 1.12 never drags a chat window by its body;
it drags it by the TAB (`FCF_Tab_OnClick`'s `chatFrame:StartMoving()`, and the
tab's own newbie tip says so in as many words: *"Left-click and drag to move the
window"*).
Ours is mouse-enabled (a `<ScrollingMessageFrame>` takes the mouse by construction,
and this window uses it for `OnClick` and `OnHyperlinkClick`), so transcribing the
title region would have three consequences the reference never pays: a press on the
chat body would be SWALLOWED before `OnClick` (0843's held-spell dismissal dies), a
LOCKED window would drag anyway (the engine title path has no lock gate — wow-re Q6
records that the movable bit is not read on it at all), and there would be no way to
turn it off again (an anchorless region falls back to its owner's rect, 1349, so
`ClearAllPoints` widens it rather than removing it). It is also inert for a fourth,
duller reason: our loader has no `<TitleRegion>` element — the region is
Lua-created only.
So the gesture is the drag TRIO instead, which is the reference's own layering
(`RegisterForDrag` + `OnDragStart`/`OnDragStop` → `StartMoving`/
`StopMovingOrSizing`) and buys exactly what the title region costs: a CLICK still
reaches `OnClick` because an un-started gesture leaves the click path alone, the
lock is a Lua gate like every other one here, and `OnDragStop` fires on the SOURCE
frame, so a drag released anywhere at all still ends the move.
The registration itself is switched by `BenillaFCF_ApplyLock` — a locked window is
not drag-registered, so it behaves exactly as it did before this arc. Both the body
(benilla's addition, the affordance the title region would have given) and the tab
(the reference's own, on ChatFrame<n>Tab) reach the same two verbs. ── -->
<OnDragStart>
FCF_StartMove(this)
</OnDragStart>
<OnDragStop>
FCF_StopMove(this)
</OnDragStop>
</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.
**A frame the dock cannot hide is the reference's own answer**, and this comment used to
claim the opposite: that 1.12.1 ships `FCF_OnUpdate` defined and never referenced, so
vanilla's tabs never hover-fade and the reveal was benilla's own (0288). That is wrong.
`UIParent.xml` l.19 calls `FCF_OnUpdate(arg1)` from **UIParent's** `<OnUpdate>`, in the
same list as `UIFrameFadeUpdate`/`UIFrameFlashUpdate` — never from a chat frame, for the
reason above. A 1x1 frame parented to UIParent is that mounting point re-expressed; the
reasoning was right and only its premise was invented. The hover reveal, the 0.2 s
stationary delay and the 1.0/0.5 selected/unselected tab alphas are all the reference's.
`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 these five, and that is the whole point of the flag now: the two
dock windows set it in the shared template's `OnLoad` (it is what the reference's move and
resize gates read), and ChatFrame3..7 are the frames that genuinely are not docked. The
Outfitter hazard the flag used to carry — `if vChatFrame:IsVisible() or vChatFrame.isDocked`
then `vTab:GetText()` on a window with no tab — is why it stays off here: these have no
tabs, and they are hidden, so neither arm reaches the nil.
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" id="1">
<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>
<!-- The window's options menu (decision 1589). A child of the TAB, because
FCF_GetCurrentChatFrameID reads the open menu's parent id — the reference's own
contract, and why the tab carries `id` above. -->
<Frame name="$parentDropDown" inherits="UIDropDownMenuTemplate" hidden="true">
<Scripts>
<OnLoad>FCFOptionsDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
</Frames>
<Scripts>
<!-- ref FloatingChatFrame.xml l.121: the tab takes the right button too, which is
what opens the options menu. A Button's default set is LeftButtonUp only, so
without this the right-click never reaches OnClick at all. -->
<OnLoad>
this:RegisterForClicks("LeftButtonUp", "RightButtonUp")
</OnLoad>
<OnClick>
BenillaFCF_TabClick(1, arg1)
</OnClick>
<!-- The reference's own move handle (ref FCF_Tab_OnClick's press/release arms, and the
tip below, which says "Left-click and drag to move the window" in as many words).
It reaches the same two verbs as the window body does; the registration that arms
it is `BenillaFCF_ApplyLock`'s, so a locked window's tab still only selects. -->
<OnDragStart>
FCF_StartMove(getglobal("ChatFrame" .. this:GetID()))
</OnDragStart>
<OnDragStop>
FCF_StopMove(getglobal("ChatFrame" .. this:GetID()))
</OnDragStop>
<!-- ref l.129-134. The tip is how the menu is DISCOVERABLE: nothing on screen says a
chat tab has a right-click menu, which is half of why B246 read as "no chat
options at all". `noNormalText` is the reference's own 6th argument. -->
<OnEnter>
BenillaTooltip_AddNewbieTipFor(this, CHAT_OPTIONS_LABEL, 1.0, 1.0, 1.0,
NEWBIE_TOOLTIP_CHATOPTIONS, 1)
</OnEnter>
<OnLeave>
GameTooltip:Hide()
</OnLeave>
</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" id="2">
<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>
<!-- The window's options menu (decision 1589). A child of the TAB, because
FCF_GetCurrentChatFrameID reads the open menu's parent id — the reference's own
contract, and why the tab carries `id` above. -->
<Frame name="$parentDropDown" inherits="UIDropDownMenuTemplate" hidden="true">
<Scripts>
<OnLoad>FCFOptionsDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
</Frames>
<Scripts>
<!-- ref FloatingChatFrame.xml l.121: the tab takes the right button too, which is
what opens the options menu. A Button's default set is LeftButtonUp only, so
without this the right-click never reaches OnClick at all. -->
<OnLoad>
this:RegisterForClicks("LeftButtonUp", "RightButtonUp")
</OnLoad>
<OnClick>
BenillaFCF_TabClick(2, arg1)
</OnClick>
<!-- The reference's own move handle (ref FCF_Tab_OnClick's press/release arms, and the
tip below, which says "Left-click and drag to move the window" in as many words).
It reaches the same two verbs as the window body does; the registration that arms
it is `BenillaFCF_ApplyLock`'s, so a locked window's tab still only selects. -->
<OnDragStart>
FCF_StartMove(getglobal("ChatFrame" .. this:GetID()))
</OnDragStart>
<OnDragStop>
FCF_StopMove(getglobal("ChatFrame" .. this:GetID()))
</OnDragStop>
<!-- ref l.129-134. The tip is how the menu is DISCOVERABLE: nothing on screen says a
chat tab has a right-click menu, which is half of why B246 read as "no chat
options at all". `noNormalText` is the reference's own 6th argument. -->
<OnEnter>
BenillaTooltip_AddNewbieTipFor(this, CHAT_OPTIONS_LABEL, 1.0, 1.0, 1.0,
NEWBIE_TOOLTIP_CHATOPTIONS, 1)
</OnEnter>
<OnLeave>
GameTooltip:Hide()
</OnLeave>
</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 seat every window's stored LOOK
(FCF_SetWindowColor/Alpha: 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). Since 1589 that colour is
read out of the engine rather than written as a literal, because the player can now
change it — a fresh store still answers 0 0 0, so a first run looks exactly as before. -->
<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)
BenillaFCF_LoadWindowLooks()
]]></Script>
</Ui>