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>
This commit is contained in:
@@ -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 (`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.
|
||||
--
|
||||
-- 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.
|
||||
</OnLoad>
|
||||
<OnEvent>
|
||||
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). ─────────── -->
|
||||
<!-- The ref template ships ignoreArrows="true": plain arrows fall through to CHARACTER
|
||||
MOVEMENT while typing (Ctrl bypasses), because 1.12 lets you keep arrow-steering with
|
||||
the box open. benilla has no typing-while-moving arrow scheme, so the attr would leave
|
||||
the keys dead; the director's call is the modern law — plain arrows move the caret,
|
||||
Up/Down recall history (decision 0301). The engine still honors the attr faithfully
|
||||
for any box that sets it. -->
|
||||
<!-- 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">
|
||||
@@ -2204,6 +2381,19 @@
|
||||
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>
|
||||
|
||||
@@ -437,8 +437,21 @@
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="13" y="-13"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<!-- **All THREE of the reference's regions, on the reference's layers**
|
||||
(ref-Minimap.xml l.311-349). This block used to carry only the icon,
|
||||
which was the same half-step the note above warns against: "an addon
|
||||
that moves a frame expects its named children to exist" is right, and
|
||||
then one of the three was declared. pfUI's `modules/minimap.lua:80` is
|
||||
`MiniMapBattlefieldBorder:Hide()`, unguarded — it hides the ring so the
|
||||
icon sits flush on its square minimap — and pfQuest, pfQuest-turtle and
|
||||
ShaguDPS all embed that module.
|
||||
|
||||
The LAYERS are the reference's too, and they are not decoration: the
|
||||
icon is BACKGROUND and the border ARTWORK above it, which is the order
|
||||
a ring around an icon has to draw in. Ours had the icon on ARTWORK, so
|
||||
adding the border there would have raced it. -->
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="MiniMapBattlefieldIcon" file="Interface\BattlefieldFrame\UI-Battlefield-Icon">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
@@ -446,6 +459,25 @@
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="MiniMapBattlefieldBorder" file="Interface\Minimap\MiniMap-TrackingBorder">
|
||||
<Size><AbsDimension x="52" y="52"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"/></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="BORDER">
|
||||
<!-- The queue-ready pulse. Nothing here drives it (benilla has no
|
||||
battleground queue), and it is declared for the same reason its
|
||||
two siblings are: `BattlegroundShine` is a global an addon can
|
||||
read, and its absence is a nil rather than a missing effect. -->
|
||||
<Texture name="BattlegroundShine" file="Interface\ComboFrame\ComboPoint" alphaMode="ADD">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"><Offset><AbsDimension x="3" y="3"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<TexCoords left="0.5625" right="1" top="0" bottom="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -471,7 +471,7 @@ fn latch_and_dispatch(
|
||||
// releases every direction bit, `0x514490`): Held latches clear once on the rising edge;
|
||||
// EdgeUpDown latches stay armed — their release half still fires (the reference delivers
|
||||
// the up of a pressed binding regardless).
|
||||
let typing = capture.0;
|
||||
let typing = capture.typing;
|
||||
if typing && !state.was_typing {
|
||||
state.latched.retain(|&(_, b)| match b {
|
||||
Bound::Spec(c) => !matches!(SPECS[c.0 as usize].kind, Kind::Held),
|
||||
@@ -488,7 +488,19 @@ fn latch_and_dispatch(
|
||||
let key = chord::normalize_key(ev.key_code);
|
||||
match ev.state {
|
||||
ButtonState::Pressed => {
|
||||
if armed || typing || sup || ev.repeat {
|
||||
// The alt-arrow exemption: a focused box in alt-arrow mode declines the four
|
||||
// arrows, so their bindings DO fire even while typing — which is the whole
|
||||
// point of the flag (turn the camera with the chat box open). Every other key
|
||||
// a focused box still swallows. See `UiKeyboardCapture::arrows_fall_through`.
|
||||
let arrow_exempt = capture.arrows_fall_through
|
||||
&& matches!(
|
||||
ev.key_code,
|
||||
KeyCode::ArrowLeft
|
||||
| KeyCode::ArrowRight
|
||||
| KeyCode::ArrowUp
|
||||
| KeyCode::ArrowDown
|
||||
);
|
||||
if armed || (typing && !arrow_exempt) || sup || ev.repeat {
|
||||
continue;
|
||||
}
|
||||
if state.latched.iter().any(|&(k, _)| k == BindKey::Key(key)) {
|
||||
@@ -965,6 +977,54 @@ mod tests {
|
||||
/// same pair wearing one chunk, so it must follow the same rule — dropped on the focus edge,
|
||||
/// it would leave whatever its down half started running forever, with no key left to press
|
||||
/// to stop it.
|
||||
/// **The alt-arrow exemption: you can turn while the chat box has focus.**
|
||||
///
|
||||
/// A focused EditBox swallows every key — that is the reference's own handler returning 1 on
|
||||
/// every path (`0x77b35e`) and our `UiKeyboardCapture::typing` gate. The four arrow codes are
|
||||
/// the single exception: with the box in alt-arrow mode (`ignoreArrows` in XML,
|
||||
/// `SetAltArrowKeyMode` in Lua) and ALT not held, the handler returns 0 at `0x77b1c4`, the
|
||||
/// strata walk carries the key down to `CGWorldFrame`, and `ExecuteBinding` runs `TURNLEFT`.
|
||||
/// The reference's own chat box ships the flag, so this is the default experience.
|
||||
///
|
||||
/// benilla read the flag as "consumed but inert, unless Ctrl" until the §5
|
||||
/// (`ignorearrows-alt-arrow-gate.md`) corrected both halves — the modifier is ALT, and the key
|
||||
/// is not consumed at all. Under the old reading, holding LEFT with the chat box open did
|
||||
/// nothing whatever.
|
||||
#[test]
|
||||
fn a_flagged_editbox_lets_the_arrow_keys_through_to_their_bindings() {
|
||||
let mut app = harness();
|
||||
|
||||
// Typing with no exemption: the arrow is swallowed, exactly like every other key.
|
||||
app.world_mut().resource_mut::<UiKeyboardCapture>().typing = true;
|
||||
press_key(&mut app, KeyCode::ArrowLeft);
|
||||
app.update();
|
||||
assert!(
|
||||
!state(&app).pressed(cmd::TURN_LEFT),
|
||||
"an unflagged focused box eats the arrow"
|
||||
);
|
||||
release_key(&mut app, KeyCode::ArrowLeft);
|
||||
app.update();
|
||||
|
||||
// Same key, same focus, exemption armed: the binding fires.
|
||||
app.world_mut()
|
||||
.resource_mut::<UiKeyboardCapture>()
|
||||
.arrows_fall_through = true;
|
||||
press_key(&mut app, KeyCode::ArrowLeft);
|
||||
app.update();
|
||||
assert!(
|
||||
state(&app).pressed(cmd::TURN_LEFT),
|
||||
"a flagged box declines the arrow, so TURNLEFT runs"
|
||||
);
|
||||
|
||||
// And the exemption is FOUR KEYS, not a hole in the typing gate: W is still swallowed.
|
||||
press_key(&mut app, KeyCode::KeyW);
|
||||
app.update();
|
||||
assert!(
|
||||
!state(&app).pressed(cmd::MOVE_FORWARD),
|
||||
"only the arrows are exempt — every other key a focused box still eats"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_on_up_addon_latch_survives_the_typing_edge_and_still_releases() {
|
||||
let mut script = UiScript::new().expect("VM");
|
||||
@@ -983,7 +1043,7 @@ mod tests {
|
||||
assert_eq!(lua_count(&app, "PROBE_DOWN"), 1);
|
||||
|
||||
// A box takes focus: movement stops, the addon's latch stays.
|
||||
app.world_mut().resource_mut::<UiKeyboardCapture>().0 = true;
|
||||
app.world_mut().resource_mut::<UiKeyboardCapture>().typing = true;
|
||||
app.update();
|
||||
assert!(!state(&app).pressed(cmd::MOVE_FORWARD));
|
||||
assert_eq!(
|
||||
@@ -1342,7 +1402,7 @@ mod tests {
|
||||
assert!(state(&app).pressed(cmd::MOVE_FORWARD));
|
||||
// A box takes focus: movement stops (the reference's focus handler releases the
|
||||
// direction bits), and new presses do nothing.
|
||||
app.world_mut().resource_mut::<UiKeyboardCapture>().0 = true;
|
||||
app.world_mut().resource_mut::<UiKeyboardCapture>().typing = true;
|
||||
app.update();
|
||||
assert!(
|
||||
!state(&app).pressed(cmd::MOVE_FORWARD),
|
||||
@@ -1356,7 +1416,7 @@ mod tests {
|
||||
);
|
||||
// Focus drops; keys work again.
|
||||
release_key(&mut app, KeyCode::KeyX);
|
||||
app.world_mut().resource_mut::<UiKeyboardCapture>().0 = false;
|
||||
app.world_mut().resource_mut::<UiKeyboardCapture>().typing = false;
|
||||
app.update();
|
||||
press_key(&mut app, KeyCode::KeyX);
|
||||
app.update();
|
||||
|
||||
@@ -984,6 +984,45 @@ fn feed_minimap_inside(
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap the disc's mask when an addon has asked for a different one — `Minimap:SetMaskTexture`.
|
||||
///
|
||||
/// The renderer already masks the map through its own `UiQuadMask`; this only decides which
|
||||
/// texture that is, so the whole feature is a load and a handle swap. It is what makes pfUI's
|
||||
/// square minimap — the single most recognisable thing it does to the default UI — actually
|
||||
/// square, and it is a real 1.12 method rather than a pfUI invention.
|
||||
///
|
||||
/// Memoised on the path, so the load happens on the edge and never per frame; a VM restart resets
|
||||
/// the memo and re-reads, because the fresh VM's widget starts at the engine default again. A path
|
||||
/// that fails to load leaves the current mask standing and says so once — a missing file must not
|
||||
/// silently unmask the map into a square, which is what dropping the handle would do.
|
||||
fn feed_minimap_mask(
|
||||
script: Option<bevy::ecs::system::NonSendMut<benilla_ui::script::UiScript>>,
|
||||
assets: Option<ResMut<MinimapAssets>>,
|
||||
world_assets: Option<ResMut<WorldAssets>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut last: Local<crate::ui_script::VmMemo<Option<String>>>,
|
||||
) {
|
||||
let (Some(script), Some(mut assets), Some(mut world_assets)) = (script, assets, world_assets)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let want = script.minimap_mask_texture();
|
||||
let last = last.get(&script);
|
||||
if *last == want {
|
||||
return;
|
||||
}
|
||||
*last = want.clone();
|
||||
let path = want
|
||||
.as_deref()
|
||||
.unwrap_or(benilla_ui::widget::MINIMAP_DEFAULT_MASK);
|
||||
match world_assets.mask_texture(path, &mut images) {
|
||||
Some(handle) => assets.mask = Some(handle),
|
||||
None => {
|
||||
warn!("minimap: SetMaskTexture(\"{path}\") — no such texture; keeping the current mask")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push the player's world facing onto the Minimap's engine-owned player-arrow `Model` — the
|
||||
/// client's `CMinimap::SetPlayerFacing 0x4eb8e0`, which writes it verbatim into the arrow's
|
||||
/// `[+0x39c]`, the field `Model:GetFacing()` reads.
|
||||
@@ -1070,6 +1109,9 @@ impl Plugin for MinimapPlugin {
|
||||
// Before the script tick, so an addon's OnUpdate steers off this frame's
|
||||
// heading rather than last frame's.
|
||||
feed_minimap_player_facing.before(crate::ui_script::UiInput),
|
||||
// Before the emit that reads `MinimapAssets::mask`, so a mask set this frame
|
||||
// is the one this frame draws with.
|
||||
feed_minimap_mask.before(UiQuadAppend),
|
||||
// Before the script tick, and after the containment verdict it reads: the
|
||||
// `MINIMAP_PING` event and `Minimap:GetPingPosition()`'s value land in the
|
||||
// same tick, on a ping the renderer already drew at the end of last frame.
|
||||
|
||||
@@ -572,7 +572,7 @@ fn control(
|
||||
// "no keys held" — so the avatar isn't also driven while typing (a `.tele` command). Mouse still
|
||||
// works. The gate is `UiKeyboardCapture`, which the focused chat EditBox drives; the free-fly
|
||||
// chord below is deliberately outside it, like every dev chord ([`modkeys::dev_chord`]).
|
||||
let typing = ui_capture.0;
|
||||
let typing = ui_capture.typing;
|
||||
// The rebindable inputs all read `binds` (decision 0997): the dispatch already enforced the
|
||||
// typing gate and 0585's exact-modifier law when it latched, so this module carries neither
|
||||
// anymore. Nothing here reads a bare key any more (decision 1043) — the free-fly toggle is on
|
||||
|
||||
@@ -382,7 +382,7 @@ pub(super) fn chat_edit_live(
|
||||
let Some(script) = script else {
|
||||
return;
|
||||
};
|
||||
if !ui_capture.0 {
|
||||
if !ui_capture.typing {
|
||||
return; // box not focused — nothing to live-parse
|
||||
}
|
||||
let text: String = script
|
||||
|
||||
@@ -105,14 +105,17 @@ fn render(looks: &[ChatWindowLook]) -> String {
|
||||
let mut out = String::from(HEADER);
|
||||
for (i, l) in looks.iter().enumerate() {
|
||||
out.push_str(&format!(
|
||||
"WINDOW {} SIZE {} COLOR {} {} {} {} LOCKED {}\n",
|
||||
"WINDOW {} SIZE {} COLOR {} {} {} {} LOCKED {} DOCKED {}\n",
|
||||
i + 1,
|
||||
l.font_size,
|
||||
l.r,
|
||||
l.g,
|
||||
l.b,
|
||||
l.a,
|
||||
i32::from(l.locked)
|
||||
i32::from(l.locked),
|
||||
// The reference's own spelling: the dock POSITION, and `0` for a window that is not
|
||||
// docked (the shipped rows for windows 3..10 are `DOCKED 0`).
|
||||
l.docked.unwrap_or(0)
|
||||
));
|
||||
}
|
||||
out
|
||||
@@ -144,7 +147,12 @@ fn parse(text: &str) -> Vec<(usize, ChatWindowLook)> {
|
||||
if index == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut look = ChatWindowLook::default();
|
||||
// Seeded from the STOCK row for this window, not one flat default: `DOCKED` is the one
|
||||
// field whose shipped value differs per window (1 and 2 are the dock), so a file written
|
||||
// by a build that knew nothing about the key must leave the dock where it shipped rather
|
||||
// than silently undocking both windows — the same leniency `LOCKED` gets below, applied
|
||||
// where a flat default could not express it.
|
||||
let mut look = ChatWindowLook::stock(index - 1);
|
||||
let byte = |s: Option<&str>| -> u8 { s.and_then(|v| v.parse::<u8>().ok()).unwrap_or(0) };
|
||||
while let Some(key) = it.next() {
|
||||
if key.eq_ignore_ascii_case("SIZE") {
|
||||
@@ -165,6 +173,12 @@ fn parse(text: &str) -> Vec<(usize, ChatWindowLook)> {
|
||||
// read, and the lenient half of it: a file from a build that wrote nothing here
|
||||
// keeps the stock `LOCKED 1` default rather than silently unlocking the window.
|
||||
look.locked = it.next().is_none_or(|v| v.trim() != "0");
|
||||
} else if key.eq_ignore_ascii_case("DOCKED") {
|
||||
// The dock POSITION, 1-based; `0` (and anything unparseable) is "not docked".
|
||||
look.docked = it
|
||||
.next()
|
||||
.and_then(|v| v.trim().parse::<u8>().ok())
|
||||
.filter(|p| *p > 0);
|
||||
}
|
||||
// Anything else is a key from a build that knows more than this one — skip it and the
|
||||
// value it would have consumed cannot be told from the next key, so skip only the key.
|
||||
@@ -298,7 +312,10 @@ pub(super) fn plugin(app: &mut App) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn look(r: u8, g: u8, b: u8, a: u8, font_size: i32) -> ChatWindowLook {
|
||||
/// A look for WINDOW `n` (1-based). The window number is a parameter because `DOCKED` is the
|
||||
/// one field whose stock value differs per window — the dock is windows 1 and 2 — so an
|
||||
/// expectation that does not say which window it is cannot be right for all of them.
|
||||
fn look(n: usize, r: u8, g: u8, b: u8, a: u8, font_size: i32) -> ChatWindowLook {
|
||||
ChatWindowLook {
|
||||
r,
|
||||
g,
|
||||
@@ -306,6 +323,7 @@ mod tests {
|
||||
a,
|
||||
font_size,
|
||||
locked: true,
|
||||
docked: ChatWindowLook::stock(n - 1).docked,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,8 +332,8 @@ mod tests {
|
||||
#[test]
|
||||
fn the_file_round_trips() {
|
||||
let looks = vec![
|
||||
look(0, 0, 0, 64, 14),
|
||||
look(255, 128, 0, 255, 0),
|
||||
look(1, 0, 0, 0, 64, 14),
|
||||
look(2, 255, 128, 0, 255, 0),
|
||||
ChatWindowLook::default(),
|
||||
];
|
||||
let parsed = parse(&render(&looks));
|
||||
@@ -346,9 +364,9 @@ mod tests {
|
||||
assert_eq!(
|
||||
got,
|
||||
vec![
|
||||
(0, look(10, 20, 30, 40, 16)),
|
||||
(1, look(1, 2, 3, 4, 0)),
|
||||
(2, look(0, 0, 0, 0, 12)),
|
||||
(0, look(1, 10, 20, 30, 40, 16)),
|
||||
(1, look(2, 1, 2, 3, 4, 0)),
|
||||
(2, look(3, 0, 0, 0, 0, 12)),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -360,21 +378,55 @@ mod tests {
|
||||
fn the_lock_round_trips_and_an_absent_key_stays_locked() {
|
||||
let unlocked = ChatWindowLook {
|
||||
locked: false,
|
||||
..look(0, 0, 0, 64, 14)
|
||||
..look(1, 0, 0, 0, 64, 14)
|
||||
};
|
||||
assert!(render(&[unlocked]).contains("LOCKED 0"));
|
||||
assert_eq!(parse(&render(&[unlocked])), vec![(0, unlocked)]);
|
||||
assert_eq!(
|
||||
parse("WINDOW 1 SIZE 14 COLOR 0 0 0 64\n"),
|
||||
vec![(0, look(0, 0, 0, 64, 14))],
|
||||
vec![(0, look(1, 0, 0, 0, 64, 14))],
|
||||
"no LOCKED key = the stock LOCKED 1"
|
||||
);
|
||||
}
|
||||
|
||||
/// **`DOCKED` round-trips, and an absent key keeps the window's own stock position** — the
|
||||
/// leniency `LOCKED` gets, in the one field where a single flat default could not express it.
|
||||
/// A file written by a build that knew nothing about the key must leave the dock where it
|
||||
/// shipped; seeding every window from one default would have undocked ChatFrame1 and
|
||||
/// ChatFrame2 on the next login of every existing player (1714).
|
||||
#[test]
|
||||
fn dock_positions_round_trip_and_an_absent_key_keeps_the_stock() {
|
||||
let undocked = ChatWindowLook {
|
||||
docked: None,
|
||||
..look(1, 0, 0, 0, 64, 14)
|
||||
};
|
||||
assert!(render(&[undocked]).contains("DOCKED 0"));
|
||||
assert_eq!(parse(&render(&[undocked])), vec![(0, undocked)]);
|
||||
|
||||
// No DOCKED key at all: window 1 keeps position 1, window 2 keeps 2, window 3 stays out.
|
||||
let got = parse(
|
||||
"WINDOW 1 SIZE 0
|
||||
WINDOW 2 SIZE 0
|
||||
WINDOW 3 SIZE 0
|
||||
",
|
||||
);
|
||||
assert_eq!(
|
||||
got.iter().map(|(_, l)| l.docked).collect::<Vec<_>>(),
|
||||
vec![Some(1), Some(2), None]
|
||||
);
|
||||
|
||||
// And a moved window round-trips at its new position.
|
||||
let moved = ChatWindowLook {
|
||||
docked: Some(3),
|
||||
..look(1, 0, 0, 0, 0, 0)
|
||||
};
|
||||
assert_eq!(parse(&render(&[moved]))[0].1.docked, Some(3));
|
||||
}
|
||||
|
||||
/// Junk costs the line it is on and nothing more.
|
||||
#[test]
|
||||
fn junk_costs_only_its_own_line() {
|
||||
let got = parse("WINDOW\nnot a window line\nWINDOW 0 SIZE 1\nWINDOW 2 SIZE 18\n");
|
||||
assert_eq!(got, vec![(1, look(0, 0, 0, 0, 18))]);
|
||||
assert_eq!(got, vec![(1, look(2, 0, 0, 0, 0, 18))]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1948,10 +1948,34 @@ fn the_combat_log_window_has_the_docks_rect() {
|
||||
"ChatFrame2:{get}() must equal ChatFrame1's — a docked window shares the dock's rect"
|
||||
);
|
||||
}
|
||||
// **Three points, and which three is the assertion.** The XML authors two (TOPLEFT and
|
||||
// BOTTOMRIGHT onto ChatFrame1) and `FCF_DockUpdate` then clears them and applies the
|
||||
// reference's own three for a non-first dock member — TOPLEFT / BOTTOMLEFT / BOTTOMRIGHT,
|
||||
// FloatingChatFrame.lua l.1059-1063. So `3` here means the dock seeding ran and this window is
|
||||
// in `DOCKED_CHAT_FRAMES`; `2` would mean it never did and the rect is standing on the XML
|
||||
// alone. Either way one alone cannot size the frame, which is what 1575 shipped.
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ChatFrame2:GetNumPoints()").unwrap(),
|
||||
3,
|
||||
"FCF_DockUpdate's three points — a 2 means the dock never seeded this window"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(DOCKED_CHAT_FRAMES)")
|
||||
.unwrap(),
|
||||
2,
|
||||
"both authored anchors must survive — one alone cannot size the frame"
|
||||
"the dock is ChatFrame1 + ChatFrame2, seeded from GetChatWindowInfo's stored positions"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SELECTED_DOCK_FRAME:GetName()")
|
||||
.unwrap(),
|
||||
"ChatFrame1"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
"return ChatFrame1.isDocked and ChatFrame2.isDocked and not ChatFrame3.isDocked"
|
||||
)
|
||||
.unwrap(),
|
||||
"FCF_DockFrame set the flag on the dock's two and nothing else"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -415,6 +415,7 @@ fn the_restore_event_repaints_the_window_from_the_store() {
|
||||
a: 255,
|
||||
font_size: 18,
|
||||
locked: true,
|
||||
docked: Some(1),
|
||||
},
|
||||
)]);
|
||||
s.fire_event("UPDATE_CHAT_WINDOWS", vec![]);
|
||||
|
||||
@@ -654,10 +654,17 @@ fn a_line_added_to_chat_frame3_lands_in_chat_frame3_only() {
|
||||
|
||||
/// `FCF_SelectDockFrame(frame)` — the corpus idiom (CustomNameplates.lua:69, Roid-Macros
|
||||
/// Utility.lua:40) is "un-hide the default chat frame before printing into it". It takes a FRAME,
|
||||
/// where ours takes a dock id; and it RAISES on a window benilla cannot dock rather than returning
|
||||
/// quietly and leaving the caller's next AddMessage in a window that is still hidden.
|
||||
/// where `BenillaFCF_SelectDock` takes a dock id.
|
||||
///
|
||||
/// **It used to RAISE on an undocked window and no longer does** (1714). The raise was right for a
|
||||
/// client whose dock was the literal `for i = 1, 2`: "select ChatFrame5" had no meaning, and
|
||||
/// answering with silence would have left the caller's next `AddMessage` in a still-hidden window.
|
||||
/// With a real `DOCKED_CHAT_FRAMES` the reference's own answer applies — the assignment, then
|
||||
/// `FCF_DockUpdate` hiding every docked window (none of them is the selection) and leaving the
|
||||
/// undocked one exactly as it was. Not a nice answer, but the client's; and neither corpus caller
|
||||
/// reaches it, since both pass `DEFAULT_CHAT_FRAME`.
|
||||
#[test]
|
||||
fn fcf_select_dock_frame_selects_by_frame_and_raises_for_an_undocked_one() {
|
||||
fn fcf_select_dock_frame_selects_by_frame_and_leaves_an_undocked_one_alone() {
|
||||
let s = chat_frame();
|
||||
s.run("FCF_SelectDockFrame(ChatFrame2)").unwrap();
|
||||
let (one, two): (bool, bool) = (
|
||||
@@ -673,9 +680,28 @@ fn fcf_select_dock_frame_selects_by_frame_and_raises_for_an_undocked_one() {
|
||||
"the corpus guard brings the default frame back"
|
||||
);
|
||||
|
||||
// An undocked window: no raise, and the reference's consequence — the dock's members all
|
||||
// hide, because none of them is the selection, and ChatFrame5 is untouched (still hidden,
|
||||
// since nothing showed it).
|
||||
let shown5_before: bool = s
|
||||
.eval("return ChatFrame5:IsShown() and true or false")
|
||||
.unwrap();
|
||||
s.run("FCF_SelectDockFrame(ChatFrame5)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<String>("return SELECTED_DOCK_FRAME:GetName()")
|
||||
.unwrap(),
|
||||
"ChatFrame5"
|
||||
);
|
||||
assert!(
|
||||
s.run("FCF_SelectDockFrame(ChatFrame5)").is_err(),
|
||||
"an undocked window raises — benilla has no dock model for it (0288 §2)"
|
||||
!s.eval::<bool>("return ChatFrame1:IsShown()").unwrap()
|
||||
&& !s.eval::<bool>("return ChatFrame2:IsShown()").unwrap(),
|
||||
"FCF_DockUpdate hides every docked window when the selection is not one of them"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<bool>("return ChatFrame5:IsShown() and true or false")
|
||||
.unwrap(),
|
||||
shown5_before,
|
||||
"an undocked window is not in the loop, so nothing shows or hides it"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -108,12 +108,14 @@ pub(super) fn feed_ui_input(
|
||||
);
|
||||
click_consumed.0 = false;
|
||||
let Some(mut script) = script else {
|
||||
capture.0 = false;
|
||||
capture.typing = false;
|
||||
capture.arrows_fall_through = false;
|
||||
payload_held.0 = false;
|
||||
return;
|
||||
};
|
||||
let Ok((window, raw_handle)) = window.single() else {
|
||||
capture.0 = false;
|
||||
capture.typing = false;
|
||||
capture.arrows_fall_through = false;
|
||||
payload_held.0 = false;
|
||||
return;
|
||||
};
|
||||
@@ -221,7 +223,18 @@ pub(super) fn feed_ui_input(
|
||||
// ── Keyboard capture gate ── read AFTER the mouse feed (a LeftButton click may have just focused a
|
||||
// box) but BEFORE feeding keys (an Escape that clears focus is still "captured" this frame, so the
|
||||
// world doesn't also act on it). Gameplay/dev readers run after `UiInput` and see this value.
|
||||
capture.0 = script.has_keyboard_focus();
|
||||
capture.typing = script.has_keyboard_focus();
|
||||
// ── The alt-arrow exemption (wow-re `ignorearrows-alt-arrow-gate.md`, §5 VERIFIED) ─────────
|
||||
// A focused EditBox in alt-arrow mode (`ignoreArrows` in XML, `SetAltArrowKeyMode` in Lua)
|
||||
// does NOT consume LEFT/UP/RIGHT/DOWN unless ALT is held: the reference's handler returns 0
|
||||
// at `0x77b1c4` and the strata walk carries the key down to `CGWorldFrame`, which runs its
|
||||
// binding. That is what lets you turn while the chat box has focus — and the reference's own
|
||||
// chat box ships the flag, so this is the default experience, not an edge case.
|
||||
//
|
||||
// Both terms are whole-frame (one focused box; ALT off the same modifier mirror), so the
|
||||
// exemption is too. Only the four arrows may use it: a focused box swallows every other key
|
||||
// on every other path (`0x77b35e` returns 1).
|
||||
capture.arrows_fall_through = !alt && script.editbox_alt_arrow_mode();
|
||||
|
||||
// ── Keyboard → VM ── the three box-event keys route to `key_input` by name (and never also
|
||||
// as text, so Enter isn't a stray newline); every *editing* key goes through the per-OS
|
||||
@@ -273,7 +286,7 @@ pub(super) fn feed_ui_input(
|
||||
};
|
||||
if let Some(name) = frame_named {
|
||||
if script.frame_key_input(name) {
|
||||
capture.0 = true; // consumed by a frame: its binding must not also fire
|
||||
capture.typing = true; // consumed by a frame: its binding must not also fire
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -297,7 +310,7 @@ pub(super) fn feed_ui_input(
|
||||
else if named.is_none() {
|
||||
if let Some(token) = crate::bindings::chord::key_token(ev.key_code) {
|
||||
if script.frame_key_input(token) {
|
||||
capture.0 = true;
|
||||
capture.typing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,16 +322,31 @@ pub(super) fn feed_ui_input(
|
||||
// reads the capture gate written above, so a key a focused box consumed this frame
|
||||
// never also fires its binding — the real client's ESC precedence, table-wide.
|
||||
// Consumption suppresses the binding — a keyboard FRAME that took this key counts
|
||||
// exactly as a focused box does (decision 1319; `capture.0` above already covers the
|
||||
// exactly as a focused box does (decision 1319; `capture.typing` above already covers the
|
||||
// box case, and this adds the frame one).
|
||||
if script.key_input(name) {
|
||||
capture.0 = true;
|
||||
capture.typing = true;
|
||||
}
|
||||
} else if let Some(chord) = chord {
|
||||
// The gate, on the KEY rather than the action: a gated arrow is not handed to the box
|
||||
// at all, which is the reference's `return 0`. It must not be turned into an
|
||||
// `EditAction` first — `Move { unit: Edge }` reaches the box from HOME/END as well as
|
||||
// from an arrow, and only the arrow is declined.
|
||||
let gated_arrow = capture.arrows_fall_through
|
||||
&& matches!(
|
||||
ev.key_code,
|
||||
KeyCode::ArrowLeft
|
||||
| KeyCode::ArrowRight
|
||||
| KeyCode::ArrowUp
|
||||
| KeyCode::ArrowDown
|
||||
);
|
||||
match chord {
|
||||
keymap::Chord::Edit(action) => {
|
||||
keymap::Chord::Edit(action) if !gated_arrow => {
|
||||
script.editbox_action(action);
|
||||
}
|
||||
// Declined: the key falls through to its binding, which `bindings.rs` allows
|
||||
// because `arrows_fall_through` exempts exactly these four from the typing gate.
|
||||
keymap::Chord::Edit(_) => {}
|
||||
// The clipboard trio needs the OS pasteboard, so it resolves here against the held
|
||||
// [`HostClipboard`]: copy/cut pull the selection out of the box (RF-0082 §4
|
||||
// `0x77e1d0` — selection required; a password box yields its mask run, never the
|
||||
@@ -349,7 +377,7 @@ pub(super) fn feed_ui_input(
|
||||
// row IS a binding (the action buttons), so a digit typed into a keyboard frame —
|
||||
// the stack-split spinner — must not also fire action button 3 (decision 1319).
|
||||
if script.char_input(text) {
|
||||
capture.0 = true;
|
||||
capture.typing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,8 @@ pub(crate) fn end_ui_session(world: &mut World) {
|
||||
hover.0 = None;
|
||||
}
|
||||
if let Some(mut keys) = world.get_resource_mut::<UiKeyboardCapture>() {
|
||||
keys.0 = false;
|
||||
keys.typing = false;
|
||||
keys.arrows_fall_through = false;
|
||||
}
|
||||
if let Some(mut held) = world.get_resource_mut::<CursorPayloadHeld>() {
|
||||
*held = CursorPayloadHeld::default();
|
||||
|
||||
@@ -184,7 +184,21 @@ pub(crate) struct PlayerUiHover(pub(crate) Option<u32>);
|
||||
/// §1). One mechanism, written once here; every reader is ordered after `UiInput` so it sees this
|
||||
/// frame's value, not last frame's.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct UiKeyboardCapture(pub(crate) bool);
|
||||
pub(crate) struct UiKeyboardCapture {
|
||||
/// True while a focused EditBox is eating every key.
|
||||
pub(crate) typing: bool,
|
||||
/// **The four arrow keys are exempt this frame** — the focused box is in alt-arrow mode
|
||||
/// (`ignoreArrows` / `SetAltArrowKeyMode`) and ALT is not held, so the reference's own key
|
||||
/// handler declines LEFT/UP/RIGHT/DOWN at `0x77b1c4` and the strata walk carries them down to
|
||||
/// `CGWorldFrame`, which runs their bindings. That is what lets you turn while the chat box
|
||||
/// has focus (wow-re `ignorearrows-alt-arrow-gate.md`, §5 VERIFIED).
|
||||
///
|
||||
/// It is a whole-frame flag rather than a per-key one because both of its terms are:
|
||||
/// there is at most one focused box, and ALT is read off the same modifier mirror. Only the
|
||||
/// four arrows may use it — every other key a focused box still swallows, since the
|
||||
/// reference's handler returns 1 on every other path (`0x77b35e`).
|
||||
pub(crate) arrows_fall_through: bool,
|
||||
}
|
||||
|
||||
/// Set each frame by [`feed_ui_input`]: true for a LEFT press this frame that hit no frame but was
|
||||
/// consumed by the UI — the world-drop of a held cursor payload over EMPTY world (0216 §3,
|
||||
|
||||
@@ -355,7 +355,9 @@ impl Loader<'_> {
|
||||
("numeric", "SetNumeric"),
|
||||
("password", "SetPassword"),
|
||||
("multiLine", "SetMultiLine"),
|
||||
("ignoreArrows", "SetIgnoreArrows"),
|
||||
// The XML spelling of the alt-arrow flag; the Lua spelling is `SetAltArrowKeyMode`,
|
||||
// and there is one flag behind both (wow-re `ignorearrows-alt-arrow-gate.md`).
|
||||
("ignoreArrows", "SetAltArrowKeyMode"),
|
||||
] {
|
||||
if let Some(on) = el.attr_bool_opt(attr) {
|
||||
self.call(wrapper, method, on, dbg);
|
||||
|
||||
@@ -177,6 +177,15 @@ pub struct ChatWindowLook {
|
||||
/// persisted state nothing could change, which is the honest-tree rule (1134 §4) at the
|
||||
/// persistence layer, and the reason 1589 §6 left it out.
|
||||
pub locked: bool,
|
||||
/// The cache's `DOCKED` — the window's **position in the dock**, 1-based, or `None` for a
|
||||
/// window that is not docked. Not a flag: window 1 answers `1` and window 2 `2`, which is the
|
||||
/// index `FCF_DockFrame(frame, docked)` takes (this module's §3 note), and the order the tabs
|
||||
/// sit in.
|
||||
///
|
||||
/// It joined this struct with the dock verbs (1714), by the same rule `locked` did: until
|
||||
/// `FCF_DockFrame`/`FCF_UnDockFrame` existed there was nothing that could move it, so it was a
|
||||
/// per-window constant read straight out of [`WINDOW_STATE`]. There is now, so it is state.
|
||||
pub docked: Option<u8>,
|
||||
}
|
||||
|
||||
impl Default for ChatWindowLook {
|
||||
@@ -194,7 +203,25 @@ impl ChatWindowLook {
|
||||
a: 0,
|
||||
font_size: 0,
|
||||
locked: true,
|
||||
// The stock row's `DOCKED` differs per window, so the array literal that uses this const
|
||||
// seeds each slot from [`WINDOW_STATE`] afterwards. `None` here is only the value a
|
||||
// slot holds before that seeding, and the value a *hand-built* look starts at.
|
||||
docked: None,
|
||||
};
|
||||
|
||||
/// The stock row for window `index` (0-based) — [`Self::DEFAULT`] with that window's own
|
||||
/// `DOCKED` position from [`WINDOW_STATE`]. The seed for the model's array and for the
|
||||
/// settings parser's per-line default, so a file that never mentions `DOCKED` keeps the
|
||||
/// shipped dock rather than silently undocking both windows.
|
||||
pub fn stock(index: usize) -> Self {
|
||||
Self {
|
||||
docked: WINDOW_STATE
|
||||
.get(index)
|
||||
.and_then(|(_, d)| *d)
|
||||
.and_then(|d| u8::try_from(d).ok()),
|
||||
..Self::DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `x` in the reference's `0..1` float domain → the byte its `__ftol(x · 255.0)` stores, clamped
|
||||
@@ -282,7 +309,10 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.app_data_ref::<Model>()
|
||||
.expect("model app_data")
|
||||
.chat_window_looks[i];
|
||||
let (shown, docked) = WINDOW_STATE[i];
|
||||
let (shown, _) = WINDOW_STATE[i];
|
||||
// `docked` is the LIVE position now, not the table's — `FCF_DockFrame`/
|
||||
// `FCF_UnDockFrame` move it through `SetChatWindowDocked` below.
|
||||
let docked = look.docked.map(i64::from);
|
||||
let num = |v: Option<i64>| match v {
|
||||
Some(n) => Value::Integer(n),
|
||||
None => Value::Nil,
|
||||
@@ -386,6 +416,37 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// SetChatWindowDocked(id, position) — the dock half of the record, written by `FCF_SaveDock`
|
||||
// (FloatingChatFrame.lua l.1276-1285) once per docked window on every dock change, and by
|
||||
// `FCF_UnDockFrame` (l.1182) with `nil` to clear it.
|
||||
//
|
||||
// **The argument is a POSITION, not a flag** — this module's §3 trap, and the reason this
|
||||
// takes a number rather than the `bool` `SetChatWindowLocked` takes: `FCF_SaveDock` passes the
|
||||
// running `count`, so window 1 stores `1` and window 2 stores `2`, and that is what
|
||||
// `GetChatWindowInfo`'s 9th return hands back to `FCF_DockFrame(frame, docked)` as its index.
|
||||
// A `nil` clears it (undocked); anything `<= 0` is treated as a clear too, since a dock
|
||||
// position is 1-based and the reference has no zeroth slot.
|
||||
//
|
||||
// **Not persisted yet, and that is a stated gap.** The `chat-cache` writer
|
||||
// (`benilla_app`'s `ui_chat::settings`) renders `DOCKED` alongside `LOCKED`, but an absent key
|
||||
// must keep the *per-window* stock rather than one flat default — see
|
||||
// [`ChatWindowLook::stock`], which is what the parser seeds from.
|
||||
lua.globals().set(
|
||||
"SetChatWindowDocked",
|
||||
lua.create_function(|lua, (id, position): (i64, Option<i64>)| {
|
||||
let i = window_index(id)?;
|
||||
let position = position
|
||||
.filter(|p| *p > 0)
|
||||
.and_then(|p| u8::try_from(p).ok());
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let look = &mut model.chat_window_looks[i];
|
||||
if position != look.docked {
|
||||
look.docked = position;
|
||||
model.chat_window_changes.insert(i);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// SetChatWindowSize(id, fontSize) — 0x4a1470.
|
||||
//
|
||||
// FrameXML's caller is `FCF_SetChatWindowFontSize` (l.752-763), which does the visible half
|
||||
@@ -679,6 +740,7 @@ mod tests {
|
||||
a: 4,
|
||||
font_size: 14,
|
||||
locked: true,
|
||||
docked: Some(1),
|
||||
},
|
||||
)]);
|
||||
assert!(
|
||||
|
||||
@@ -137,13 +137,11 @@ pub(super) fn action(lua: &Lua, a: EditAction) -> bool {
|
||||
};
|
||||
match a {
|
||||
EditAction::Move { unit, back, extend } => match unit {
|
||||
// ignoreArrows: consumed but inert (guard `0x77b18e`) — arrows are the only chord
|
||||
// source of Char moves, and the ref's Ctrl bypass maps to Word, which passes.
|
||||
EditUnit::Char => {
|
||||
if !with_eb(lua, h, |eb| eb.ignore_arrows).unwrap_or(false) {
|
||||
move_horizontal(lua, h, !back, extend);
|
||||
}
|
||||
}
|
||||
// No alt-arrow test here: the gate is on the KEY and lives in the host, which declines
|
||||
// the four arrow codes before the chord is ever built (`editbox_alt_arrow_mode`).
|
||||
// Anything reaching this arm was Alt-held or was not an arrow, and the reference moves
|
||||
// the caret for both.
|
||||
EditUnit::Char => move_horizontal(lua, h, !back, extend),
|
||||
// Ctrl/Option picks the word-granular cursor helper (RF-0082 §4: "char- vs
|
||||
// word-granular by the Ctrl check").
|
||||
EditUnit::Word => interact::move_word(lua, h, !back, extend),
|
||||
@@ -159,8 +157,11 @@ pub(super) fn action(lua: &Lua, a: EditAction) -> bool {
|
||||
// restored past the newest. Single-line only (benilla's multiLine box has no vertical
|
||||
// caret nav — survey gap — a multiLine box consumes the step inert). The recall chords
|
||||
// are the host keymap's plain Up/Down (rf82's history controller is untraced; decision
|
||||
// 0301) — `ignoreArrows` does not gate them: the ref chat box ships BOTH ignoreArrows and
|
||||
// historyLines, so arrows-as-history is exactly what that combination leaves.
|
||||
// 0301). The alt-arrow gate DOES cover them, upstream: UP and DOWN are two of the four
|
||||
// codes it declines, so on a flagged box — which the reference's own chat box is —
|
||||
// history recall is **Alt**+Up/Down and a plain Up/Down turns the camera. This file used
|
||||
// to say the opposite ("`ignoreArrows` does not gate them"), which followed from reading
|
||||
// the flag as consume-but-inert; the §5 corrected both halves.
|
||||
EditAction::HistoryPrev | EditAction::HistoryNext => {
|
||||
if !with_eb(lua, h, |eb| eb.multi_line).unwrap_or(false) {
|
||||
history_step_key(lua, h, a == EditAction::HistoryPrev);
|
||||
|
||||
@@ -187,9 +187,47 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// The config flags. SetAutoFocus/SetNumeric/SetPassword/SetMultiLine mirror the live API;
|
||||
// SetIgnoreArrows has no client method (the flag is XML-only) — a benilla convenience so the
|
||||
// loader can drive every flag through one method surface.
|
||||
// The config flags. All four mirror the live API. The fifth flag — bit4, the XML's
|
||||
// `ignoreArrows` — is NOT here: its Lua surface is `SetAltArrowKeyMode`/`GetAltArrowKeyMode`
|
||||
// below, which take a different argument coercion and answer a number rather than a boolean,
|
||||
// so it cannot share this loop. benilla used to register a `SetIgnoreArrows` "convenience"
|
||||
// for the loader to drive; 5875 has no such method (the 48-entry table
|
||||
// `[0x87bb68, 0x87bce8)` carries no entry whose name contains "Ignore"), so publishing it was
|
||||
// exactly the error decision 1189 names — and it stood in for the two real names, which were
|
||||
// missing. The loader drives the XML attribute through `SetAltArrowKeyMode` now.
|
||||
// ── SetAltArrowKeyMode / GetAltArrowKeyMode (`0x7996e0` / `0x799790`) ────────────────────
|
||||
//
|
||||
// **The setter's argument is `GetBoolOrDefault(L, 2, default = 1)`** (`0x6f1c10`), not Lua
|
||||
// truthiness and not a plain numeric coercion — `0x7996e0` pushes the default `1` before the
|
||||
// call, so **an absent argument ENABLES** the mode. `nil` disables; a number goes through
|
||||
// `__ftol` so `0` and `0.5` are false and `-1` is true; `""` matches nothing in the
|
||||
// off/disabled/on/enabled chain and falls to the default, so it ENABLES; `"0"` disables.
|
||||
// [`crate::script::binding_abi::bool_or_default`] already models every arm.
|
||||
//
|
||||
// **The getter answers the NUMBER 1 or nil**, never a boolean (`0x799815`: the set arm pushes
|
||||
// the double 1.0 via `0x6f3810`, the clear arm pushes nil via `0x6f37f0`) — the same idiom as
|
||||
// `IsShiftKeyDown`. An addon writing `if box:GetAltArrowKeyMode() then` reads either the same;
|
||||
// one writing `== 1` only reads the number.
|
||||
m.set(
|
||||
"SetAltArrowKeyMode",
|
||||
lua.create_function(|lua, (this, args): (Table, mlua::MultiValue)| {
|
||||
// `MultiValue`, not `Value`, because the reference DISTINGUISHES absent from nil here
|
||||
// and mlua's `Value` cannot: `0x7996e0` pushes the default `1` before the call, so
|
||||
// `SetAltArrowKeyMode()` enables while `SetAltArrowKeyMode(nil)` disables.
|
||||
let args: Vec<Value> = args.into_iter().collect();
|
||||
let on = crate::script::binding_abi::bool_or_default(args.first(), true);
|
||||
with_editbox(lua, &this, |eb| eb.alt_arrow_key_mode = on)?;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetAltArrowKeyMode",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let on = with_editbox(lua, &this, |eb| eb.alt_arrow_key_mode)?;
|
||||
Ok(if on { Value::Integer(1) } else { Value::Nil })
|
||||
})?,
|
||||
)?;
|
||||
|
||||
for (name, set) in flag_setters() {
|
||||
let refresh_justify = name == "SetMultiLine";
|
||||
m.set(
|
||||
@@ -307,12 +345,11 @@ fn install_font_block(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
type FlagSetter = (&'static str, fn(&mut EditBoxState, bool));
|
||||
|
||||
/// The config-flag setters, in one place so the loader and the Lua surface share the list.
|
||||
fn flag_setters() -> [FlagSetter; 5] {
|
||||
fn flag_setters() -> [FlagSetter; 4] {
|
||||
[
|
||||
("SetAutoFocus", |eb, on| eb.auto_focus = on),
|
||||
("SetNumeric", |eb, on| eb.numeric = on),
|
||||
("SetPassword", |eb, on| eb.password = on),
|
||||
("SetMultiLine", |eb, on| eb.multi_line = on),
|
||||
("SetIgnoreArrows", |eb, on| eb.ignore_arrows = on),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -468,20 +468,83 @@ fn multiline_enter_inserts_a_newline_without_onspacepressed() {
|
||||
assert_eq!(s.eval::<i64>("return spaces").unwrap(), 0);
|
||||
}
|
||||
|
||||
/// **Alt-arrow mode: the two real verbs, and the flag they share with the XML attribute.**
|
||||
///
|
||||
/// The §5 (`ignorearrows-alt-arrow-gate.md`) settled four things this pins:
|
||||
///
|
||||
/// · 5875 has **no `SetIgnoreArrows`** — the 48-entry EditBox method table
|
||||
/// `[0x87bb68, 0x87bce8)` carries `SetAltArrowKeyMode`/`GetAltArrowKeyMode` at 46/47 and no
|
||||
/// entry whose name contains "Ignore". benilla published the invented name for two rounds and
|
||||
/// was missing both real ones.
|
||||
/// · The XML attribute `ignoreArrows` and the Lua verbs drive **one** flag (`[E+0x318] & 0x10`).
|
||||
/// · The setter's argument is `GetBoolOrDefault(L, 2, default = 1)` (`0x6f1c10`), not Lua
|
||||
/// truthiness — an **absent** argument ENABLES, `""` ENABLES, `0` and `"0"` disable.
|
||||
/// · The getter answers the **number 1 or nil**, never a boolean.
|
||||
///
|
||||
/// And the negative that matters: the engine core no longer swallows anything. The gate is on the
|
||||
/// KEY and lives in the host (`UiKeyboardCapture::arrows_fall_through`), so an `EditAction` that
|
||||
/// arrives here moves the caret whatever the flag says — it only ever arrives when ALT was held or
|
||||
/// the key was not an arrow.
|
||||
#[test]
|
||||
fn ignore_arrows_swallows_char_moves_but_not_word_or_edge() {
|
||||
fn alt_arrow_key_mode_is_the_flag_and_the_engine_core_no_longer_swallows_moves() {
|
||||
let mut s = script();
|
||||
s.run(
|
||||
r#"
|
||||
E = CreateFrame("EditBox", "E")
|
||||
E:SetIgnoreArrows(true)
|
||||
E:SetText("abc")
|
||||
E:SetFocus()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// ignoreArrows: a Char move (a plain arrow) is consumed but inert; the cursor stays put — a
|
||||
// subsequent BACKSPACE removes the char before end-of-text ('c'), proving it didn't move.
|
||||
|
||||
// The invented name is gone.
|
||||
assert!(
|
||||
s.eval::<bool>("return E.SetIgnoreArrows == nil").unwrap(),
|
||||
"5875 has no SetIgnoreArrows — publishing it was decision 1189's error"
|
||||
);
|
||||
|
||||
// The getter: a number or nil, never a boolean.
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return E:GetAltArrowKeyMode()")
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
s.run("E:SetAltArrowKeyMode(1)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return E:GetAltArrowKeyMode()")
|
||||
.unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return type(E:GetAltArrowKeyMode()) == 'number'")
|
||||
.unwrap(),
|
||||
"the set arm pushes the double 1.0 (0x6f3810), not a boolean"
|
||||
);
|
||||
|
||||
// `GetBoolOrDefault(default = 1)`, arm by arm — three of these are backwards under Lua
|
||||
// truthiness, which is why the coercion has its own helper.
|
||||
for (arg, want) in [
|
||||
("", Some(1)), // ABSENT -> the default 1 -> enabled
|
||||
("nil", None), // nil -> 0
|
||||
("0", None), // __ftol(0) -> false
|
||||
("-1", Some(1)), // __ftol(-1) != 0 -> true
|
||||
(r#""0""#, None), // the string "0" -> false
|
||||
(r#""""#, Some(1)), // "" matches no arm -> the default -> enabled
|
||||
] {
|
||||
s.run("E:SetAltArrowKeyMode(nil)").unwrap();
|
||||
s.run(&format!("E:SetAltArrowKeyMode({arg})")).unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return E:GetAltArrowKeyMode()")
|
||||
.unwrap(),
|
||||
want,
|
||||
"SetAltArrowKeyMode({arg})"
|
||||
);
|
||||
}
|
||||
|
||||
// The engine core does NOT swallow a Char move any more — the gate is on the key, upstream.
|
||||
// `SetText` leaves the caret at the end, so one back-Char move puts it between 'b' and 'c'.
|
||||
s.run(r#"E:SetAltArrowKeyMode(1) E:SetText("abc")"#)
|
||||
.unwrap();
|
||||
assert!(s.editbox_action(EditAction::Move {
|
||||
unit: EditUnit::Char,
|
||||
back: true,
|
||||
@@ -491,21 +554,24 @@ fn ignore_arrows_swallows_char_moves_but_not_word_or_edge() {
|
||||
unit: EditUnit::Char,
|
||||
back: true
|
||||
}));
|
||||
assert_eq!(s.eval::<String>("return E:GetText()").unwrap(), "ab");
|
||||
// The ref's Ctrl bypass (guard 0x77b18e) maps to Word — which passes the gate; Edge too.
|
||||
s.editbox_action(EditAction::Move {
|
||||
unit: EditUnit::Word,
|
||||
back: true,
|
||||
extend: false,
|
||||
});
|
||||
assert!(s.editbox_action(EditAction::Delete {
|
||||
unit: EditUnit::Char,
|
||||
back: false
|
||||
}));
|
||||
assert_eq!(
|
||||
s.eval::<String>("return E:GetText()").unwrap(),
|
||||
"b",
|
||||
"the word move must have reached text start"
|
||||
"ac",
|
||||
"the caret moved, so BACKSPACE took 'b' — a flagged box that is handed the action acts on it"
|
||||
);
|
||||
}
|
||||
|
||||
/// The XML attribute is the same flag under its other name (`ignoreArrows` occurs once in the
|
||||
/// image, at `0x879b78`, with one xref: the attribute push at `0x77a13a`).
|
||||
#[test]
|
||||
fn the_ignore_arrows_xml_attribute_is_alt_arrow_key_mode() {
|
||||
let s = script();
|
||||
s.run(r#"E = CreateFrame("EditBox", "E")"#).unwrap();
|
||||
s.run("E:SetAltArrowKeyMode(1)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<Option<i64>>("return E:GetAltArrowKeyMode()")
|
||||
.unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,20 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
// SetMaskTexture(path) — the disc's mask art. A real 1.12 method (the name is in the 5875
|
||||
// image) with no getter beside it, so this is write-only from Lua, exactly as there.
|
||||
//
|
||||
// An absent or empty path restores the engine default rather than leaving the map
|
||||
// unmasked: `SetMaskTexture` is how a UI replaces the circle, never how it removes one,
|
||||
// and a nil-means-square reading would hand every mistyped path a square minimap with no
|
||||
// error. (pfUI passes a real path; this is about the failure mode, not about pfUI.)
|
||||
"SetMaskTexture",
|
||||
lua.create_function(|lua, (this, path): (Table, Option<String>)| {
|
||||
let path = path.filter(|p| !p.is_empty());
|
||||
with_minimap(lua, &this, |m| m.mask_texture = path)
|
||||
})?,
|
||||
)?;
|
||||
m.set(
|
||||
"GetZoomLevels",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
|
||||
@@ -829,6 +829,23 @@ impl UiScript {
|
||||
self.model_mut().minimap_ping_request.take()
|
||||
}
|
||||
|
||||
/// The mask art the Minimap widget asks its renderer for — `Minimap:SetMaskTexture`'s value,
|
||||
/// or `None` while no widget has set one (the app then keeps
|
||||
/// [`crate::widget::MINIMAP_DEFAULT_MASK`]).
|
||||
///
|
||||
/// The **first** Minimap in the registry answers, not every one: the mask is a property of the
|
||||
/// map the app draws, and the app draws one. (Everything else in `MinimapState` is per widget
|
||||
/// because the Lua API reads it back per widget; this has no getter in 1.12 to read back.)
|
||||
pub fn minimap_mask_texture(&self) -> Option<String> {
|
||||
let model = self.model_ref();
|
||||
model.arena.minimap_kinds().iter().find_map(|&h| {
|
||||
match model.arena.frame(h).map(|f| &f.kind_state) {
|
||||
Some(KindState::Minimap(m)) => m.mask_texture.clone(),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Monotonic count of Minimap widgets ever created in this VM — the O(1) signal that a new
|
||||
/// one exists and needs the containment state pushed.
|
||||
pub fn minimap_widgets_created(&self) -> u64 {
|
||||
@@ -1216,6 +1233,29 @@ impl UiScript {
|
||||
editbox::action(&self.lua, action)
|
||||
}
|
||||
|
||||
/// Is the focused EditBox in **alt-arrow mode** — the flag the XML spells `ignoreArrows` and
|
||||
/// the Lua surface spells `SetAltArrowKeyMode` (`[editbox+0x318] & 0x10`)?
|
||||
///
|
||||
/// The host asks this to decide whether an arrow key reaches the box at all. With the flag set
|
||||
/// and ALT not held, the reference's key handler returns 0 at `0x77b1c4` for the four arrow
|
||||
/// codes — **not consumed** — and the strata walk carries down to `CGWorldFrame`, which runs
|
||||
/// the key's binding. That is what lets you turn while the chat box has focus, and it is why
|
||||
/// the gate is on the key rather than on the [`EditAction`]: `Move { unit: Edge }` reaches the
|
||||
/// box from HOME/END as well as from an arrow, and only the arrow is gated.
|
||||
///
|
||||
/// `false` when nothing is focused, which is the same answer as an unflagged box: neither
|
||||
/// declines the key.
|
||||
pub fn editbox_alt_arrow_mode(&self) -> bool {
|
||||
let model = self.model_ref();
|
||||
model.focused_editbox.is_some_and(|h| {
|
||||
model.arena.frame(h).is_some_and(|f| {
|
||||
f.effective_visible
|
||||
&& matches!(&f.kind_state,
|
||||
crate::widget::KindState::EditBox(eb) if eb.alt_arrow_key_mode)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether an EditBox currently holds keyboard focus (and is effectively visible) — the app gates
|
||||
/// world/player key input on this, matching the client's `DAT_00cf4dc8 != 0` test (RF-0082 §1).
|
||||
pub fn has_keyboard_focus(&self) -> bool {
|
||||
|
||||
@@ -1494,8 +1494,9 @@ impl Model {
|
||||
petition_requests: Vec::new(),
|
||||
tell_requests: Vec::new(),
|
||||
open_chat_requests: Vec::new(),
|
||||
chat_window_looks: [chat_window::ChatWindowLook::DEFAULT;
|
||||
chat_window::NUM_CHAT_WINDOWS],
|
||||
// Seeded per window, because the stock `DOCKED` position is not the same for all
|
||||
// seven (`ChatWindowLook::stock`): 1 and 2 are the dock, 3..7 are undocked.
|
||||
chat_window_looks: std::array::from_fn(chat_window::ChatWindowLook::stock),
|
||||
chat_window_changes: HashSet::new(),
|
||||
user_placed_changed: false,
|
||||
default_language: None,
|
||||
|
||||
@@ -260,3 +260,36 @@ fn a_minimap_is_born_with_nine_model_children_and_the_ninth_is_the_player_arrow(
|
||||
.unwrap()
|
||||
.is_none_or(|p| p.is_empty()));
|
||||
}
|
||||
|
||||
/// **`Minimap:SetMaskTexture` is state, and an empty path restores the default rather than
|
||||
/// unmasking the map.**
|
||||
///
|
||||
/// A real 1.12 method (the name is in the 5875 image) with no getter beside it, so the readback
|
||||
/// here is through the arena — the same shape `simplehtml`'s and `modelframe`'s tests use for a
|
||||
/// write-only verb. pfUI's `modules/minimap.lua:27` is the caller that matters: swapping this art
|
||||
/// is how its square minimap becomes square.
|
||||
#[test]
|
||||
fn set_mask_texture_is_state_and_empty_restores_the_default() {
|
||||
let s = script();
|
||||
s.run(r#"m = CreateFrame("Minimap", "TestMinimap")"#)
|
||||
.unwrap();
|
||||
assert_eq!(s.minimap_mask_texture(), None, "fresh = the engine default");
|
||||
|
||||
s.run(r#"m:SetMaskTexture("Interface\\AddOns\\pfUI\\img\\minimap")"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.minimap_mask_texture().as_deref(),
|
||||
Some("Interface\\AddOns\\pfUI\\img\\minimap")
|
||||
);
|
||||
|
||||
// Empty and nil both mean "back to the engine's circle" — never "no mask at all". A
|
||||
// nil-means-unmasked reading would hand every mistyped path a silently square minimap.
|
||||
s.run(r#"m:SetMaskTexture("")"#).unwrap();
|
||||
assert_eq!(s.minimap_mask_texture(), None);
|
||||
s.run(r#"m:SetMaskTexture("Interface\\Foo") m:SetMaskTexture(nil)"#)
|
||||
.unwrap();
|
||||
assert_eq!(s.minimap_mask_texture(), None);
|
||||
|
||||
// There is no getter in 1.12, and we do not invent one (decision 1189).
|
||||
assert!(s.eval::<bool>("return m.GetMaskTexture == nil").unwrap());
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub enum EditUnit {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EditAction {
|
||||
/// Move the caret one `unit` back/forward; `extend` drags the selection from its fixed
|
||||
/// anchor (the Shift family). `Char` moves honor `ignoreArrows` (consumed but inert — the
|
||||
/// anchor (the Shift family). (The alt-arrow gate is upstream, on the key — the
|
||||
/// ref guard `0x77b18e`; arrows are the only chord source of `Char` moves).
|
||||
Move {
|
||||
unit: EditUnit,
|
||||
@@ -92,9 +92,26 @@ pub struct EditBoxState {
|
||||
/// `password` (bit3): the display string is one `'*'` per *character* (the mask, `E+0x334`); the
|
||||
/// real text is untouched.
|
||||
pub password: bool,
|
||||
/// `ignoreArrows` (bit4): LEFT/RIGHT (and UP/DOWN) are consumed but do nothing unless Ctrl is
|
||||
/// held (RF-0082 §4, guard `0x77b18e`).
|
||||
pub ignore_arrows: bool,
|
||||
/// **Alt-arrow mode** (bit4) — the XML attribute spells it `ignoreArrows`, the Lua surface
|
||||
/// spells it `SetAltArrowKeyMode`/`GetAltArrowKeyMode` (`0x7996e0`/`0x799790`), and there is
|
||||
/// exactly one flag behind both names. `SetIgnoreArrows` does **not** exist in 5875: the
|
||||
/// 48-entry EditBox method table `[0x87bb68, 0x87bce8)` carries the Alt pair at 46/47 and no
|
||||
/// entry whose name contains "Ignore" (wow-re `ignorearrows-alt-arrow-gate.md`, §5 VERIFIED).
|
||||
///
|
||||
/// **What it does, and it is not what "ignore" suggests.** With the flag set, the four arrow
|
||||
/// keys (`0x204` LEFT / `0x205` UP / `0x206` RIGHT / `0x207` DOWN) are **not consumed by the
|
||||
/// box at all** unless ALT is held — the guard `0x77b18e` returns 0, the strata walk carries
|
||||
/// on down to `CGWorldFrame`, and the key reaches its binding (`TURNLEFT`, `MOVEFORWARD`).
|
||||
/// That is the whole point: it is what lets you turn while the chat box has focus. With Alt
|
||||
/// held the guard falls through to ordinary handling and the caret moves.
|
||||
///
|
||||
/// A focused box otherwise consumes **everything** (the handler's shared tail `0x77b35e`
|
||||
/// returns 1), so these four keys are the only ones it ever lets past.
|
||||
///
|
||||
/// benilla read this as "consumed but inert, unless Ctrl" for two rounds — the modifier was
|
||||
/// wrong (a wow-re note said `0x41f8f0(2)` was Ctrl; it is ALT, and five other notes already
|
||||
/// said so) and so was the consumption. Both corrected by the §5.
|
||||
pub alt_arrow_key_mode: bool,
|
||||
/// `maxLetters` (`E+0x340`, 0 = unlimited): after each insert, trim from the end while the
|
||||
/// *letter* (char) count exceeds this (RF-0082 §3).
|
||||
pub max_letters: usize,
|
||||
@@ -254,7 +271,7 @@ impl Default for EditBoxState {
|
||||
multi_line: false,
|
||||
numeric: false,
|
||||
password: false,
|
||||
ignore_arrows: false,
|
||||
alt_arrow_key_mode: false,
|
||||
max_letters: 0,
|
||||
max_bytes: None,
|
||||
text_region: None,
|
||||
@@ -590,13 +607,12 @@ impl EditBoxState {
|
||||
match action {
|
||||
EditAction::Move { unit, back, extend } => {
|
||||
match unit {
|
||||
// `ignoreArrows`: consumed but inert (guard `0x77b18e`). Arrows are the only
|
||||
// chord source of a Char move, and the ref's Ctrl bypass maps to Word.
|
||||
EditUnit::Char => {
|
||||
if !self.ignore_arrows {
|
||||
self.move_by_char(!back, extend);
|
||||
}
|
||||
}
|
||||
// No flag test here any more. The alt-arrow gate is on the KEY, not on the
|
||||
// action — with the flag set the box never sees the arrow at all, because the
|
||||
// host declines it before the chord is dispatched (`UiScript::
|
||||
// editbox_alt_arrow_fallthrough`). Anything that reaches this far was either
|
||||
// Alt-held or not an arrow, and both move the caret normally.
|
||||
EditUnit::Char => self.move_by_char(!back, extend),
|
||||
EditUnit::Word => self.move_by_word(!back, extend),
|
||||
EditUnit::Edge => self.move_to_edge(!back, extend),
|
||||
}
|
||||
|
||||
@@ -392,6 +392,15 @@ pub struct MinimapState {
|
||||
/// Is the player inside a WMO interior (the client's `0xceaa60`)? Selects which index the Lua
|
||||
/// zoom API reads and writes. Pushed down by the app, which owns the WMO containment test.
|
||||
pub inside: bool,
|
||||
/// The mask art the disc is cut to — `Minimap:SetMaskTexture(path)`, a real 1.12 method (the
|
||||
/// name is in the 5875 image; there is no `GetMaskTexture` beside it, so this is write-only
|
||||
/// from Lua). `None` = the engine default, [`MINIMAP_DEFAULT_MASK`].
|
||||
///
|
||||
/// State-only here, pixels app-side, like the zoom index (0203): the app already masks the
|
||||
/// disc through its own `UiQuadMask`, and this only decides which texture it loads. It is what
|
||||
/// makes a **square minimap** possible, which is the single most recognisable thing pfUI does
|
||||
/// to the default UI (`modules/minimap.lua:27`).
|
||||
pub mask_texture: Option<String>,
|
||||
/// The **player-arrow** `Model` — the client's `[Minimap+0x338]`, ninth and last of the nine
|
||||
/// engine children the ctor builds ([`MINIMAP_ENGINE_CHILDREN`]). An explicit slot because that
|
||||
/// is the shape the client keeps: `CMinimap::SetPlayerFacing 0x4eb8e0` reaches the arrow through
|
||||
@@ -415,6 +424,7 @@ impl Default for MinimapState {
|
||||
zoom: MINIMAP_DEFAULT_ZOOM,
|
||||
inside_zoom: MINIMAP_DEFAULT_ZOOM,
|
||||
inside: false,
|
||||
mask_texture: None,
|
||||
// Filled by the arena the moment the widget is inserted — the nine are ctor-time, so a
|
||||
// Minimap is never observably without them (`WidgetArena::create`).
|
||||
player_arrow: None,
|
||||
@@ -445,6 +455,11 @@ impl MinimapState {
|
||||
/// The client's minimap zoom-level count (`get_zoom_levels` `0x6da9a0` returns the constant 6).
|
||||
pub const MINIMAP_ZOOM_LEVELS: u8 = 6;
|
||||
|
||||
/// The engine's own minimap mask — the circle `Minimap:SetMaskTexture` replaces. benilla's
|
||||
/// renderer has loaded this path since the minimap existed; naming it here is what lets the Lua
|
||||
/// setter override it.
|
||||
pub const MINIMAP_DEFAULT_MASK: &str = "Textures\\MinimapMask";
|
||||
|
||||
/// **Nine `Model` children, built by the `CMinimap` ctor before anything else sees the widget** —
|
||||
/// wow-re `ui/scratch/widget-list-bindings.md` §5 / `ui/ui.md`, VERIFIED. The ctor `0x4edbc0`
|
||||
/// (TU `Ui\MinimapFrame.cpp`) allocates `CSimpleModel` (`0x76c8e0`) nine times with the Minimap as
|
||||
|
||||
@@ -177,9 +177,9 @@ pub use kinds::{
|
||||
EditAction, EditBoxState, EditOutcome, EditUnit, FrameKind, InsertMode, KindState,
|
||||
MessageFrameState, MessageLine, MinimapState, ModelState, RegionKind, ScrollFrameState,
|
||||
ScrollingMessageState, SliderState, StatusBarState, TooltipState, COOLDOWN_FLASH_SECS,
|
||||
MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_PLAYER_MODEL, MINIMAP_DEFAULT_ZOOM,
|
||||
MINIMAP_ENGINE_CHILDREN, MINIMAP_ZOOM_LEVELS, TOOLTIP_DOUBLE_GAP, TOOLTIP_FADE_SECS,
|
||||
TOOLTIP_LINE_GAP, TOOLTIP_PAD, TOOLTIP_WRAP_WIDTH,
|
||||
MINIMAP_DEFAULT_ARROW_MODEL, MINIMAP_DEFAULT_MASK, MINIMAP_DEFAULT_PLAYER_MODEL,
|
||||
MINIMAP_DEFAULT_ZOOM, MINIMAP_ENGINE_CHILDREN, MINIMAP_ZOOM_LEVELS, TOOLTIP_DOUBLE_GAP,
|
||||
TOOLTIP_FADE_SECS, TOOLTIP_LINE_GAP, TOOLTIP_PAD, TOOLTIP_WRAP_WIDTH,
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user