From 850f7df3b0f0fe154909c41d146c613d78ea3d6d Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 31 Aug 2026 19:51:11 +0700 Subject: [PATCH] the chat dock exists, and alt-arrow keys are declined not swallowed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- crates/benilla-app/assets/ui/ChatFrame.xml | 342 ++++++++++++++---- .../benilla-app/assets/ui/MinimapCluster.xml | 34 +- crates/benilla-app/src/bindings.rs | 70 +++- crates/benilla-app/src/minimap/mod.rs | 42 +++ crates/benilla-app/src/player.rs | 2 +- crates/benilla-app/src/ui_chat/edit.rs | 2 +- crates/benilla-app/src/ui_chat/settings.rs | 76 +++- crates/benilla-app/src/ui_chat/tests.rs | 26 +- .../src/ui_script/chat_options_tests.rs | 1 + .../benilla-app/src/ui_script/chat_tests.rs | 36 +- crates/benilla-app/src/ui_script/input.rs | 46 ++- crates/benilla-app/src/ui_script/lifecycle.rs | 3 +- crates/benilla-app/src/ui_script/mod.rs | 16 +- crates/benilla-ui/src/loader/widgets.rs | 4 +- crates/benilla-ui/src/script/chat_window.rs | 64 +++- crates/benilla-ui/src/script/editbox.rs | 19 +- .../benilla-ui/src/script/editbox/methods.rs | 47 ++- crates/benilla-ui/src/script/editbox/tests.rs | 100 ++++- crates/benilla-ui/src/script/minimap.rs | 14 + crates/benilla-ui/src/script/mod.rs | 40 ++ crates/benilla-ui/src/script/model.rs | 5 +- crates/benilla-ui/src/script/tests/minimap.rs | 33 ++ crates/benilla-ui/src/widget/kinds/editbox.rs | 40 +- crates/benilla-ui/src/widget/kinds/mod.rs | 15 + crates/benilla-ui/src/widget/mod.rs | 6 +- 25 files changed, 920 insertions(+), 163 deletions(-) diff --git a/crates/benilla-app/assets/ui/ChatFrame.xml b/crates/benilla-app/assets/ui/ChatFrame.xml index 171324f9..473e313e 100644 --- a/crates/benilla-app/assets/ui/ChatFrame.xml +++ b/crates/benilla-app/assets/ui/ChatFrame.xml @@ -484,7 +484,10 @@ -- The saved variable is a STRING, not a boolean — the uvar contract every options row -- in this client shares, and what `RegisterForSave` writes out. CHAT_LOCKED = isLocked and "1" or "0" - for i = 1, 2 do + -- 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 @@ -537,9 +540,8 @@ -- ref FCF_OnUpdate's own `(MouseIsOver(...) or chatFrame.resizing)`. Without it the box -- fades out under a moving cursor and takes the grip out of the hand holding it. function BenillaFCF_Dragging() - for i = 1, 2 do - local f = getglobal("ChatFrame" .. i) - if f and (f.resizing or f.moving) then return 1 end + for _, f in ipairs(DOCKED_CHAT_FRAMES) do + if f.resizing or f.moving then return 1 end end return nil end @@ -549,7 +551,10 @@ -- Runs at load and on `UPDATE_CHAT_WINDOWS`, which is the event the host fires after it -- restores the player's file. function BenillaFCF_LoadWindowLooks() - for i = 1, 2 do + -- 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) @@ -569,62 +574,232 @@ end end - -- FCF_SelectDockFrame: show the selected frame, hide the other; selected tab full alpha, - -- unselected half (both still scaled by the reveal fade below). Stops the selected tab's - -- whisper flash (ref FCF_SelectDockFrame → UIFrameFlashRemoveFrame). The tab getglobal - -- stays guarded: both dock tabs exist (1575), but the guard also covers ChatFrame3..7, - -- which have no tabs at all and which the error above is what actually keeps out. - function BenillaFCF_SelectDock(id) - BenillaFCF.selected = id - for i = 1, 2 do - local frame = getglobal("ChatFrame" .. i) - if i == id then frame:Show() else frame:Hide() end - end - -- `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 FCF_SelectDockFrame - -- (`FloatingChatFrame.lua:1232`) and again on a drag (`FloatingChatFrame.xml:158`); - -- ours has one selection verb that every one of those paths goes through, so it is - -- written here rather than replicated at three call sites. - SELECTED_CHAT_FRAME = getglobal("ChatFrame" .. id) or SELECTED_CHAT_FRAME - if BenillaFCF.flashTab == id then - BenillaFCF.flashTab = 0 - local flash = getglobal("ChatFrame" .. id .. "TabFlash") - if flash then flash:Hide() 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 - -- FCF_SelectDockFrame(frame) — the reference's own name for the operation above, taking a - -- FRAME where ours takes a dock id. Both corpus callers pass DEFAULT_CHAT_FRAME: + -- ref l.1049-1140. Three of its four jobs are here; the fourth is stated, not stubbed. -- - -- if not DEFAULT_CHAT_FRAME:IsVisible() then - -- FCF_SelectDockFrame(DEFAULT_CHAT_FRAME) - -- end - -- DEFAULT_CHAT_FRAME:AddMessage(out) + -- · 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. -- - -- verbatim in both CustomNameplates (`CustomNameplates.lua:69`) and Roid-Macros - -- (`Utility.lua:40`) — "un-hide the chat window before printing into it". Two addons, - -- two independent files, not a replicated library. + -- NOT here: the dock REGIONS (`ChatFrameTabDockRegion`) 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. -- - -- Ours does the dock half and NOT the rest of ref FCF_SelectDockFrame l.1204-1217: the ref - -- also assigns `SELECTED_DOCK_FRAME` (a global no corpus addon reads — 0 hits) and calls - -- `FCF_DockUpdate()` (tab geometry, which FCF_OnUpdate below re-derives every frame). + -- **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. -- - -- An UNDOCKED frame RAISES rather than quietly doing nothing. ChatFrame3..7 exist as real - -- frames but benilla has no undock/dock model at all (0288 §2 — window management is chat - -- settings, deliberately out), so "select ChatFrame5" has no meaning here; answering it - -- with silence would leave the caller's next AddMessage landing in a window that is still - -- hidden, which is the bug the guard was written to prevent. + -- 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 - local id = frame:GetID() - if id ~= 1 and id ~= 2 then - error("FCF_SelectDockFrame: " .. (frame:GetName() or "?") - .. " is not docked — benilla docks only ChatFrame1/ChatFrame2 (decision 0288 §2)", 2) + 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 - BenillaFCF_SelectDock(id) + 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) @@ -921,10 +1096,14 @@ -- 36 us/frame, the largest single handler in the client (1395 measured it). The -- guard restores the reference's own behaviour rather than inventing one. if s.reveal ~= s.appliedReveal or s.selected ~= s.appliedSelected then - for i = 1, 2 do + -- 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(((i == s.selected) and 1.0 or 0.5) * s.reveal) + 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 @@ -943,8 +1122,8 @@ -- stops for good. if not s.tabsSettled then local settled = true - for i = 1, 2 do - local tab = getglobal("ChatFrame" .. i .. "Tab") + 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 @@ -1553,24 +1732,17 @@ this:RegisterEvent("EXECUTE_CHAT_LINE") this:RegisterEvent("UPDATE_CHAT_WINDOWS") end - -- `isDocked` — the reference's own flag, and it is LOAD-BEARING now rather than an - -- advertisement. **The gate below is the chat-cache's own `DOCKED` byte** (1712): - -- the shipped file marks windows 1 and 2 `DOCKED 1` and windows 3..10 `DOCKED 0`, - -- so the two that reach the assignment are exactly the two that share one rect - -- (window 2's anchors are window 1's corners) and one tab strip. It was - -- unconditional while this template had two instances; it now has seven. + -- `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. -- - -- This file used to withhold the flag on the grounds that it "belongs to a dock - -- MODEL (dock/undock, FCF_DockUpdate's re-anchoring) that 0288 §2 holds out", i.e. - -- setting it would advertise verbs we do not have. What changed is that the - -- reference's move and resize gates READ it — `chatFrame.isDocked and chatFrame ~= - -- DEFAULT_CHAT_FRAME` is the clause that keeps the Combat Log from being resized - -- out of the dock it is anchored into — so withholding it now means the gate that - -- protects window 2 silently never fires. The flag is a fact about our two docked - -- windows; the verbs remain out. - if this:GetID() == 1 or this:GetID() == 2 then - this.isDocked = 1 - end + -- 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. if event == "EXECUTE_CHAT_LINE" then @@ -2086,12 +2258,17 @@ 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). ─────────── --> - +