From 26fb25a3f8b42870bd7bc8894686599a05eeece5 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 25 Aug 2026 18:49:16 +0700 Subject: [PATCH] the colour wheel draws, chat windows move, and the minimap ping returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the colour picker can pick a colour: the hue disc and the value strip are engine-generated pixels (no BLP in the chain is a wheel), reproduced from the reference's own lattice — a hard-cut circle at a literal V = 1.0, the brightness marker moving over a disc that never dims - chat windows move and resize once unlocked (Lock/Unlock Window on the tab menu; the reference's own 296×75 to 608×400 bounds), the border textures doubling as the grips, and the placement is remembered per character in benilla-config/layout; both windows ship locked, as 1.12's reset leaves them - the minimap ping is back and pinned: one world point the engine draws, the click converted in the right unit (the old path was ~27% off at 1080p), and no 10-yard auto-clear — that was the gossip marker's rule, and walking to your own ping was deleting it - the Lightwell can be clicked: a billboard card is a world root with no parent hop, so the hit resolved to nothing and the whole shaft published a null mouseover; casting a hunter shot chains Auto Shot through `Spell.dbc`'s modalNextSpell column (the mechanism nobody had read — the next snapshot carries the seam that makes it fire in play); tradeskill list rows stop tooltipping (that hover was ours, not Blizzard's), and OnMouseUp goes to the frame that took the press Co-Authored-By: Claude Fable 5 --- crates/benilla-app/assets/ui/ChatFrame.xml | 571 ++++++++++++-- .../assets/ui/ColorPickerFrame.xml | 76 +- crates/benilla-app/assets/ui/CraftFrame.xml | 12 +- .../benilla-app/assets/ui/MinimapCluster.xml | 31 +- .../benilla-app/assets/ui/TradeSkillFrame.xml | 61 +- crates/benilla-app/assets/ui/UIParent.xml | 17 + crates/benilla-app/src/capture/fixtures.rs | 17 + crates/benilla-app/src/capture/scenarios.rs | 15 + crates/benilla-app/src/lib.rs | 7 + crates/benilla-app/src/local_state.rs | 41 +- crates/benilla-app/src/minimap/blips/mod.rs | 9 +- crates/benilla-app/src/minimap/mod.rs | 92 ++- crates/benilla-app/src/minimap/ping.rs | 479 ++++++++++++ crates/benilla-app/src/net.rs | 4 + crates/benilla-app/src/net/apply.rs | 21 +- crates/benilla-app/src/net/apply/spells.rs | 212 ++++++ crates/benilla-app/src/net/io.rs | 1 + crates/benilla-app/src/target/hover.rs | 151 +++- crates/benilla-app/src/ui_action/drain.rs | 28 + crates/benilla-app/src/ui_action/mod.rs | 19 + crates/benilla-app/src/ui_chat/input/mod.rs | 35 + crates/benilla-app/src/ui_chat/input/parse.rs | 6 +- crates/benilla-app/src/ui_chat/settings.rs | 55 +- crates/benilla-app/src/ui_chat/tests.rs | 49 +- crates/benilla-app/src/ui_layout.rs | 381 ++++++++++ .../src/ui_script/chat_options_tests.rs | 29 +- .../src/ui_script/chat_resize_tests.rs | 582 +++++++++++++++ .../benilla-app/src/ui_script/chat_tests.rs | 17 +- .../src/ui_script/color_picker_tests.rs | 158 ++++ .../src/ui_script/extract/colorselect.rs | 274 +++++++ .../benilla-app/src/ui_script/extract/mod.rs | 48 +- .../src/ui_script/minimap_tests.rs | 84 +++ crates/benilla-app/src/ui_script/mod.rs | 3 + crates/benilla-app/tests/tradeskill_frame.rs | 106 +++ crates/benilla-assets/src/world_assets/mod.rs | 32 + .../src/spells/catalog_tests.rs | 66 ++ crates/benilla-formats/src/spells/display.rs | 49 +- crates/benilla-formats/src/spells/mod.rs | 12 + crates/benilla-ui/src/loader/mod.rs | 1 + crates/benilla-ui/src/loader/widgets.rs | 80 ++ crates/benilla-ui/src/script/chat_window.rs | 82 +- crates/benilla-ui/src/script/colorselect.rs | 702 +++++++++++++++++- crates/benilla-ui/src/script/extract.rs | 51 +- crates/benilla-ui/src/script/layout_cache.rs | 203 +++++ crates/benilla-ui/src/script/minimap.rs | 44 ++ crates/benilla-ui/src/script/mod.rs | 23 +- crates/benilla-ui/src/script/model.rs | 35 +- crates/benilla-ui/src/script/object.rs | 2 +- .../benilla-ui/src/script/object/movable.rs | 66 +- crates/benilla-ui/src/script/pointer.rs | 97 ++- crates/benilla-ui/src/script/region.rs | 18 + crates/benilla-ui/src/script/types.rs | 28 + crates/benilla-ui/src/widget/kinds/mod.rs | 33 +- 53 files changed, 5035 insertions(+), 280 deletions(-) create mode 100644 crates/benilla-app/src/minimap/ping.rs create mode 100644 crates/benilla-app/src/ui_layout.rs create mode 100644 crates/benilla-app/src/ui_script/chat_resize_tests.rs create mode 100644 crates/benilla-app/src/ui_script/extract/colorselect.rs create mode 100644 crates/benilla-ui/src/script/layout_cache.rs diff --git a/crates/benilla-app/assets/ui/ChatFrame.xml b/crates/benilla-app/assets/ui/ChatFrame.xml index 9fb9a9fc..3135ccd0 100644 --- a/crates/benilla-app/assets/ui/ChatFrame.xml +++ b/crates/benilla-app/assets/ui/ChatFrame.xml @@ -254,6 +254,27 @@ REMOVE_CHAT_DELAY = "0" RegisterForSave("REMOVE_CHAT_DELAY") + -- CHAT_LOCKED — the master "no chat window may be moved or resized" switch, the reference's + -- own saved variable. The reference declares it in `UIOptionsFrame.lua` l.96-97 (`"0"` plus + -- `RegisterForSave`) because that is where its *Lock Chat Settings* checkbox lives; ours is + -- declared here for the reason the row above is — the file that owns what it changes owns + -- the variable, and a row whose defining file was not loaded gets a nil `applyFunc` and is + -- skipped rather than half-applied. + -- + -- **`"0"` — off.** Note the reference's own two-value disagreement: its checkbox table says + -- `default = "1"` (`UIOptionsFrame.lua:44`) while the variable is initialised `"0"` two + -- lines later, and the initialiser is what a fresh client actually runs. `"0"` here too. + -- The per-window lock (`FCF_SetLocked`, the tab menu's row) is the one that ships ON; this + -- one is the blanket override on top of it. + CHAT_LOCKED = "0" + RegisterForSave("CHAT_LOCKED") + + -- The tab menu's lock row labels (GlobalStrings l.2590 / l.4448). Live text comes off the + -- player's own `GlobalStrings.lua`; these are the headless-test fallback this file already + -- uses everywhere else. + LOCK_WINDOW = LOCK_WINDOW or "Lock Window" + UNLOCK_WINDOW = UNLOCK_WINDOW or "Unlock Window" + -- The row's applyFunc. The reference has no such function because it calls -- `SetChatMouseOverDelay(REMOVE_CHAT_DELAY)` inline from two places (its options load, -- l.216, and the checkbox's own commit); ours needs a zero-argument name to hand the row, @@ -293,17 +314,28 @@ flashLeft = 0, -- remaining flash seconds (ref: 60s, blink 0.25/0.25) } - -- The per-window texture sets the hover fade drives (ref CHAT_FRAME_TEXTURES: the - -- background + the 8 border pieces; benilla authors the borders as plain textures — - -- resizing is settings, the hover LOOK is not). Global: the load-time tint below needs it. + -- CHAT_FRAME_TEXTURES — the reference's own list (FloatingChatFrame.lua l.14-24), verbatim + -- in content and in spelling: the background plus the eight RESIZE GRIPS' textures. The + -- window's border art IS the grips, which is why one list drives both the hover fade + -- (`FCF_OnUpdate`) and the tint/alpha setters (`FCF_SetWindowColor`/`FCF_SetWindowAlpha`) — + -- the ring appears with the box and is invisible with it. + -- + -- benilla used to author these as plain `$parentBorder*` textures ("the hover LOOK without + -- the resize mechanics"); the mechanics landed with the move/resize arc, so the suffixes + -- are the reference's again and the pieces are its buttons' children. + CHAT_FRAME_TEXTURES = { + "Background", + "ResizeTopLeftTexture", "ResizeTopRightTexture", + "ResizeBottomLeftTexture", "ResizeBottomRightTexture", + "ResizeTopTexture", "ResizeBottomTexture", + "ResizeLeftTexture", "ResizeRightTexture", + } + + -- The live texture objects of one window's set. Global: the load-time tint below needs it. function BenillaFCF_Textures(id) local n = "ChatFrame" .. id local list = {} - for _, suffix in ipairs({ - "Background", - "BorderTopLeft", "BorderTopRight", "BorderBottomLeft", "BorderBottomRight", - "BorderTop", "BorderBottom", "BorderLeft", "BorderRight", - }) do + for _, suffix in ipairs(CHAT_FRAME_TEXTURES) do local t = getglobal(n .. suffix) if t then table.insert(list, t) end end @@ -375,6 +407,143 @@ return base + (DEFAULT_CHATFRAME_ALPHA - base) * reveal end + -- ── The lock, the move and the resize (ref FloatingChatFrame.lua) ──────────────────── + -- + -- Three gates decide whether a window may be dragged or resized, and the reference applies + -- all three at both verbs (`FCF_Resize` l.583-601, `FCF_Tab_OnClick`'s move arm l.1247-1256): + -- 1 · `FCF_Get_ChatLocked()` — the blanket `CHAT_LOCKED` saved variable; + -- 2 · `chatFrame.isLocked` — this window's own lock, the tab menu's row; + -- 3 · docked-and-not-the-default — a docked window's rect belongs to the DOCK, so only + -- `DEFAULT_CHAT_FRAME` may move it. ChatFrame2's anchors ARE ChatFrame1's corners: + -- resizing it directly would stretch it off the window it follows. + + -- The eight grips' name suffixes, the reference's own (see `CHAT_FRAME_TEXTURES`). + local RESIZE_GRIPS = { + "TopLeft", "TopRight", "BottomLeft", "BottomRight", + "Top", "Bottom", "Left", "Right", + } + + -- May this window be moved/resized at all? The three gates above, in one place because + -- both verbs and the lock's own plumbing ask the same question. + function FCF_CanMove(chatFrame) + if FCF_Get_ChatLocked() then return nil end + if chatFrame.isLocked then return nil end + if chatFrame.isDocked and chatFrame ~= DEFAULT_CHAT_FRAME then return nil end + return 1 + end + + -- Hand the window's EDGES and its BODY to the mouse, or take them back. benilla's own — + -- the reference needs nothing like it because its chat window is `enableMouse="false"` and + -- so competes for no click. Ours does (`OnClick`, `OnHyperlinkClick`), so a locked window + -- has to be exactly as click-through as it was before this arc: eight live grip buttons + -- would shadow the outer 16px of the window, and the bottom strip is where the newest chat + -- line — the one with the item link in it — is drawn. + function BenillaFCF_ApplyLock(chatFrame) + local free = FCF_CanMove(chatFrame) + local name = chatFrame:GetName() + for _, grip in ipairs(RESIZE_GRIPS) do + local b = getglobal(name .. "Resize" .. grip) + if b then b:EnableMouse(free and true or false) end + end + local tab = getglobal(name .. "Tab") + if free then + chatFrame:RegisterForDrag("LeftButton") + if tab then tab:RegisterForDrag("LeftButton") end + else + chatFrame:RegisterForDrag() + if tab then tab:RegisterForDrag() end + end + end + + -- ref l.802-805, verbatim: the frame field and the engine store, together. + -- `BenillaFCF_ApplyLock` is ours, and it is what makes the flag do something structurally + -- rather than only in the two early returns below. + function FCF_SetLocked(chatFrame, isLocked) + chatFrame.isLocked = isLocked + SetChatWindowLocked(chatFrame:GetID(), isLocked) + BenillaFCF_ApplyLock(chatFrame) + end + + -- ref l.785-800. **Its undock branch is deliberately absent**: the reference, on unlocking + -- a docked frame that is not the default one, calls `FCF_UnDockFrame`, re-centres it on the + -- screen and re-seats its tab. benilla has no undock (0288 §2), so that arm has nothing to + -- run — named here rather than stubbed (1134 §4), and reachable only from the Combat Log's + -- tab, whose own gate above already refuses to move it. + function FCF_ToggleLock() + local chatFrame = FCF_GetCurrentChatFrame() + if chatFrame.isLocked then + FCF_SetLocked(chatFrame, nil) + else + FCF_SetLocked(chatFrame, 1) + end + end + + -- ref l.1428-1441, verbatim. `CHAT_LOCKED` is the blanket switch over every window; the + -- re-apply is ours, for the reason `FCF_SetLocked`'s is. + function FCF_Set_ChatLocked(isLocked) + -- The saved variable is a STRING, not a boolean — the uvar contract every options row + -- in this client shares, and what `RegisterForSave` writes out. + CHAT_LOCKED = isLocked and "1" or "0" + for i = 1, 2 do + local frame = getglobal("ChatFrame" .. i) + if frame then BenillaFCF_ApplyLock(frame) end + end + end + + function FCF_Get_ChatLocked() + -- `1`/nil rather than a boolean, because every caller here is an `if` and the + -- reference's own callers pass the result around as one. + if CHAT_LOCKED == "1" then return 1 end + return nil + end + + -- ref l.583-601. `this` is the grip button; the window is its parent. The commented-out + -- `DEFAULT_CHAT_FRAME.resizing = 1` block in the reference is left out with it. + function FCF_Resize(anchorPoint) + local chatFrame = this:GetParent() + if not FCF_CanMove(chatFrame) then return end + chatFrame.resizing = 1 + chatFrame:StartSizing(anchorPoint) + end + + -- ref l.603-609. The reference also calls `FCF_DockUpdate()` when the parent is the default + -- frame; ours has no such function and needs none — the tab is anchored to the window's own + -- background, so it follows a resize by anchor rather than by a re-layout pass. + function FCF_StopResize() + local chatFrame = this:GetParent() + chatFrame:StopMovingOrSizing() + chatFrame.resizing = nil + end + + -- The move half. The reference reaches `StartMoving`/`StopMovingOrSizing` from + -- `FCF_Tab_OnClick`'s press/release arms; ours reaches them from the drag trio, for the + -- reasons written at the template's `OnDragStart`. Same two engine verbs, same three gates. + -- + -- `MOVING_CHATFRAME` (the reference's global) is not set: its only readers there are + -- `FCF_IsValidChatFrame` and the dock-region hit test, neither of which exists here, and a + -- global nothing reads is state that starts lying the first time a path forgets to clear it. + function FCF_StartMove(chatFrame) + if not FCF_CanMove(chatFrame) then return end + chatFrame.moving = 1 + chatFrame:StartMoving() + end + + function FCF_StopMove(chatFrame) + chatFrame:StopMovingOrSizing() + chatFrame.moving = nil + end + + -- Is either dock window mid-drag? The hover fade holds the chrome visible while it is — + -- ref FCF_OnUpdate's own `(MouseIsOver(...) or chatFrame.resizing)`. Without it the box + -- fades out under a moving cursor and takes the grip out of the hand holding it. + function BenillaFCF_Dragging() + for i = 1, 2 do + local f = getglobal("ChatFrame" .. i) + if f and (f.resizing or f.moving) then return 1 end + end + return nil + end + -- FloatingChatFrame_Update's look half (ref l.49-57): pull every window's stored tint and -- alpha out of the engine and seat them, WITHOUT saving them back (they came from there). -- Runs at load and on `UPDATE_CHAT_WINDOWS`, which is the event the host fires after it @@ -383,9 +552,15 @@ for i = 1, 2 do local frame = getglobal("ChatFrame" .. i) if frame then - local _, fontSize, r, g, b, a = GetChatWindowInfo(i) + local _, fontSize, r, g, b, a, _, locked = GetChatWindowInfo(i) FCF_SetWindowColor(frame, r, g, b, 1) FCF_SetWindowAlpha(frame, a, 1) + -- ref l.56, in the same block and for the same reason as the two above: the + -- window's stored LOCKED is seated onto the frame. It ships `1` (the stock + -- chat-cache row), so both dock windows start locked exactly as a fresh 1.12 + -- client's do — and the write back through `SetChatWindowLocked` is idempotent, + -- so seating what was just read dirties nothing. + FCF_SetLocked(frame, locked) -- SIZE 0 is the stock "use the font's own height" and applies nothing. if fontSize and fontSize > 0 then FCF_SetChatWindowFontSize(frame, fontSize, 1) @@ -520,6 +695,18 @@ return end + -- Window options — the reference's FIRST row (ref l.236-246), and the only one of its + -- four window verbs benilla can honour. Rename, Create New and Remove each need the + -- window management 0288 §2 holds out (there is no rename, no undock, no create/close), + -- so they are named here rather than stubbed: a row that looks like a setting and + -- changes nothing cannot be told from a working one by looking (1134 §4). + local current = FCF_GetCurrentChatFrame() + info = {} + info.text = (current and current.isLocked) and UNLOCK_WINDOW or LOCK_WINDOW + info.func = FCF_ToggleLock + info.notCheckable = 1 + UIDropDownMenu_AddButton(info) + -- Display header (ref l.273-279). info = {} info.text = DISPLAY @@ -690,6 +877,10 @@ s.revealTarget = 0 end if s.flashTab ~= 0 then s.revealTarget = 1 end + -- ref FCF_OnUpdate's `(MouseIsOver(...) or chatFrame.resizing)`: a drag in flight pins + -- the chrome open. The cursor is moving during a resize, which would otherwise keep + -- resetting the stationary-hover delay and fade the grips out from under it. + if BenillaFCF_Dragging() then s.revealTarget = 1 end -- The linear fade toward the target (ref UIFrameFadeIn/Out over CHAT_FRAME_FADE_TIME). -- **Zero is a real setting, not a guard against nothing**: the Chat page's *Remove @@ -1012,17 +1203,29 @@ The chrome the template carries: displayDuration 120 / maxLines 128 (ref ChatFrameTemplate); a background stretched -2/+2 x and +3/-6 y past the frame (ref $parentBackground anchors) - and the 8 border slices ringing it (the ref draws those through its resize buttons' - textures — benilla authors them as plain textures, the hover LOOK without the resize - mechanics); the scroll column; and the per-frame scripts. Everything starts alpha 0; - BenillaFCF fades it. + and the 8 resize grips ringing it, which are the border art AND the handles (ref + CHAT_FRAME_TEXTURES — see the block below); the scroll column; and the per-frame scripts. + Everything starts alpha 0; BenillaFCF fades it. + + `movable`/`resizable`/`` are the reference's own attributes at the reference's + own numbers (FloatingChatFrame.xml l.211, l.222-230): min 296×75, max 608×400. The + `` that sits beside them THERE is deliberately absent + here — see the drag scripts at the foot of this template for why, and for what replaces it. **`EXECUTE_CHAT_LINE` is gated to window 1 on purpose** — see the OnLoad below. It is the one script here that is genuinely ChatFrame1's rather than the dock's, so the template carries the gate rather than the registration. --> + frameStrata="BACKGROUND" displayDuration="120.0" maxLines="128" + movable="true" resizable="true"> + + + + + @@ -1031,63 +1234,212 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + +