Files
benilla-octo/crates/benilla-app/assets/ui/ChatFrame.xml
T
SamandClaude Fable 5 b46cb7e338 the chat dock exists, and alt-arrow keys are declined not swallowed
- the chat dock is a list, not a hardcoded pair: DOCKED_CHAT_FRAMES, SELECTED_DOCK_FRAME and the FCF verbs — DockFrame, UnDockFrame, DockUpdate, SelectDockFrame, SaveDock, SetTabPosition, Close and the button-side trio — are transcribed from FloatingChatFrame.lua, and every "for i = 1, 2" moved to what it meant, the dock or every window; the dock seeds at load from the window record's own docked byte handed to FCF_DockFrame, whose first line IS the reference's early return — transcribing the function that answers the way the reference answers, where an empty stub "because we have no dock" would have been the bandaid; its unconditional re-lock cannot clobber an unlock you saved, because the stored look is applied last; pfUI's RefreshChat now runs end to end
- ignoreArrows is alt-arrow mode, and the arrow is DECLINED, not swallowed: the reference's edit-box handler returns unconsumed on an arrow key, so the walk carries it past the box and it runs TURNLEFT — the whole feature is steering while the chat box holds focus, where the old reading left the arrows dead; holding Alt resumes the caret; SetAltArrowKeyMode and its getter land with the reference's own quirk that a missing argument enables while nil disables (the getter answers 1 or nil, never a boolean), SetIgnoreArrows turns out not to exist in 1.12 and is unpublished, and the binding pass gets a named four-key exemption rather than a hole, because the handler consumes every other path
- pfUI's square minimap is actually square: Minimap:SetMaskTexture makes the disc's mask state instead of a load-time constant — write-only from Lua, as shipped, no invented getter — where an empty path restores the round default and a failed load keeps the current mask and warns, because either mistake would otherwise hand a mistyped path a silently square map that looks like a feature; and MiniMapBattlefieldFrame grows the Border and Shine regions beside its icon, on the reference's own layers — icon on BACKGROUND, ring on ARTWORK above it, the only order a ring around an icon can draw in — which pfUI hides unguarded to sit the icon flush

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:51:11 +07:00

2401 lines
140 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"
-- EVERY window, which is what "the blanket switch" means and what the reference's own
-- l.1428-1441 loop does — not the dock's two. It read `1, 2` while those were the only
-- windows with grips and tabs to re-arm (1714).
for i = 1, NUM_CHAT_WINDOWS 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 _, f in ipairs(DOCKED_CHAT_FRAMES) do
if 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()
-- All seven, because the reference's `FloatingChatFrame_Update` is per window and
-- every one of them now has the chrome these setters paint (1712). It read `1, 2`
-- while ChatFrame3..7 had no textures to tint.
for i = 1, NUM_CHAT_WINDOWS 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
-- ── The DOCK (decision 1714) ────────────────────────────────────────────────────────
-- `DOCKED_CHAT_FRAMES` is the reference's own list — the docked windows in tab order —
-- and `SELECTED_DOCK_FRAME` the one of them that is shown. Everything below is
-- FloatingChatFrame.lua l.1049-1302, and the loops that used to run `for i = 1, 2` run
-- over the list instead: "the dock is windows 1 and 2" was a fact about our seeding, and
-- writing it as a literal is what made `FCF_DockFrame` impossible to answer honestly.
--
-- **The verbs exist; the GESTURE still does not.** 0288 §3 excluded drag/undock/resize as
-- chat *settings*, on the director's word, and nothing here adds a way for a player to
-- dock or undock a window — no dock regions, no tab-drag docking, no Remove row in the
-- tab menu. What changed is that an ADDON can now call the functions, which is a
-- different thing from us shipping the UI: pfUI's `RefreshChat` calls `FCF_DockFrame` on
-- every docked window and `FCF_UnDockFrame` on every undocked one, unguarded
-- (`chat.lua:374`/`:384`), and it is the top-installed addon of the expansion with
-- pfQuest, pfQuest-turtle and ShaguDPS embedding the same module. On our seeding both of
-- its calls hit the reference's own early returns and do nothing at all — which is the
-- point: a faithful no-op is what the reference gives it, and a missing global is not.
DOCKED_CHAT_FRAMES = {}
SELECTED_DOCK_FRAME = nil
-- ref l.1261-1264, verbatim. Every tab's seat, docked or not: `x` is the running dock
-- width for a docked tab and `0` for a loose one.
function FCF_SetTabPosition(chatFrame, x)
local chatTab = getglobal(chatFrame:GetName() .. "Tab")
chatTab:SetPoint("BOTTOMLEFT", chatFrame:GetName() .. "Background", "TOPLEFT", x + 2, 0)
end
-- ref l.1276-1285, verbatim: compact the list and write each window's POSITION back to the
-- engine record, which is what `GetChatWindowInfo`'s 9th return hands the next load.
function FCF_SaveDock()
local count = 1
local tempDock = DOCKED_CHAT_FRAMES
DOCKED_CHAT_FRAMES = {}
for _, value in ipairs(tempDock) do
DOCKED_CHAT_FRAMES[count] = value
SetChatWindowDocked(value:GetID(), count)
count = count + 1
end
end
-- ref l.1049-1140. Three of its four jobs are here; the fourth is stated, not stubbed.
--
-- · the non-first docked windows take DEFAULT_CHAT_FRAME's rect (which is what being
-- docked MEANS, and what ChatFrame2's XML anchors already say statically — this is
-- the same three points, re-applied whenever the list changes);
-- · show the selected window, hide the rest;
-- · lay the tabs out left to right by running dock width.
--
-- NOT here: the dock REGIONS (`ChatFrame<n>TabDockRegion`) the reference hides and
-- re-anchors in the same loop. They exist only to hit-test a tab being DRAGGED onto the
-- dock, which is the gesture 0288 §3 leaves out; there is nothing to anchor and nothing
-- to hide. Also not here: the overflow pass that shrinks every tab to the average width
-- when the strip outgrows the window (l.1101-1139) — it needs
-- `PanelTemplates_TabResize(0, tab, width)`'s explicit-width form, and two tabs at their
-- measured widths cannot outgrow a 430 px window.
--
-- **The tab ALPHAS are deliberately not written here**, though the reference writes them
-- in this loop. `FCF_OnUpdate` owns them behind a latch keyed on `(reveal, selected)`
-- (1395): writing them from here would leave the latch believing it had already applied a
-- value it had not, and the next hover would fade from the wrong place. Setting
-- `BenillaFCF.selected` below is what makes the latch fire, one frame later, with the
-- whole fade state consistent.
function FCF_DockUpdate()
local dockWidth = 0
local previousDockedFrame
for index, value in ipairs(DOCKED_CHAT_FRAMES) do
if index ~= 1 then
value:ClearAllPoints()
value:SetPoint("TOPLEFT", DEFAULT_CHAT_FRAME, "TOPLEFT", 0, 0)
value:SetPoint("BOTTOMLEFT", DEFAULT_CHAT_FRAME, "BOTTOMLEFT", 0, 0)
value:SetPoint("BOTTOMRIGHT", DEFAULT_CHAT_FRAME, "BOTTOMRIGHT", 0, 0)
end
if value == SELECTED_DOCK_FRAME then value:Show() else value:Hide() end
local chatTab = getglobal(value:GetName() .. "Tab")
if chatTab then
BenillaFCF_TabResize(chatTab)
if previousDockedFrame then
chatTab:ClearAllPoints()
FCF_SetTabPosition(value, dockWidth)
end
dockWidth = dockWidth + chatTab:GetWidth()
end
previousDockedFrame = value
end
end
-- ref l.1204-1217. Takes a FRAME, where `BenillaFCF_SelectDock` below takes a dock id.
--
-- It no longer RAISES on an undocked frame. It used to, because "select ChatFrame5" had
-- no meaning in a client with no dock model, and answering with silence would have left
-- the caller's next `AddMessage` landing in a still-hidden window. There is a dock model
-- now, and the reference's own answer is the assignment below: `FCF_DockUpdate` then
-- hides every docked window (none of them is the selection) and leaves the undocked one
-- exactly as it was. That is not a nice answer, but it is the client's, and the two corpus
-- callers — CustomNameplates `CustomNameplates.lua:69` and Roid-Macros `Utility.lua:40`,
-- both `FCF_SelectDockFrame(DEFAULT_CHAT_FRAME)` before printing — never reach it.
function FCF_SelectDockFrame(frame)
if not frame then
error("FCF_SelectDockFrame: nil frame", 2)
end
SELECTED_DOCK_FRAME = frame
-- `SELECTED_CHAT_FRAME` tracks the SELECTION, which is what makes it different from
-- `DEFAULT_CHAT_FRAME` (the print target, seeded once and left alone). The reference
-- writes it from the tab's own OnClick before it even reaches here
-- (`FloatingChatFrame.lua:1232`) and again on a drag (`FloatingChatFrame.xml:158`);
-- ours has one selection verb every one of those paths goes through, so it is written
-- here rather than replicated at three call sites.
SELECTED_CHAT_FRAME = frame
BenillaFCF.selected = frame:GetID()
-- Stop the selected tab's whisper flash (ref UIFrameFlashRemoveFrame + Hide).
if BenillaFCF.flashTab == frame:GetID() then
BenillaFCF.flashTab = 0
local flash = getglobal(frame:GetName() .. "TabFlash")
if flash then flash:Hide() end
end
FCF_DockUpdate()
end
-- The id-taking entry point, which is what every call site in this file uses.
function BenillaFCF_SelectDock(id)
FCF_SelectDockFrame(getglobal("ChatFrame" .. id))
end
-- ── The scroll column's SIDE (ref l.1015-1047, verbatim) ────────────────────────────
-- The column hangs off the window's left edge until the window is close enough to the
-- screen's left that it would hang off-screen, at which point it flips to the right. Only
-- `BottomButton` is re-anchored: `DownButton` sits on top of it and `UpButton` on top of
-- that, so the one point moves all three (the same chain the template declares).
--
-- The template's authored anchor IS the "left" case at the reference's own numbers
-- (`BOTTOMLEFT` of the frame's `BOTTOMLEFT`, −32/−4), so the first `FCF_SetButtonSide`
-- re-applies the point it already has and only the `buttonSide` field actually changes.
--
-- pfUI calls `FCF_UpdateButtonSide` directly on every member of `DOCKED_CHAT_FRAMES`
-- (`chat.lua:449`) after re-parenting the windows into its own panels, which is exactly
-- what this is for: it has just moved them, so the side has to be re-derived.
function FCF_UpdateButtonSide(chatFrame)
local leftDist = chatFrame:GetLeft()
local rightDist = GetScreenWidth() - chatFrame:GetRight()
local changed = nil
if (leftDist > 0 and leftDist <= rightDist) or rightDist < 0 then
if chatFrame.buttonSide ~= "left" then
FCF_SetButtonSide(chatFrame, "left")
changed = 1
end
else
if chatFrame.buttonSide ~= "right" or leftDist < 0 then
FCF_SetButtonSide(chatFrame, "right")
changed = 1
end
end
return changed
end
function FCF_SetButtonSide(chatFrame, buttonSide)
if chatFrame.buttonSide == buttonSide then return end
local bottom = getglobal(chatFrame:GetName() .. "BottomButton")
if buttonSide == "left" then
bottom:SetPoint("BOTTOMLEFT", chatFrame, "BOTTOMLEFT", -32, -4)
elseif buttonSide == "right" then
bottom:SetPoint("BOTTOMLEFT", chatFrame, "BOTTOMRIGHT", 0, -4)
end
chatFrame.buttonSide = buttonSide
end
function FCF_GetButtonSide(chatFrame)
return chatFrame.buttonSide
end
-- ref l.1148-1175, complete. The early return is the whole of what pfUI reaches: it calls
-- this on windows whose `isDocked` is already set.
function FCF_DockFrame(frame, index)
if frame.isDocked then return end
frame.isDocked = 1
if not index then
index = table.getn(DOCKED_CHAT_FRAMES) + 1
end
table.insert(DOCKED_CHAT_FRAMES, index, frame)
FCF_SaveDock()
FCF_SelectDockFrame(frame)
-- ref l.1167-1171: the dock's base window derives its own side; every other member
-- copies it, because they share its rect and a per-window derivation would race the
-- re-anchor above.
if frame == DEFAULT_CHAT_FRAME then
FCF_UpdateButtonSide(frame)
else
FCF_SetButtonSide(frame, FCF_GetButtonSide(DEFAULT_CHAT_FRAME))
end
FCF_SetLocked(frame, 1)
end
-- ref l.1177-1202. Minus the dock-region reset (l.1195-1197), for the reason FCF_DockUpdate
-- gives. The tab going to full alpha is the reference's own line and is kept: a loose tab
-- is not part of the dock's fade, so it does not follow the reveal.
function FCF_UnDockFrame(frame)
if frame == DEFAULT_CHAT_FRAME or not frame.isDocked then return end
SetChatWindowDocked(frame:GetID(), nil)
for index, value in ipairs(DOCKED_CHAT_FRAMES) do
if value == frame then
table.remove(DOCKED_CHAT_FRAMES, index)
break
end
end
frame.isDocked = nil
local chatTab = getglobal(frame:GetName() .. "Tab")
if chatTab then chatTab:SetAlpha(1.0) end
FCF_SelectDockFrame(DOCKED_CHAT_FRAMES[1])
end
-- ref l.1287-1302, verbatim. `HideUIPanel` is the reference's own call and is right here:
-- a chat window holds no panel slot, and ours falls through to `frame:Hide()` for exactly
-- that case (UiPanels.xml, "seated nowhere: a plain toplevel, or an addon's frame we never
-- placed").
function FCF_Close(frame)
if not frame then
frame = FCF_GetCurrentChatFrame()
end
if not frame:IsShown() then
FCF_DockUpdate()
return
end
if frame == DEFAULT_CHAT_FRAME then return end
HideUIPanel(frame)
local chatTab = getglobal(frame:GetName() .. "Tab")
if chatTab then chatTab:Hide() end
FCF_UnDockFrame(frame)
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
-- Over the DOCK, not over `1, 2`: the fade belongs to the windows sharing one
-- rect and one tab strip, and which windows those are is `DOCKED_CHAT_FRAMES`'s
-- answer now that `FCF_DockFrame` can change it (1714).
for _, frame in ipairs(DOCKED_CHAT_FRAMES) do
local i = frame:GetID()
local tab = getglobal("ChatFrame" .. i .. "Tab")
if tab then
tab:SetAlpha(((frame == SELECTED_DOCK_FRAME) 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 _, frame in ipairs(DOCKED_CHAT_FRAMES) do
local tab = getglobal(frame:GetName() .. "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 seven chat windows: ChatFrame1 (General) + ChatFrame2 (Combat Log) docked, 3..7
hidden and undocked ──────────────────────────────────────────────────────────────── -->
<!--
**One declaration, seven windows.** A chat window is the SAME widget seated differently, and
saying it 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.
**All seven, not two — and that changed for a measured reason (1712).** ChatFrame3..7 were
declared bare: a `ScrollingMessageFrame` and nothing else, on the argument that nothing
reads their furniture while they stay hidden and undocked. The top-20 corpus falsifies it.
pfUI's `chat.lua:588` is `for i = 1, NUM_CHAT_WINDOWS do _G["ChatFrame"..i.."UpButton"]:Hide()`
— the scroll column, the tab, and (in `RefreshChat`) `$parentBackground`, every
`CHAT_FRAME_TEXTURES` slice and `$parentResizeBottom`, all seven, none guarded. The
reference has them because the template is shared; the visibility of the window it hangs
off has nothing to do with whether the global resolves.
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 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="BenillaChatFrameTemplate" 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` is NOT set here, and that is the reference's own shape: the flag is
-- `FCF_DockFrame`'s to write (FloatingChatFrame.lua:1153), and the chat-cache
-- load is what calls it — this file's trailing seeding loop, which reads
-- `GetChatWindowInfo`'s `docked` position exactly as `FloatingChatFrame_Update`
-- (l.34-70) does.
--
-- It briefly lived here (1712), gated to ids 1 and 2, while there was no dock
-- model to own it. There is one now (1714), and leaving the assignment here would
-- break it outright: `FCF_DockFrame` early-returns on an already-docked frame, so
-- a pre-set flag would keep both windows out of `DOCKED_CHAT_FRAMES` — docked
-- according to the flag, absent from the list every dock verb walks.
</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="BenillaChatFrameTemplate" 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="BenillaChatFrameTemplate" 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.
**They now carry the full template, and that is 1712's correction.** These five used to
be declared bare — no tab, no background, no border art, no scroll column — on the
argument that the furniture is "for a dock model we do not have (0288 §2), and nothing
reads it while these frames stay hidden and undocked". The reading of Outfitter behind it
was right (`Outfitter.lua:3099` gates its `vTab:GetText()` behind `IsVisible() or
isDocked`) and the conclusion was still wrong, because one addon is not the corpus.
pfUI reads every piece of it, for all seven, unguarded:
chat.lua:588 `_G["ChatFrame"..i.."UpButton"]:Hide()` — and Down/Bottom, and
`_G["ChatFrame"..i.."Tab"].SetAlpha`, at module load
chat.lua:322 `local tab = _G["ChatFrame"..i.."Tab"]` → `tab:SetParent(…)`,
`tab:GetRegions()`
chat.lua:404 every `CHAT_FRAME_TEXTURES` slice, `texture:SetTexture()`
chat.lua:419 `_G["ChatFrame"..i.."ResizeBottom"]:Hide()`
Visibility has nothing to do with whether a global resolves, and pfQuest, pfQuest-turtle
and ShaguDPS embed the same library. So all seven inherit the same template, exactly as
the reference does (FloatingChatFrame.xml l.822-832 are five one-liners), and the tabs
below are declared the same way.
`isDocked` is still NOT set on these five, and that stays the whole point of the flag: the
template's `OnLoad` gates it to the two windows the chat-cache marks `DOCKED 1`, and these
are the frames that genuinely are not docked — which is what keeps Outfitter's own gate
doing what it does on a stock client.
They keep the reference's own look: window alpha 0 (the chat-cache `COLOR`'s fourth byte)
means a shown ChatFrame5 renders its text over the world with no box, and the template's
art is all alpha-0 until `BenillaFCF` fades it — which it does for the dock's two. ── -->
<ScrollingMessageFrame name="ChatFrame3" inherits="BenillaChatFrameTemplate" hidden="true" id="3">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame4" inherits="BenillaChatFrameTemplate" hidden="true" id="4">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame5" inherits="BenillaChatFrameTemplate" hidden="true" id="5">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame6" inherits="BenillaChatFrameTemplate" hidden="true" id="6">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
</ScrollingMessageFrame>
<ScrollingMessageFrame name="ChatFrame7" inherits="BenillaChatFrameTemplate" hidden="true" id="7">
<Size><AbsDimension x="430" y="128"/></Size>
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="100" y="100"/></Offset></Anchor></Anchors>
</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="BenillaChatTabTemplate" virtual="true" frameStrata="LOW" alpha="0">
<Size><AbsDimension x="70" y="32"/></Size>
<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")
<!-- The tab's LABEL is the window's name. The reference reads it from
`GetChatWindowInfo` (`FCF_SetWindowName`), and the shipped chat-cache holds a
NAME for windows 1 and 2 and leaves it EMPTY for 3..10 — so an undocked
window's tab has no text there either, and `GetText()` on it is nil. Ours
seats the same two from their GlobalStrings, which is where the loader's
ALL-CAPS key rule used to reach for them when the text was authored here. -->
local id = this:GetID()
if id == 1 then
this:SetText(GENERAL)
elseif id == 2 then
this:SetText(COMBAT_LOG)
end
</OnLoad>
<OnClick>
BenillaFCF_TabClick(this:GetID(), 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">
<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>
<!-- ChatFrame1Tab — "General". Only the seat is per-instance, exactly as the reference's own
instance is (FloatingChatFrame.xml l.712-720: an `<Anchors>` block and nothing else). -->
<Button name="ChatFrame1Tab" inherits="BenillaChatTabTemplate" id="1">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrame1Background" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</Button>
<!-- ChatFrame2Tab — "Combat Log". Its seat is the dock's tab strip: abutted to tab 1's right
edge, which is our standing-in for `FCF_DockUpdate`'s cumulative `FCF_SetTabPosition` x.
`BenillaFCF_TabResize` sizes both to their measured label widths, so the abutment survives
a locale whose words are longer. -->
<Button name="ChatFrame2Tab" inherits="BenillaChatTabTemplate" id="2">
<Anchors>
<Anchor point="LEFT" relativeTo="ChatFrame1Tab" relativePoint="RIGHT"/>
</Anchors>
</Button>
<!-- ChatFrame3Tab..ChatFrame7Tab — the five undocked windows' tabs. `hidden`, because that is
the reference's `ChatTabTemplate` default and only `FCF_DockFrame`/`FCF_OpenNewWindow`
un-hide one; and seated on their own window's background at the reference's own offset —
`FCF_SetTabPosition(chatFrame, 0)` is `SetPoint("BOTTOMLEFT", <frame>Background, "TOPLEFT",
x+2, 0)` (FloatingChatFrame.lua:1263), which is the same anchor tab 1 carries above.
They exist because addons read them, not because anything here shows them: pfUI's
`chat.lua:595` is `_G["ChatFrame"..i.."Tab"].SetAlpha` for all seven at module load, and
`chat.lua:322` hands the same tab to `SetParent`/`GetRegions` (1712). -->
<Button name="ChatFrame3Tab" inherits="BenillaChatTabTemplate" hidden="true" id="3">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrame3Background" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</Button>
<Button name="ChatFrame4Tab" inherits="BenillaChatTabTemplate" hidden="true" id="4">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrame4Background" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</Button>
<Button name="ChatFrame5Tab" inherits="BenillaChatTabTemplate" hidden="true" id="5">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrame5Background" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</Button>
<Button name="ChatFrame6Tab" inherits="BenillaChatTabTemplate" hidden="true" id="6">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrame6Background" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</Button>
<Button name="ChatFrame7Tab" inherits="BenillaChatTabTemplate" hidden="true" id="7">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="ChatFrame7Background" relativePoint="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</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": the four arrows are NOT consumed by the box
unless **ALT** is held, so they fall through to their bindings and you keep arrow-steering
with the box open. (This comment said "Ctrl bypasses" — a wow-re note had `0x41f8f0(2)`
labelled Ctrl; it is Alt, and the §5 `ignorearrows-alt-arrow-gate.md` corrected the note.
The binding on that very bit is named `SetAltArrowKeyMode`.)
benilla deliberately does NOT set the attribute here — the director's call is the modern
law: plain arrows move the caret, Up/Down recall history (decision 0301). What changed is
only that the engine now honours the attribute FAITHFULLY for a box that does set it: the
key is declined rather than swallowed-and-ignored, which is the difference between
arrow-steering working and the keys being dead. -->
<!-- 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
-- ...and `SELECTED_CHAT_FRAME`, which the reference seats on the SAME line of the same
-- OnLoad (`FloatingChatFrame.xml:739-740`). The two are not synonyms and the difference
-- is why both exist: DEFAULT is where FrameXML PRINTS, SELECTED is which window the
-- player last picked, and it moves with the tabs (BenillaFCF_SelectDock, above). Seeded
-- here because `BenillaFCF_SelectDock(1)` on the next line is what first writes it —
-- pfUI reads it from a chat-tab SetAlpha hook that can fire before any tab is clicked,
-- and a nil there stopped it (and, through its embedded copies, pfQuest, pfQuest-turtle
-- and ShaguDPS) dead.
SELECTED_CHAT_FRAME = ChatFrame1
-- ── The four per-window fields `ChatFrame_OnLoad` seats on every chat frame ───────────
-- `this.tellTimer`, `this.channelList`, `this.zoneChannelList`, `this.messageTypeList`
-- (ChatFrame.lua l.1155-1158). Written here in one loop rather than in an OnLoad, because
-- ChatFrame3..7 deliberately have no OnLoad at all (see their block above) and the
-- reference's semantics are per-FRAME, not per-template — a loop over the seven windows
-- is the same set of writes with nothing left uncovered.
--
-- **The three tables are EMPTY, which is the same honest shape `ChatTypeGroup` above
-- carries and for the same reason**: our chat routes host-side (`ui_chat`), so a window
-- does not accumulate a message-group subscription list to put in `messageTypeList`, and
-- channel membership is the engine's, not a per-window Lua list. The divergence, stated:
-- an addon that reads these gets a truthful "this window subscribes to nothing in Lua"
-- rather than a nil. That is the `ChatFrame_OnEvent` architecture question again, not
-- these fields'.
--
-- Measured demand: pfUI's `RefreshChat` walks `_G["ChatFrame"..i].messageTypeList` with
-- `pairs` and NO nil guard (`modules/chat.lua:325`), counting how many of a window's
-- types look like a combat log. A nil there is `bad argument #1 to 'pairs'` — and pfUI is
-- embedded in pfQuest, pfQuest-turtle and ShaguDPS, so it is four top-20 addons on one
-- unset field. With an empty list the count is 0 and no window is classed as a combat
-- log, which is exactly right for a UI that has no combat-log window.
for i = 1, NUM_CHAT_WINDOWS do
local frame = getglobal("ChatFrame" .. i)
if frame then
frame.tellTimer = GetTime()
frame.channelList = {}
frame.zoneChannelList = {}
frame.messageTypeList = {}
end
end
-- ── Seed the dock (ref `FloatingChatFrame_Update` l.34-70, the `docked` arm) ─────────
-- `GetChatWindowInfo`'s 9th return is the window's stored dock POSITION, and the
-- reference hands it straight to `FCF_DockFrame` as the insertion index. Windows 1 and 2
-- answer 1 and 2 out of the shipped chat-cache; 3..7 answer nil and stay loose. This runs
-- before the selection below because `FCF_SelectDockFrame` shows the selected member of
-- `DOCKED_CHAT_FRAMES` and hides the rest — with an empty list it would hide nothing and
-- show nothing.
for i = 1, NUM_CHAT_WINDOWS do
local _, _, _, _, _, _, _, _, docked = GetChatWindowInfo(i)
if docked then
FCF_DockFrame(getglobal("ChatFrame" .. i), docked)
end
end
BenillaFCF_SelectDock(1)
BenillaFCF_LoadWindowLooks()
]]></Script>
</Ui>