Files
benilla-octo/crates/benilla-app/assets/ui/QuestLogFrame.xml
T
SamandClaude Fable 5 f3ecb5c968 a cinematic holds your body, and all seven chat windows exist
- a cinematic holds the body while the camera flies: the streaming focus follows the eye — a Tauren's intro opens 1741 yards from the body — so the body's own ground unloaded under it and it fell (z=59.4 to −62 in four seconds, the server yanking it back); it now takes the same hold a teleport takes, released when the focus comes home to a resident tile, and the loading screen stops covering the fly-by, because "the player's ground is missing" is not a load while the eye is deliberately elsewhere; the letterbox measurement prints as an ordinary info line, and scripts/cine.sh plays any one cinematic and prints what the client did to the world while the camera was away
- a cinematic cuts the zone music: the reference asserts exactly what /console EnableMusic 0 asserts, through the same setter — a cut, not a fade, the music pump dead for the duration, resume at +3.000 s — and benilla asserts the runtime flag, never the CVar, or quitting during a 102-second intro would save your music off for good; ambience is deliberately untouched (it has the analogous flag and the cinematic never writes it); the narration has no category at all — its flags route it around every volume slider but not around the Enable Sound Effects gate, so unchecking that silences it — and the listener follows the cinematic camera for the duration
- addons can walk a UI they did not build: GetChildren/GetNumChildren/GetRegions/GetNumRegions land — the basis of the whole reskin genre, and the wall four of the twenty most-installed 1.12 addons died on: pfUI drew nothing before and now paints 495 quads, ShaguDPS 71 → 566, pfQuest 3 → 498 — with the reference's own quirks: the title region is NOT in the list (its creation path never reaches the region linker), hidden regions are returned and counted, oldest first; the Model widget gets its Lua surface (SetModel, SetSequence and siblings, read out of the shipped image, where SetRotation and SetFacing are two names for one slot); and the Minimap carries the nine engine-created Model children no XML declares, built before the frame descent, the ninth being the player arrow Questie indexes as ({Minimap:GetChildren()})[9]
- all seven chat windows come from one template, tabs and scroll buttons included — whether a window is shown has nothing to do with whether its globals resolve, and pfUI reads every piece of that furniture for all seven, unguarded; isDocked only on windows 1 and 2 (the chat-cache's own DOCKED byte), SELECTED_CHAT_FRAME seated beside DEFAULT_CHAT_FRAME (the window you last picked vs the one FrameXML prints to), and the four per-window fields ChatFrame_OnLoad seats; two more one-name walls fell — a Button's label answers to its second spelling NormalText, which KLHThreatMeter's column headers die without, and ABANDON_QUEST_WITH_ITEMS is declared because a dialog table is read at load whatever the client later shows, leaving Questie with zero errors of ours — and the addon harness gains --probe/--eval, one addon's VM answering a question in ~5 s instead of a 90 s corpus run

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

1947 lines
144 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Ui>
<!--
benilla's quest log window (decision 0088 arc, the quest-log slice — the sibling of
`QuestFrame.xml`'s questgiver window, dressed from the real 1.12 `QuestLogFrame.xml`/
`QuestLogFrame.lua` (scratchpad `ref-framexml/QUESTLOG-PIN.md`; section cites below are into
that pin, `file:line` cites are into the pin's own extracted `QuestLogFrame.xml`/`.lua`). One
`QuestLogFrame` carries BOTH the list page and the detail page — FLAT layout (decision
0088 §2, quoted in `QuestFrame.xml`'s header): a child frame shown at runtime does not render
its own `<Layers>` FontStrings in this engine, so every FontString/Button here lives directly
on the one always-visible window, never on a nested sub-frame. `enableMouse`/`toplevel`/
`movable` are LoadXML no-ops in this engine (`loader.rs` warn-once gaps) — omitted here exactly
as `QuestFrame.xml` omits them; they cost nothing since Buttons are mouse-enabled by
construction regardless (`widget/mod.rs`'s ctor comment).
Docks the left UIPanel slot exactly like Gossip/Merchant/Quest (`UiPanels.xml`
`UIPanelWindows["QuestLogFrame"] = { area = "left", pushable = 0, whileDead = 1 }`
(whileDead carried since 1507), pin §8's
`UIParent.lua l.25` quote) — so it competes for/replaces those windows, never stacks with
them. UNLIKE those windows this is not an NPC session: no `CloseX()` on hide, no range guard;
it is durable player state the app can push at any time (`benilla-ui`'s `quest_log.rs` module
doc). `ToggleQuestLog()` (pin §8, `QuestLogFrame.lua:24-30`) is the show/hide entry point the
app's 'L' binding already calls (`ui_script/mod.rs`).
Faux-scroll (pin §2): 6 title rows (`QUESTS_DISPLAYED`), a Lua-side `questLogOffset` upvalue
standing in for the real `QuestLogListScrollFrame`/`FauxScrollFrame_Update` (no real
ScrollFrame/FauxScrollFrame engine exists yet) — `<OnMouseWheel>` adjusts it directly. The
wheel handler lives on the window Frame itself: the engine bubbles a spin UP the parent chain
(`script/pointer.rs` `mouse_wheel`), so a spin over a title row (no handler of its own) rises to
this window and scrolls. (A bare `<Frame>` is not a mouse HIT target — only
Button/CheckButton/EditBox/ScrollingMessageFrame are, `widget/mod.rs`'s ctor comment, and
`enableMouse` is a no-op stub — but it is still a valid bubble ANCESTOR: bubbling checks for the
handler, not mouse-enablement.) The `QuestLogWheelCatcher` BUTTON, declared FIRST so every
real row out-ranks it in hit-testing (`order::hit_test` walks the draw-order-descending list),
now only gives the list's EMPTY tail a hit-target whose spin also bubbles here.
v1 scope/deviations from the pin (each cited again at its own widget below):
- The LIST page still has no real ScrollFrame — `questLogOffset`'s faux-scroll stands in
(this comment's own "Faux-scroll" paragraph above); real real-estate accounting there
would need a genuine header/collapse model this v1 doesn't ship. The DETAIL page now
DOES sit on a real ScrollFrame (`QuestLogDetailScroll`, decision 0112's mechanism)
— a quest with many objectives/a long description/many rewards scrolls and clips at the
pane's own edge instead of running past the window's bottom (pin §3's doNotScroll
asymmetry is implemented — see `BenillaQuestLogDetail_Update`'s own comment).
- Difficulty coloring wears the reference's names and bands (`QuestDifficultyColor` +
`GetDifficultyColor`, specified at l.14-20 + l.584-599) over the real
`GetQuestGreenRange()` binding — see the function's own comment. (The old
`BenillaQuestDifficultyColor` INTERIM and its approximated thresholds are retired.)
- No headers ship (`benilla-ui`'s `quest_log.rs`: flat entries only, headers need the
QuestSort/AreaTable DBC join) — the header branch below is dead code today, kept for when
that join lands rather than re-derived then.
- No party share/push, no timed-quest countdown, no reward-spell row — still-named v1 stubs
in `benilla-ui`'s `quest_log.rs`. Quest-watch tracking (this comment's own arc) IS wired.
- `GetQuestLogRequiredMoney()` exists on the Lua surface (pin §3/§6) but is not wired to a
detail-pane line — rare in vanilla (ferry-style quests), a documented v1 gap.
- `EmptyQuestLogFrame` (pin §2, a whole separate book-art frame) is simplified to one
centered FontString + hidden rows on this same window.
- The abandon confirm is the ref's `ABANDON_QUEST` entry on the shared StaticPopup
engine (UiPanels.xml, decision 0308 §3).
- Quest-watch toggle is SHIFT+left-click, the ref's own fork (pin §5,
`QuestLogTitleButton_OnClick`, `QuestLogFrame.lua:469-505`). The modifier plumbing this
note once named as the missing engine slice (`IsShiftKeyDown` + the app's per-frame
mirror) landed with the cursor arc; the interim right-click substitute is retired.
- The two ref watch errors (`QUEST_WATCH_NO_OBJECTIVES`/`QUEST_WATCH_TOO_MANY`) go out
through the reference's own `UIErrorsFrame:AddMessage`. That was once impossible here —
`UIErrorsFrame` was a hand-rolled `<Frame>` reachable only by firing `UI_ERROR_MESSAGE`
from Rust — and the guards no-opped silently in the interim; the MessageFrame widget
(`script::messageframe::plain`) closed it, and the seam this note asked for turned out
to be the real widget method rather than a benilla global.
-->
<Script><![CDATA[
QUESTS_DISPLAYED = 6 -- QUESTS_DISPLAYED (pin §2, QuestLogFrame.lua:1)
MAX_QUESTLOG_QUESTS = 20 -- MAX_QUESTLOG_QUESTS (pin §2, :6) — display cap only
MAX_OBJECTIVES = 10 -- MAX_OBJECTIVES (pin §3, QuestLogFrame.lua:3)
-- Reward/choice row cap: the SAME v1 cap `QuestFrame.xml`'s BENILLA_QUEST_MAX_ITEMS uses (the
-- ref's own MAX_NUM_ITEMS is 10, QuestFrame.lua:2 — we halve it there too).
BENILLA_QUESTLOG_MAX_ITEMS = 6
-- MAX_WATCHABLE_QUESTS (pin §2/§5, QuestLogFrame.lua:8) — the SAME cap `quest_log.rs`'s
-- AddQuestWatch already enforces engine-side; this is just this file's own guard so the
-- right-click handler can skip the AddQuestWatch call (and its error) locally.
BENILLA_QUESTLOG_MAX_WATCHES = 5
-- The tracker HUD's flat line pool (ref MAX_QUESTWATCH_LINES, QuestLogFrame.lua:7):
-- one rolling pool across every watch, dynamically re-chained each paint — the ref's own
-- model (the old fixed 5×(1+4) per-slot grid predated region Show/Hide/SetPoint).
BENILLA_QUESTWATCH_LINES = 30
-- The timed-quest countdown row (decision 1150). UPDATE_DELAY is the ref's own
-- (QuestLogFrame.lua:5) — the throttle on the "Time Remaining:" repaint, not a benilla
-- dial. TIME_REMAINING is the shipped enUS GlobalString (GlobalStrings.lua:4248), inlined
-- like every other string in these files (we load no GlobalStrings.lua).
BENILLA_QUESTLOG_UPDATE_DELAY = 0.1
BENILLA_TIME_REMAINING = "Time Remaining:"
-- The detail pane's ScrollFrame child-resize sum (`BenillaQuestLogDetail_ResizeChild`'s own
-- comment) — fixed offsets/row heights transcribed from this chain's own FontString/SetPoint
-- comments below, gathered here with the file's other constants.
BENILLA_QUESTLOG_DETAIL_TOP = 5 -- child TOPLEFT -> QuestTitle (ref :624-635, offset 5,-5)
BENILLA_QUESTLOG_DETAIL_OBJ_GAP = 10 -- ObjectivesText->Objective1 AND last objective/
-- ObjectivesText->DescriptionTitle (both -10, XML/Lua)
BENILLA_QUESTLOG_DETAIL_OBJ_ROW = 12 -- one objective line (Objective1's own comment)
BENILLA_QUESTLOG_DETAIL_OBJ_STEP = 2 -- gap between objective lines
BENILLA_QUESTLOG_DETAIL_BOTTOM_PAD = 10 -- v1: a little breathing room past the last row
-- The detail pane's minimal scrollbar (`BenillaQuestLogDetailScroll_SyncBar`'s own comment).
BENILLA_QUESTLOG_SCROLL_STEP = 20 -- px per wheel notch / arrow click
BENILLA_QUESTLOG_SCROLL_TRACK_H = 229 -- ScrollFrame height 261 - 16 (up) - 16 (down)
BENILLA_QUESTLOG_SCROLL_THUMB_H = 16
-- The faux-scroll offset (pin §2's FauxScrollFrame stand-in) — a plain chunk-local upvalue
-- every function below closes over, the same pattern `UiPanels.xml`'s `slots` table uses.
local questLogOffset = 0
-- The detail pane's last-computed resize inputs (`BenillaQuestLogDetail_ResizeChild`'s own
-- comment on WHY a re-check is needed at all: `GetHeight()` on the auto-height FontStrings
-- is one tick stale on the FIRST populate of brand-new text — the host measures it only on
-- the NEXT `resolve()`/measure round-trip, which lands after this Lua tick already returned.
-- Detail_Update has no reason to run again once the quest log's data stops changing, so
-- nothing would ever ask for the now-correct height — these upvalues let the ScrollFrame's
-- own `<OnUpdate>` re-ask every frame (cheaply — see that handler's own comment) using the
-- SAME inputs the last full populate computed, until the measured heights catch up and the
-- resize stops changing.
local detailObjAnchorIndex = 0
local detailRewardHeight = 18
-- The last height actually applied via SetHeight — lets the OnUpdate re-check below skip
-- SetHeight/UpdateScrollChildRect (and the OnScrollRangeChanged repaint it fires) entirely
-- once the resize has converged and stopped changing, rather than redoing that work every
-- single frame forever while the window sits open.
local detailLastAppliedHeight = nil
-- ── Difficulty coloring ──────────────────────────────────────────────────────────────────
-- The six bands and their exact colors are the reference's contract, not a palette of ours:
-- ref-QuestLogFrame.lua's table (l.14-20) and GetDifficultyColor (l.584-599, "Used for
-- quests and enemy coloration" — the ref's own comment; the TARGET FRAME's level number
-- colors through this same function, TargetFrame_CheckLevel l.130), and `QuestDifficultyColor`
-- is a name addons read, so both the global and its six keys are fixed. The green→grey
-- boundary is the real GetQuestGreenRange() binding (benilla-ui unit.rs — the byte-verified
-- grey band the plate/tooltip share; the binding's own byte read is under the 2026-07-17
-- wow-re §5). This retires the BenillaQuestDifficultyColor INTERIM and its approximated
-- -2/-10 thresholds.
QuestDifficultyColor = {
impossible = { r = 1.00, g = 0.10, b = 0.10 },
verydifficult = { r = 1.00, g = 0.50, b = 0.25 },
difficult = { r = 1.00, g = 1.00, b = 0.00 },
standard = { r = 0.25, g = 0.75, b = 0.25 },
trivial = { r = 0.50, g = 0.50, b = 0.50 },
header = { r = 0.70, g = 0.70, b = 0.70 },
}
-- How far ABOVE the player the quest sits picks the band. Note the shape of the last two
-- tests: the three hard bands are fixed offsets, but the green/grey boundary is not a
-- constant at all — GetQuestGreenRange() widens as the player levels — which is why it is
-- asked rather than compared against a number, and why "trivial" is what is left over
-- rather than a band with an edge of its own.
function GetDifficultyColor(level)
local levelDiff = level - UnitLevel("player")
if levelDiff >= 5 then
return QuestDifficultyColor.impossible
elseif levelDiff >= 3 then
return QuestDifficultyColor.verydifficult
elseif levelDiff >= -2 then
return QuestDifficultyColor.difficult
elseif -levelDiff <= GetQuestGreenRange() then
return QuestDifficultyColor.standard
end
return QuestDifficultyColor.trivial
end
-- ── The list page ────────────────────────────────────────────────────────────────────────
-- `mouseButton` is arg1 off RegisterForClicks("LeftButtonUp", "RightButtonUp")
-- (BenillaQuestLogTitleTemplate's OnLoad). The watch toggle is SHIFT+left-click, the ref's
-- own fork (QuestLogFrame.lua:469-505) — the modifier plumbing this file's header once
-- named as the missing engine slice landed with the cursor arc (0216/0218 follow-up), so
-- the documented right-click substitute is retired.
function BenillaQuestLogTitle_OnClick(button, mouseButton)
local index = button.questIndex
if not index then return end
local _, _, _, isHeader, isCollapsed = GetQuestLogTitle(index)
if isHeader then
-- pin §5, QuestLog_SetSelection (QuestLogFrame.lua:321-329): a header never becomes
-- the selection, it only (un)collapses. No header ever ships in v1 (module doc,
-- `benilla-ui/src/script/quest_log.rs`) so Expand/CollapseQuestHeader don't exist yet
-- — guarded rather than assumed absent forever.
if isCollapsed and type(ExpandQuestHeader) == "function" then
ExpandQuestHeader(index)
elseif type(CollapseQuestHeader) == "function" then
CollapseQuestHeader(index)
end
return
end
-- pin §5, QuestLogTitleButton_OnClick (QuestLogFrame.lua:469-501): shift-click either
-- posts the quest's NAME into an open chat edit box (l.478-480) or, with chat closed,
-- toggles the watch (l.481-500 — the ref's own `else`). Chat wins; the watch is the
-- fallback, not the other way round (decision 1059).
--
-- The insert (l.480) is `gsub(this:GetText(), " *(.*)", "%1")` — the row LABEL with its
-- leading indent trimmed, i.e. the bare quest name; note it is plain text, NOT a
-- hyperlink (1.12 has no |Hquest: link). We read `GetQuestLogTitle(index)` instead of
-- the label, because our label is not the ref's: this window appends the Complete/Failed
-- tag onto the ONE title string (the v1 simplification in BenillaQuestLogList_Update,
-- where the ref keeps a separate right-justified $parentTag FontString), so the ref's
-- gsub over our label would post "Quest 1 (Complete)". The API title is exactly the
-- string the ref's own trim yields — same result, different expression. `ChatFrameEditBox
-- and` guards a window test that hasn't loaded ChatFrame.xml (BenillaPaperDollSlot_OnClick's
-- precedent).
--
-- Guard order inside the watch arm matches the ref exactly: already watched → unwatch,
-- unconditionally; else zero objectives (QUEST_WATCH_NO_OBJECTIVES) or the watch list
-- full (QUEST_WATCH_TOO_MANY) put the ref's red line on the errors frame
-- (UIErrorsFrame:AddMessage, the ref's own call — lua:490/495), else watch it. The ref calls QuestWatch_Update() itself
-- right after each Add/RemoveQuestWatch (same two call sites) — it does NOT lean on
-- the QUEST_LOG_UPDATE/QUEST_WATCH_UPDATE events alone for this, since neither engine
-- fires an event off a plain Add/RemoveQuestWatch call; we mirror that exactly.
if mouseButton == "LeftButton" and IsShiftKeyDown() then
local title = GetQuestLogTitle(index)
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
-- The visibility test stays HERE, not in the helper: it is the ref's own fork
-- (l.474/478) — with chat open this click never watches, and the helper's
-- return would conflate "chat closed" with "nothing to post".
BenillaChatEdit_InsertLink(title)
elseif IsQuestWatched(index) then
-- Clearing the timer too: a lingering timed entry would later expire and
-- silently unwatch a re-added MANUAL watch.
if title then BENILLA_QUEST_WATCH_TIMERS[title] = nil end
RemoveQuestWatch(index)
QuestWatch_Update()
elseif GetNumQuestLeaderBoards(index) == 0 then
-- ref QUEST_WATCH_NO_OBJECTIVES, red, straight onto the errors frame.
UIErrorsFrame:AddMessage("This quest has no objectives to track", 1.0, 0.1, 0.1)
elseif GetNumQuestWatches() >= BENILLA_QUESTLOG_MAX_WATCHES then
-- ref QUEST_WATCH_TOO_MANY, red.
UIErrorsFrame:AddMessage("You may only watch " .. BENILLA_QUESTLOG_MAX_WATCHES .. " quests at a time", 1.0, 0.1, 0.1)
else
-- A manual watch is PERMANENT (ref: QUEST_WATCH_NO_EXPIRE) — drop any timed
-- entry the auto-watch may have armed for it.
if title then BENILLA_QUEST_WATCH_TIMERS[title] = nil end
AddQuestWatch(index)
QuestWatch_Update()
end
end
-- pin §5: every click (shifted/right-clicked or not) ends in select + a full repaint.
SelectQuestLogEntry(index)
BenillaQuestLogFrame_Update()
end
-- The fold-all tab (ref QuestLogCollapseAllButton_OnClick, QuestLogFrame.lua:554-563):
-- index 0 = every header; the tab's own +/- icon tracks the all-state.
function QuestLogCollapseAllButton_OnClick()
local b = getglobal("QuestLogCollapseAllButton")
if b.allCollapsed then
b.allCollapsed = nil
b:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
ExpandQuestHeader(0)
else
b.allCollapsed = 1
b:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
CollapseQuestHeader(0)
end
end
-- pin §2's QuestLog_SetFirstValidSelection/QuestLog_GetFirstSelectableQuest
-- (QuestLogFrame.lua:293-296, :565-582): if nothing is selected, pick the first non-header row.
function QuestLog_SetFirstValidSelection()
if GetQuestLogSelection() ~= 0 then return end
local numEntries = GetNumQuestLogEntries()
for i = 1, numEntries do
local _, _, _, isHeader = GetQuestLogTitle(i)
if not isHeader then
SelectQuestLogEntry(i)
return
end
end
end
function BenillaQuestLogList_Update()
local numEntries, numQuests = GetNumQuestLogEntries()
-- Ref QuestLogFrame.lua:123-124 with QUEST_LOG_COUNT_TEMPLATE inlined verbatim
-- (GlobalStrings.lua:3236): gold "Quests:" label, white count via the |cff markup; the
-- pill's middle piece then shrinks to the text (ref sizes it every update). The >10
-- guard skips the first-ever paint, where the host hasn't measured the string yet
-- (GetWidth 0 would collapse the pill; the next update — and every one after — sizes it).
getglobal("QuestLogCount"):SetText(string.format("Quests: |cffffffff%d/%d|r", numQuests, MAX_QUESTLOG_QUESTS))
local countWidth = getglobal("QuestLogCount"):GetWidth()
if countWidth and countWidth > 10 then
getglobal("QuestLogCountMiddle"):SetWidth(countWidth)
end
-- v1 empty state (pin §2 drives a whole separate EmptyQuestLogFrame; simplified here to
-- one message string + hidden rows, see this file's header comment). NOTE: throughout
-- this file "hiding" text is a blank SetText(""), never a Show/Hide call — exactly how
-- `QuestFrame.xml`'s BenillaQuest_HideAll blanks its texts instead of hiding them. That
-- convention predates region Show/Hide (`region.rs` has the real pair now, and the
-- loader honours hidden= on regions); an empty string draws nothing, so it is equivalent
-- either way and the idiom stays for consistency with the sibling windows.
local abandonButton = getglobal("QuestLogAbandonButton")
if numEntries == 0 then
getglobal("QuestLogEmptyText"):SetText("Your quest log is empty.")
abandonButton:Disable()
else
getglobal("QuestLogEmptyText"):SetText("")
abandonButton:Enable()
end
-- Clamp the faux-scroll offset (pin §2's FauxScrollFrame_Update clamp).
local maxOffset = numEntries - QUESTS_DISPLAYED
if maxOffset < 0 then maxOffset = 0 end
if questLogOffset > maxOffset then questLogOffset = maxOffset end
if questLogOffset < 0 then questLogOffset = 0 end
local selection = GetQuestLogSelection()
for i = 1, QUESTS_DISPLAYED do
local button = getglobal("QuestLogTitle" .. i)
local index = i + questLogOffset
if index <= numEntries then
local title, level, tag, isHeader, isCollapsed, isComplete = GetQuestLogTitle(index)
local text = title or ""
if isHeader then
-- Ref QuestLogFrame.lua:146-159: a header shows the plus/minus fold icon
-- (its NormalTexture — the row template declares the 16x16 LEFT+3 slot),
-- the plus-glow hover art, no indent, no tag, no watch check.
if isCollapsed then
button:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-Up")
else
button:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-Up")
end
button.foldHighlight:SetTexture("Interface\\Buttons\\UI-PlusButton-Hilight")
else
-- Ref l.161-166: quest rows clear both state slots and indent two spaces.
text = " " .. text
button:SetNormalTexture("")
button.foldHighlight:SetTexture("")
if isComplete == 1 then
text = text .. " (Complete)"
elseif isComplete == -1 then
text = text .. " (Failed)"
end
end
-- pin §2: the ref recolors via a SEPARATE right-side tag FontString; one combined
-- string (title + tag suffix, above) is our v1 simplification — see this widget's
-- own template comment.
button.text:SetText(text)
button.questIndex = index
if isHeader then
local c = QuestDifficultyColor["header"]
button.text:SetTextColor(c.r, c.g, c.b)
else
local c = GetDifficultyColor(level)
button.text:SetTextColor(c.r, c.g, c.b)
end
-- pin §2's $parentCheck: the per-row watch checkbox. SetTexture(path)/
-- SetTexture(nil) as the show/hide toggle — the icon-slot blanking pattern
-- (`BenillaBagSlot_Update` etc.). (Regions DO have Show/Hide these days —
-- region.rs; this site just predates it and the pattern is equivalent.)
if not isHeader and IsQuestWatched(index) then
button.check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
else
button.check:SetTexture(nil)
end
button:Show()
else
button.questIndex = nil
button:Hide()
end
end
-- The selection highlight follows the row only while it is inside the visible scroll
-- window (pin §5, QuestLog_SetSelection QuestLogFrame.lua:330-340) — a selection scrolled
-- off-screen still holds state, but the highlight itself doesn't show until the list
-- scrolls back to it. Ref l.337-339 verbatim: the glow rides at (5,0) on the row and is
-- vertex-colored to the row's own difficulty color (that green bar in the ref shot IS
-- this vertex color on the white ADD gradient).
local hl = getglobal("QuestLogSelectHighlight")
if selection > questLogOffset and selection <= questLogOffset + QUESTS_DISPLAYED
and selection <= numEntries then
local selButton = getglobal("QuestLogTitle" .. (selection - questLogOffset))
local _, selLevel, _, selIsHeader = GetQuestLogTitle(selection)
if selIsHeader then
local c = QuestDifficultyColor["header"]
getglobal("QuestLogSkillHighlight"):SetVertexColor(c.r, c.g, c.b)
else
local c = GetDifficultyColor(selLevel)
getglobal("QuestLogSkillHighlight"):SetVertexColor(c.r, c.g, c.b)
end
hl:SetPoint("TOPLEFT", selButton, "TOPLEFT", 5, 0)
hl:Show()
else
hl:Hide()
end
end
function BenillaQuestLogFrame_OnMouseWheel(delta)
-- WoW wheel convention: +1 per notch up, -1 down (ChatFrame.xml's own handler quotes the
-- same convention). Up = earlier entries (offset toward 0); down = later entries.
if delta > 0 then
questLogOffset = questLogOffset - 1
else
questLogOffset = questLogOffset + 1
end
BenillaQuestLogList_Update()
end
-- ── The detail page ──────────────────────────────────────────────────────────────────────
-- The detail pane's real ScrollFrame (decision 0112's mechanism, landed this session — this
-- window used to be a fixed layout that simply clipped past the window's bottom edge, this
-- file's header comment). `QuestLogDetailScroll`'s ScrollChild is
-- `QuestLogDetailChild`: every FontString/Button below that used to live flat on
-- `QuestLogFrame` now lives on that child instead (the 0088 §2 flat-frame constraint
-- this file's header comment quoted DIED with the 0112 resolver fixpoint — child frames
-- render their own `<Layers>` regions again), so its subtree clips at the ScrollFrame's own
-- rect and scrolls independent of the rest of the window.
--
-- The child has no *reported* height from the engine (a Frame's height is its own SetSize —
-- the resolver never grows a frame to fit content that overflows it, `script/mod.rs`'s
-- region-resolution comment), so `BenillaQuestLogDetail_ResizeChild` below sums one by hand
-- from the same fixed offsets/row heights this chain's own comments already cite, plus each
-- auto-height FontString's OWN `GetHeight()` (region.rs: measured from the host font atlas
-- once text is set — confirmed live, not a guess). That measurement is a FRAME LATE relative
-- to *this* call (Lua runs before `resolve()`/the measure round-trip each tick, `ui_script/
-- mod.rs`'s drive loop) — a BRAND-NEW selection's text sizes off zero (never yet measured)
-- on this very populate, understating the true height. Nothing guarantees a SECOND populate
-- ever runs for the same stable selection (`QUEST_LOG_UPDATE` only fires on an actual data
-- change — `ui_quest_log.rs`'s diffed feed — not every tick), so relying on "the next
-- update self-heals it" silently under-sizes the child forever once the log goes quiet
-- (caught on the ui-questlog capture: the reward text visibly clipped with the scrollbar
-- never appearing). The honest fix is `BenillaQuestLogDetail_OnUpdate` below: it re-sums
-- every frame the window is open (cheap — its own comment) until the measurement lands and
-- the sum stops changing, which converges within a couple of frames of any repaint. A
-- same-frame exact answer isn't available without an engine-side "resize to fit" primitive;
-- this converges honestly instead of asserting a fiction.
-- A FontString's content height for the resize sum: blank text is a TRUE 0 regardless of
-- what GetHeight() answers. Needed because `measured` (region.rs) is a one-way cache keyed
-- off the LAST non-blank text — SetText("") never clears it (this engine has no live "0,0"
-- the instant a string blanks, unlike the real client's font engine), so a freshly-blanked
-- FontString would otherwise still report its previous text's height. A NON-blank string
-- still reports last tick's measurement (this function's own header comment on the lag).
function BenillaQuestLogDetail_TextHeight(fontString)
local text = fontString:GetText()
if not text or text == "" then return 0 end
return fontString:GetHeight()
end
-- Sums the child's needed height from this ONE chain's own fixed offsets (cited per term,
-- each already documented at its own FontString/SetPoint above) + every auto-height
-- FontString's measured `GetHeight()` + `rewardSectionHeight` (BenillaQuestLogRewards_Update's
-- own return — its header-through-money layout has its own conditional branches, cheaper to
-- total right where those branches already live than to re-derive them here). Then
-- `UpdateScrollChildRect()` so the scrollbar (BenillaQuestLogDetailScroll_SyncBar) hears the
-- new range.
function BenillaQuestLogDetail_ResizeChild(objAnchorIndex, rewardSectionHeight)
detailObjAnchorIndex = objAnchorIndex
detailRewardHeight = rewardSectionHeight
local title = getglobal("QuestLogQuestTitle")
local objectivesText = getglobal("QuestLogObjectivesText")
local description = getglobal("QuestLogDescription")
local objSpan
if objAnchorIndex > 0 then
objSpan = BENILLA_QUESTLOG_DETAIL_OBJ_GAP
+ objAnchorIndex * BENILLA_QUESTLOG_DETAIL_OBJ_ROW
+ (objAnchorIndex - 1) * BENILLA_QUESTLOG_DETAIL_OBJ_STEP
+ BENILLA_QUESTLOG_DETAIL_OBJ_GAP
else
objSpan = BENILLA_QUESTLOG_DETAIL_OBJ_GAP
end
-- The countdown row, when the selection has one (decision 1150): one more -10 hop and
-- one more fixed 12px row ahead of the objectives, whether or not any objective follows
-- it — both branches above start at the same ObjectivesText→next -10, so the delta is
-- the same either way. Read off the ref's own hasTimer flag rather than a second
-- parameter, so the OnUpdate re-check below re-sums it without threading state.
if BenillaQuestLogHasTimer then
objSpan = objSpan + BENILLA_QUESTLOG_DETAIL_OBJ_GAP + BENILLA_QUESTLOG_DETAIL_OBJ_ROW
end
local height = BENILLA_QUESTLOG_DETAIL_TOP
+ BenillaQuestLogDetail_TextHeight(title)
+ 5 -- QuestTitle -> ObjectivesText
+ BenillaQuestLogDetail_TextHeight(objectivesText)
+ objSpan
+ 18 -- DescriptionTitle: fixed span regardless of text (its own comment)
+ 5 -- DescriptionTitle -> Description
+ BenillaQuestLogDetail_TextHeight(description)
+ 15 -- Description -> RewardTitleText
+ rewardSectionHeight
+ BENILLA_QUESTLOG_DETAIL_BOTTOM_PAD
if height ~= detailLastAppliedHeight then
detailLastAppliedHeight = height
getglobal("QuestLogDetailChild"):SetHeight(height)
getglobal("QuestLogDetailScroll"):UpdateScrollChildRect()
end
end
-- The re-check `QuestLogDetailScroll`'s own `<OnUpdate>` calls every frame the log
-- window is open (`UiScript::tick`'s OnUpdate gate gates it on effective-visibility, so this
-- costs nothing while the window is closed): re-sums the SAME inputs the last full populate
-- computed (`BenillaQuestLogDetail_ResizeChild`'s own header comment on why a single populate
-- isn't enough — the auto-height FontStrings' `GetHeight()` only reflects a NEW text's real
-- measurement one tick after this function first set it). `ResizeChild`'s own
-- detailLastAppliedHeight guard makes every one of these calls a no-op once the resize has
-- converged (no host measurement changed since the last check).
function BenillaQuestLogDetail_OnUpdate()
BenillaQuestLogDetail_ResizeChild(detailObjAnchorIndex, detailRewardHeight)
end
-- ref QuestLog_OnUpdate (QuestLogFrame.lua:96-105), re-expressed here including its
-- UPDATE_DELAY throttle: the countdown is repainted ten times a second, not every frame —
-- SecondsToTime's output only changes once a second, so anything faster is a re-measure of
-- an identical string. The flag/accumulator pair is the ref's own (its two fields on
-- QuestLogFrame; ours are file globals because our detail state already lives that way).
function QuestLog_OnUpdate(elapsed)
if not BenillaQuestLogHasTimer then
return
end
BenillaQuestLogTimePassed = (BenillaQuestLogTimePassed or 0) + (elapsed or 0)
if BenillaQuestLogTimePassed > BENILLA_QUESTLOG_UPDATE_DELAY then
local left = GetQuestLogTimeLeft()
getglobal("QuestLogTimerText"):SetText(
BENILLA_TIME_REMAINING .. " " .. SecondsToTime(left or 0))
BenillaQuestLogTimePassed = 0
end
end
-- Steps the detail ScrollFrame by `delta` px — the wheel handler and both scrollbar arrow
-- buttons share this one path; SetVerticalScroll's own clamp (benilla-ui's `scrollframe.rs`)
-- does the range-bounding, this function just supplies the signed step.
function BenillaQuestLogDetailScroll_Step(delta)
local sf = getglobal("QuestLogDetailScroll")
sf:SetVerticalScroll(sf:GetVerticalScroll() + delta)
end
-- Repaints the minimal scrollbar (this window's own scrollbar comment: VISUAL only, no
-- drag) from the CURRENT offset/range — wired to both OnVerticalScroll (the value changed)
-- and OnScrollRangeChanged (the content resized), the ref UIPanelScrollFrameTemplate's own
-- pair (UIPanelTemplates.xml). Hides the whole bar when nothing overflows (range 0); disables
-- either arrow at its own end, mirroring the ref's identical two Enable/Disable calls off a
-- real Slider's min/max (there's no Slider here — this window's own comment on why).
function BenillaQuestLogDetailScroll_SyncBar()
local sf = getglobal("QuestLogDetailScroll")
local value = sf:GetVerticalScroll()
local range = sf:GetVerticalScrollRange()
local bar = getglobal("QuestLogDetailScrollBar")
if range <= 0 then
bar:Hide()
return
end
bar:Show()
if value > 0 then
getglobal("QuestLogDetailScrollUp"):Enable()
else
getglobal("QuestLogDetailScrollUp"):Disable()
end
if value < range then
getglobal("QuestLogDetailScrollDown"):Enable()
else
getglobal("QuestLogDetailScrollDown"):Disable()
end
local travel = BENILLA_QUESTLOG_SCROLL_TRACK_H - BENILLA_QUESTLOG_SCROLL_THUMB_H
local frac = value / range
getglobal("QuestLogDetailScrollThumb"):SetPoint(
"TOPLEFT", getglobal("QuestLogDetailScrollTrack"), "TOPLEFT", 0, -(frac * travel))
end
function BenillaQuestLogDetail_Clear()
getglobal("QuestLogQuestTitle"):SetText("")
getglobal("QuestLogObjectivesText"):SetText("")
-- The ref clears hasTimer on the empty-log path too (QuestLog_Update, :108) — without
-- it the per-tick repaint keeps writing a countdown into a pane with no selection, and
-- the child height keeps paying for a row that isn't there.
BenillaQuestLogHasTimer = nil
getglobal("QuestLogTimerText"):SetText("")
getglobal("QuestLogDescriptionTitle"):SetText("")
getglobal("QuestLogDescription"):SetText("")
for i = 1, MAX_OBJECTIVES do
getglobal("QuestLogObjective" .. i):SetText("")
end
getglobal("QuestLogRewardTitleText"):SetText("")
getglobal("QuestLogItemChooseText"):SetText("")
getglobal("QuestLogItemReceiveText"):SetText("")
for i = 1, BENILLA_QUESTLOG_MAX_ITEMS do
getglobal("QuestLogChoice" .. i):Hide()
getglobal("QuestLogReward" .. i):Hide()
end
BenillaMoney_Clear("QuestLogMoney")
-- Nothing selected: the header-only span (BenillaQuestLogRewards_Update's own
-- haveAnyReward==false case, 18px) is the right resize input here too.
BenillaQuestLogDetail_ResizeChild(0, 18)
end
-- Fill + show one reward/choice row. `getter` is GetQuestLogChoiceInfo or
-- GetQuestLogRewardInfo (pin §6): both answer the shared 5-tuple `name, texture, count,
-- quality, usable` — the same shape QuestFrame.xml's GetQuestItemInfo(kind, index) answers
-- for the questgiver panels, just under quest-log-specific getter names instead of one
-- kind-dispatched function. `kind` ("choice"/"reward") rides alongside because the LINK
-- getter IS kind-dispatched — GetQuestLogItemLink(this.type, this:GetID()), ref
-- QuestLogFrame.lua:545/549 — and because it is the ref's own `questItem.type`
-- (QuestFrame.lua:369/476, the shared layout routine both windows run).
function BenillaQuestLogItem_Set(button, getter, index, kind)
local name, texture, count, quality, usable, itemId = getter(index)
button.index = index
button.kind = kind
button.itemId = itemId
-- ref QuestFrame.lua:388/490 ("-- For the tooltip"), the same stamp the giver window's
-- BenillaQuestItem_Set makes: only the reward-SPELL row ever reads "spell" (l.445), and
-- v1 builds none (GetQuestLogRewardSpell is nil — this file's header). The click arms
-- below guard on it exactly as the ref does, so the guard is live when that row lands.
button.rewardType = "item"
button.quality = quality
button.icon:SetTexture(texture or BENILLA_FALLBACK_ICON)
button.name:SetText(name or "...")
if (count or 1) > 1 then button.count:SetText(count) else button.count:SetText("") end
local r, g, b = 1, 1, 1
if quality and BenillaTooltip_QualityColor then r, g, b = BenillaTooltip_QualityColor(quality) end
button.name:SetTextColor(r, g, b)
button:Show()
end
-- ref QuestLogRewardItem_OnClick (QuestLogFrame.lua:542-552), the OnClick
-- QuestLogRewardItemTemplate carries (ref QuestLogFrame.xml:118-120): CTRL previews the
-- reward in the dressing room (decision 1060), SHIFT posts its link into an open chat edit
-- box (decision 1059). There is no third arm here — a quest-log reward row is never
-- selectable, so a plain click is inert, which is why these rows carried no OnClick before
-- this arc.
--
-- Guard-for-guard with the ref: ctrl is gated on `this.rewardType ~= "spell"` (l.544), and
-- so is shift — but on the ELSEIF line itself (l.547), where the giver window's twin puts it
-- inside (QuestFrame.lua:121). Same outcome, and kept where the ref puts it. Ours
-- adds `ChatFrameEditBox and` (ChatFrame.xml is a separate file a window test need not
-- load — BenillaPaperDollSlot_OnClick's precedent) and a nil-link guard: `link` is nil until
-- the ask-once item template lands, a state the reference's synchronous item cache never
-- has, and EditBox:Insert takes a string (DressUpItemLink carries the same guard itself —
-- ref DressUpFrame.lua:11-13).
function QuestLogRewardItem_OnClick()
local button = this
if IsControlKeyDown() then
if button.rewardType ~= "spell" then
DressUpItemLink(GetQuestLogItemLink(button.kind, button.index))
end
elseif IsShiftKeyDown() and button.rewardType ~= "spell" then
-- The visible/non-nil pair is the shared helper's (UIParent.xml).
BenillaChatEdit_InsertLink(GetQuestLogItemLink(button.kind, button.index))
end
end
-- The reward-panel layout, ported from the ref's shared `QuestFrameItems_Update`
-- (QuestFrame.lua:311-522, the same routine the live QuestFrame reward panel uses) — this
-- window's v1 subset of it: no spell-reward row (GetQuestLogRewardSpell has no v1 caller,
-- QuestLogFrame.xml's header comment), capped at BENILLA_QUESTLOG_MAX_ITEMS (6, half the ref's
-- MAX_NUM_ITEMS=10, same halving QuestFrame.xml's own BENILLA_QUEST_MAX_ITEMS does). Every
-- SetPoint below mirrors the ref's own line-for-line (cited per block) — this is the mechanism
-- the ref itself uses; a static XML band was never right, and now that a runtime SetPoint
-- re-propagates to dependents (commit d0c23c5) we don't need to fake it with fixed offsets.
--
-- RETURNS the section's own total height (RewardTitleText's top to the lowest visible row's
-- bottom) for `BenillaQuestLogDetail_ResizeChild` to add onto the rest of the chain — this is
-- the one place that already knows every conditional branch (choices-only / fixed-only /
-- money-only / any combination), cheaper to total right here than to re-derive elsewhere.
function BenillaQuestLogRewards_Update()
local numChoices = GetNumQuestLogChoices()
local numRewards = GetNumQuestLogRewards()
local money = GetQuestLogRewardMoney()
-- pin §3 / QuestFrame.lua:343-349: QuestLogRewardTitleText shown only if
-- numRewards+numChoices+money > 0. Text is toggled rather than Show/Hide — see
-- BenillaQuestLogList_Update's comment on this file's blank-text convention.
local header = getglobal("QuestLogRewardTitleText")
local haveAnyReward = numChoices > 0 or numRewards > 0 or (money and money > 0)
if haveAnyReward then
header:SetText("Rewards")
else
header:SetText("")
end
-- ── Choosable rewards (QuestFrame.lua:367-413): 2-per-row under ItemChooseText. Row 1
-- anchors below the header text (-3,-5); each following ODD index anchors BOTTOMLEFT of
-- the row two back (0,-2) — starts a new row; each EVEN index anchors TOPRIGHT of the
-- previous (1,0) — sits beside it. ──
local chooseText = getglobal("QuestLogItemChooseText")
local lastChoice = math.min(numChoices, BENILLA_QUESTLOG_MAX_ITEMS)
if numChoices > 0 then
chooseText:SetText("You may choose one of these rewards:")
else
chooseText:SetText("")
end
for i = 1, BENILLA_QUESTLOG_MAX_ITEMS do
local choice = getglobal("QuestLogChoice" .. i)
if i <= lastChoice then
BenillaQuestLogItem_Set(choice, GetQuestLogChoiceInfo, i, "choice")
if i == 1 then
choice:SetPoint("TOPLEFT", chooseText, "BOTTOMLEFT", -3, -5)
elseif i % 2 == 1 then
choice:SetPoint("TOPLEFT", getglobal("QuestLogChoice" .. (i - 2)), "BOTTOMLEFT", 0, -2)
else
choice:SetPoint("TOPLEFT", getglobal("QuestLogChoice" .. (i - 1)), "TOPRIGHT", 1, 0)
end
else
choice:Hide()
end
end
-- ── "You will receive:" / "You will also receive:" (QuestFrame.lua:454-473): shown
-- whenever there's a fixed reward OR money, even with zero fixed-reward rows (money-only).
-- Anchored below the LAST CHOICE ROW'S LEFT COLUMN (the shorter of the two columns) when
-- choices exist, else below the Rewards header directly. ──
local receiveText = getglobal("QuestLogItemReceiveText")
local haveFixed = numRewards > 0 or (money and money > 0)
if haveFixed then
if numChoices > 0 then
receiveText:SetText("You will also receive:")
local anchorIndex = lastChoice
if anchorIndex % 2 == 0 then anchorIndex = anchorIndex - 1 end
receiveText:SetPoint("TOPLEFT", getglobal("QuestLogChoice" .. anchorIndex), "BOTTOMLEFT", 3, -5)
else
receiveText:SetText("You will receive:")
receiveText:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 3, -5)
end
else
receiveText:SetText("")
end
-- ── Fixed rewards (QuestFrame.lua:474-513): row 1 anchors below ItemReceiveText (-3,-5);
-- the same odd/even 2-per-row pattern as the choice rows above, chained off each other. ──
local lastReward = math.min(numRewards, BENILLA_QUESTLOG_MAX_ITEMS)
for i = 1, BENILLA_QUESTLOG_MAX_ITEMS do
local reward = getglobal("QuestLogReward" .. i)
if i <= lastReward then
BenillaQuestLogItem_Set(reward, GetQuestLogRewardInfo, i, "reward")
if i == 1 then
reward:SetPoint("TOPLEFT", receiveText, "BOTTOMLEFT", -3, -5)
elseif i % 2 == 1 then
reward:SetPoint("TOPLEFT", getglobal("QuestLogReward" .. (i - 2)), "BOTTOMLEFT", 0, -2)
else
reward:SetPoint("TOPLEFT", getglobal("QuestLogReward" .. (i - 1)), "TOPRIGHT", 1, 0)
end
else
reward:Hide()
end
end
-- Money: a STATIC XML anchor (LEFT of ItemReceiveText's RIGHT, +15,0 — ref
-- QuestLogFrame.xml:896-903) tracks receiveText's runtime SetPoint above via the resolver's
-- fixpoint re-propagation; only the coin fill/clear is Lua's job here.
if money and money > 0 then
BenillaMoney_Set("QuestLogMoney", money, true)
else
BenillaMoney_Clear("QuestLogMoney")
end
-- The section's total height, mirroring the branches above term-for-term (a money-only
-- coin row adds no height of its own — it rides receiveText's own row, LEFT of it, not
-- a new one — this window's own coin-anchor comment just above).
local height = 18 -- RewardTitleText: fixed span regardless of text (its own comment)
if haveAnyReward then
if numChoices > 0 then
local numChoiceRows = math.ceil(lastChoice / 2)
height = height
+ 5 -- header -> chooseText
+ BenillaQuestLogDetail_TextHeight(chooseText)
+ 5 -- chooseText -> choice row 1
+ numChoiceRows * 41 + (numChoiceRows - 1) * 2
end
if haveFixed then
height = height + 5 -- (last choice row, or the header) -> receiveText
+ BenillaQuestLogDetail_TextHeight(receiveText)
if numRewards > 0 then
local numRewardRows = math.ceil(lastReward / 2)
height = height + 5 -- receiveText -> reward row 1
+ numRewardRows * 41 + (numRewardRows - 1) * 2
end
end
end
return height
end
-- pin §3/§5's doNotScroll asymmetry (QuestLogFrame.lua:348/455-457, QUESTLOG-PIN.md's own
-- section on it): a MANUAL reselect (`BenillaQuestLogTitle_OnClick`, no arg here) always
-- snaps the detail pane's scroll back to the top; the QUEST_LOG_UPDATE data-refresh path
-- (`BenillaQuestLogFrame_OnEvent` below) passes `doNotScroll=true` so a quest updating while
-- already being read doesn't yank the player's scroll position out from under them.
function BenillaQuestLogDetail_Update(doNotScroll)
local selection = GetQuestLogSelection()
if selection == 0 or selection > GetNumQuestLogEntries() then
BenillaQuestLogDetail_Clear()
if not doNotScroll then
getglobal("QuestLogDetailScroll"):SetVerticalScroll(0)
end
return
end
-- pin §3, QuestLog_UpdateQuestDetails (QuestLogFrame.lua:349-356): title from
-- GetQuestLogTitle's FIRST return only; " - (Failed)" appended when failed.
local title = GetQuestLogTitle(selection)
if IsCurrentQuestFailed() then
title = (title or "") .. " - (Failed)"
end
getglobal("QuestLogQuestTitle"):SetText(title or "")
-- pin §3: description is GetQuestLogQuestText()'s FIRST return, the objectives FLAVOR
-- paragraph (not the leaderboard lines) is the second.
local description, objectivesText = GetQuestLogQuestText()
getglobal("QuestLogObjectivesText"):SetText(objectivesText or "")
-- pin §3, QuestLog_UpdateQuestDetails (QuestLogFrame.lua:364-374) followed, minus the
-- Show/Hide our region model has no verb for: a timed quest gets the "Time Remaining:"
-- row and pushes Objective1 below it; an untimed one blanks the row (our stand-in for
-- the ref's :Hide()) and anchors Objective1 straight onto ObjectivesText, so the row
-- costs an untimed quest no space at all. `hasTimer` is the ref's own flag and drives
-- both the per-tick repaint (QuestLog_OnUpdate) and the child-height sum.
local questTimer = GetQuestLogTimeLeft()
local timerText = getglobal("QuestLogTimerText")
local objective1 = getglobal("QuestLogObjective1")
if questTimer then
BenillaQuestLogHasTimer = 1
BenillaQuestLogTimePassed = 0
timerText:SetText(BENILLA_TIME_REMAINING .. " " .. SecondsToTime(questTimer))
objective1:SetPoint("TOPLEFT", timerText, "BOTTOMLEFT", 0, -10)
else
BenillaQuestLogHasTimer = nil
timerText:SetText("")
objective1:SetPoint("TOPLEFT", getglobal("QuestLogObjectivesText"), "BOTTOMLEFT", 0, -10)
end
getglobal("QuestLogDescription"):SetText(description or "")
-- Lua-managed header (ref text=QUEST_DESCRIPTION is static since the ref always has a real
-- selection whenever this pane is even visible; our flat window keeps the same FontString
-- alive with no selection, so THIS function — not a static `text=` — owns "Description",
-- blanked by BenillaQuestLogDetail_Clear on the empty-log path).
getglobal("QuestLogDescriptionTitle"):SetText("Description")
local numObjectives = GetNumQuestLeaderBoards()
for i = 1, MAX_OBJECTIVES do
local line = getglobal("QuestLogObjective" .. i)
if i <= numObjectives then
local text, kind, finished = GetQuestLogLeaderBoard(i)
local display = text
if not display or display == "" then display = kind or "" end
-- Ref QuestLogFrame.lua:389-394 verbatim: finished objectives go (0.2,0.2,0.2)
-- with the "(Complete)" suffix, unfinished stay (0,0,0) — both on the parchment.
if finished then
display = display .. " (Complete)"
line:SetTextColor(0.2, 0.2, 0.2)
else
line:SetTextColor(0, 0, 0)
end
line:SetText(display)
else
line:SetText("")
end
end
-- pin §3, QuestLog_UpdateQuestDetails (QuestLogFrame.lua:414-424): DescriptionTitle is
-- runtime-SetPoint'd off the LAST actually-used objective slot, not a static chain off
-- Objective10 — a quest with fewer than 10 objectives would otherwise drag this header
-- down by however much dead space the unused tail slots add (this window's own comment
-- on the FontString). No RequiredMoney branch here (this file's documented v1 gap: no
-- widget backs GetQuestLogRequiredMoney yet) — that ref branch collapses into the rest.
-- The objective-less TIMED branch (ref :432-437) is kept: a timed quest with no
-- leaderboard lines hangs the header off the countdown row, not off ObjectivesText.
local objAnchorIndex = math.min(numObjectives, MAX_OBJECTIVES)
local descTitle = getglobal("QuestLogDescriptionTitle")
if objAnchorIndex > 0 then
descTitle:SetPoint("TOPLEFT", getglobal("QuestLogObjective" .. objAnchorIndex), "BOTTOMLEFT", 0, -10)
elseif questTimer then
descTitle:SetPoint("TOPLEFT", getglobal("QuestLogTimerText"), "BOTTOMLEFT", 0, -10)
else
descTitle:SetPoint("TOPLEFT", getglobal("QuestLogObjectivesText"), "BOTTOMLEFT", 0, -10)
end
local rewardHeight = BenillaQuestLogRewards_Update()
BenillaQuestLogDetail_ResizeChild(objAnchorIndex, rewardHeight)
if not doNotScroll then
getglobal("QuestLogDetailScroll"):SetVerticalScroll(0)
end
end
-- ── The always-visible quest-watch tracker (QuestWatchFrame) ─────────────────────────
-- Ref QuestWatch_Update (QuestLogFrame.lua:602-663), re-expressed onto the flat 30-line pool
-- (the frame's own XML comment): every paint re-anchors each used line — a quest TITLE drops
-- -5 below the previous line (ref -4 under its 13px boxes; ours are measured 12s, same
-- pitch), an OBJECTIVE drops -1 (ref 0) — writes " - "-prefixed objective rows colored by
-- the ref law (finished WHITE, unfinished 0.8-gray; the title dark-gold 0.75/0.61/0 until
-- every objective completes, then full gold), sizes the frame to the content (width
-- maxW+10 growing LEFT off the TOPRIGHT anchor, height lines×13), recolors the log
-- window's Track dot, and hides the pool's trailing lines. `GetQuestIndexForWatch(w)`
-- answers each watch's CURRENT log index (it follows the quest across a log shuffle —
-- `quest_log.rs`'s id-keyed watch set), exactly like the ref.
function QuestWatch_Update()
local watchTextIndex = 1
local maxWidth = 0
for i = 1, GetNumQuestWatches() do
local questIndex = GetQuestIndexForWatch(i)
if questIndex then
local numObjectives = GetNumQuestLeaderBoards(questIndex)
if numObjectives > 0 then
local titleLine = getglobal("QuestWatchLine" .. watchTextIndex)
titleLine:SetText(GetQuestLogTitle(questIndex) or "")
if watchTextIndex > 1 then
titleLine:ClearAllPoints()
titleLine:SetPoint("TOPLEFT", getglobal("QuestWatchLine" .. (watchTextIndex - 1)), "BOTTOMLEFT", 0, -5)
end
titleLine:Show()
if titleLine:GetStringWidth() > maxWidth then maxWidth = titleLine:GetStringWidth() end
watchTextIndex = watchTextIndex + 1
local objectivesCompleted = 0
for j = 1, numObjectives do
local text, kind, finished = GetQuestLogLeaderBoard(j, questIndex)
if not text or text == "" then text = kind or "" end
local line = getglobal("QuestWatchLine" .. watchTextIndex)
line:SetText(" - " .. text)
if finished then
line:SetTextColor(1, 1, 1)
objectivesCompleted = objectivesCompleted + 1
else
line:SetTextColor(0.8, 0.8, 0.8)
end
line:ClearAllPoints()
line:SetPoint("TOPLEFT", getglobal("QuestWatchLine" .. (watchTextIndex - 1)), "BOTTOMLEFT", 0, -1)
line:Show()
if line:GetStringWidth() > maxWidth then maxWidth = line:GetStringWidth() end
watchTextIndex = watchTextIndex + 1
end
if objectivesCompleted == numObjectives then
titleLine:SetTextColor(1, 0.82, 0)
else
titleLine:SetTextColor(0.75, 0.61, 0)
end
end
end
end
-- The log window's Track dot (the ref's tracking-indicator tail): green with ≥1
-- watch, red with none.
if GetNumQuestWatches() > 0 then
getglobal("QuestLogTrackTracking"):SetVertexColor(0, 1.0, 0)
else
getglobal("QuestLogTrackTracking"):SetVertexColor(1.0, 0, 0)
end
local frame = getglobal("QuestWatchFrame")
if watchTextIndex == 1 then
frame:Hide()
return
end
frame:Show()
frame:SetHeight(watchTextIndex * 13)
if maxWidth > 0 then frame:SetWidth(maxWidth + 10) end
-- The GetStringWidth reads above are extract-measured — a frame stale after a text
-- change — so the OnUpdate sizing tail re-reads them for two frames and converges.
frame.used_lines = watchTextIndex - 1
frame.resize_pending = 2
for i = watchTextIndex, BENILLA_QUESTWATCH_LINES do
getglobal("QuestWatchLine" .. i):Hide()
end
end
-- ── Auto quest watching (ref QuestLogFrame.lua:702-786; AUTO_QUEST_WATCH defaults "1" —
-- "Quests are automatically watched for 5 minutes when you achieve a quest objective.").
-- BENILLA_QUEST_PROGRESS(questLogIndex) — our own engine event — arms a 300 s timed
-- watch: a manual shift-click watch is permanent (no timer entry) and never downgraded,
-- fresh progress re-arms a timed one, expiry unwatches, and a full watch list evicts the
-- timed entry nearest expiry (never a manual one). Timers key by quest TITLE (stable
-- across log shuffles; the engine's watch set is id-keyed — the ref's index-keyed
-- QUEST_WATCH_LIST bookkeeping is transcribed to its observable law, not its
-- iterate-while-mutating quirks).
--
-- Why NOT the native QUEST_WATCH_UPDATE the ref Lua consumes: the §5-verified engine
-- (wow-re quest-update-ui-feedback-law.md, handler 0x5e5ad0) fires that event with the
-- quest's 1-based WATCH-LIST position — 0 when unwatched — while the ref's
-- AutoQuestWatch_Update(arg1) treats arg1 as a quest-LOG index: the shipped 1.12 chain
-- is broken at exactly that seam (an unwatched quest's progress arrives as 0, and
-- AddQuestWatch(0) no-ops), so the advertised auto-watch never actually adds a quest.
-- We keep the advertised behavior — the feature the option tooltip documents — off the
-- benilla event that carries what the Lua needed; a deliberate intent-over-letter
-- divergence (the fold-back decision record).
--
-- The FLAG is the ref's own uvar, under the ref's own name since decision 1136 — it was
-- BENILLA_AUTO_QUEST_WATCH (a boolean) while nothing could change it. It is a "1"/"0"
-- string now because that is what an options row writes and what the reference's own
-- consumers compare against; the Interface page's "Automatic Quest Tracking" row drives it.
--
-- Which is the SECOND break in this feature, independent of the event seam above and just
-- as fatal: 1.12 assigns `AUTO_QUEST_WATCH = 1` — the NUMBER — at UIOptionsFrame.lua:122,
-- while every consumer tests `AUTO_QUEST_WATCH == "1"` (QuestLogFrame.lua:64/68) and the
-- checkbox loads on the same string compare. In Lua 5.0 `1 ~= "1"`, so on a fresh install
-- the option paints UNCHECKED and the auto-watch is dead, despite the table declaring
-- `default = "1"`; it only comes alive once the player clicks the box, which writes the
-- string. We ship the string, so the advertised default is the real one.
AUTO_QUEST_WATCH = "1"
RegisterForSave("AUTO_QUEST_WATCH")
BENILLA_QUEST_WATCH_TIMER = 300
BENILLA_QUEST_WATCH_TIMERS = {}
function BenillaQuestWatch_RemoveByTitle(title)
for w = 1, GetNumQuestWatches() do
local questIndex = GetQuestIndexForWatch(w)
if questIndex and GetQuestLogTitle(questIndex) == title then
RemoveQuestWatch(questIndex)
return
end
end
end
function AutoQuestWatch_Update(questIndex)
if AUTO_QUEST_WATCH ~= "1" or not questIndex then return end
local title = GetQuestLogTitle(questIndex)
if not title then return end
if IsQuestWatched(questIndex) then
if BENILLA_QUEST_WATCH_TIMERS[title] then
BENILLA_QUEST_WATCH_TIMERS[title] = BENILLA_QUEST_WATCH_TIMER
end
return
end
if GetNumQuestLeaderBoards(questIndex) == 0 then return end
if GetNumQuestWatches() >= BENILLA_QUESTLOG_MAX_WATCHES then
local lowestTitle, lowestTimer = nil, BENILLA_QUEST_WATCH_TIMER + 1
for t, timer in pairs(BENILLA_QUEST_WATCH_TIMERS) do
if timer < lowestTimer then lowestTitle, lowestTimer = t, timer end
end
if not lowestTitle then return end -- every slot is a manual watch: no eviction
BENILLA_QUEST_WATCH_TIMERS[lowestTitle] = nil
BenillaQuestWatch_RemoveByTitle(lowestTitle)
end
AddQuestWatch(questIndex)
BENILLA_QUEST_WATCH_TIMERS[title] = BENILLA_QUEST_WATCH_TIMER
end
function AutoQuestWatch_OnUpdate(elapsed)
local expired = nil
for title, timer in pairs(BENILLA_QUEST_WATCH_TIMERS) do
if timer - elapsed <= 0 then
expired = expired or {}
table.insert(expired, title)
else
BENILLA_QUEST_WATCH_TIMERS[title] = timer - elapsed
end
end
if expired then
for _, title in ipairs(expired) do
BENILLA_QUEST_WATCH_TIMERS[title] = nil
BenillaQuestWatch_RemoveByTitle(title)
end
QuestWatch_Update()
BenillaQuestLogList_Update() -- the log's row ticks follow (ref: QuestLog_Update)
end
end
function BenillaQuestWatchFrame_OnLoad(frame)
-- Repaints on all three: QUEST_LOG_UPDATE (every log change — the ref's own
-- QuestLog_OnEvent repaints QuestWatch_Update() off it too, QuestLogFrame.lua:57-70),
-- the native QUEST_WATCH_UPDATE (arg1 = the WATCH-LIST index, 0 if unwatched — kept
-- for repaint + any future addon, never for auto-watch: the divergence comment
-- above), and BENILLA_QUEST_PROGRESS (arg1 = the quest-LOG index) which feeds the
-- auto-watch.
frame:RegisterEvent("QUEST_LOG_UPDATE")
frame:RegisterEvent("QUEST_WATCH_UPDATE")
frame:RegisterEvent("BENILLA_QUEST_PROGRESS")
QuestWatch_Update()
end
function BenillaQuestWatchFrame_OnEvent(frame)
if event == "BENILLA_QUEST_PROGRESS" then
AutoQuestWatch_Update(arg1)
QuestWatch_Update()
elseif event == "QUEST_WATCH_UPDATE" or event == "QUEST_LOG_UPDATE" then
QuestWatch_Update()
end
end
-- The watch frame's OnUpdate carries the ref's auto-watch timer decay (ref
-- QuestLogFrame.xml:1140-1144 — the ALWAYS-VISIBLE frame hosts it, so timers tick with
-- the log closed; hidden-with-no-watches is safe, an empty watch set has no timers) plus
-- our sizing-convergence tail (QuestWatch_Update's own comment).
function BenillaQuestWatchFrame_OnUpdate(frame, elapsed)
AutoQuestWatch_OnUpdate(elapsed or 0)
if frame.resize_pending and frame.resize_pending > 0 then
frame.resize_pending = frame.resize_pending - 1
local maxw = 0
for i = 1, (frame.used_lines or 0) do
local w = getglobal("QuestWatchLine" .. i):GetStringWidth()
if w > maxw then maxw = w end
end
if maxw > 0 then frame:SetWidth(maxw + 10) end
end
end
-- ── Window lifecycle ─────────────────────────────────────────────────────────────────────
-- `doNotScroll` (pin §5's asymmetry, threaded through to `BenillaQuestLogDetail_Update`):
-- absent/falsy for a manual call (a row click, the window's own OnShow); `true` from the
-- QUEST_LOG_UPDATE event handler below.
function BenillaQuestLogFrame_Update(doNotScroll)
QuestLog_SetFirstValidSelection()
BenillaQuestLogList_Update()
-- pin §5, QuestLog_OnEvent (QuestLogFrame.lua:57-63): the detail pane only repaints while
-- the window is visible — the list rebuild above runs unconditionally either way.
if getglobal("QuestLogFrame"):IsVisible() then
BenillaQuestLogDetail_Update(doNotScroll)
end
end
function BenillaQuestLogFrame_OnLoad(frame)
frame:RegisterEvent("QUEST_LOG_UPDATE")
end
function BenillaQuestLogFrame_OnEvent(frame)
if event == "QUEST_LOG_UPDATE" then
-- pin §5: the event-driven refresh path passes doNotScroll=true (QuestLogFrame.lua:62
-- passes `1`) — a quest updating while already being read must not yank the reader's
-- scroll position back to the top.
BenillaQuestLogFrame_Update(true)
end
end
-- pin §8, QuestLogFrame.lua:24-30 — the app's 'L' binding already calls this
-- (ui_script/mod.rs).
function ToggleQuestLog()
local frame = getglobal("QuestLogFrame")
if frame:IsVisible() then
HideUIPanel(frame)
else
ShowUIPanel(frame)
end
end
function BenillaQuestLogCloseButton_OnClick()
HideUIPanel(getglobal("QuestLogFrame"))
end
-- ── The abandon two-step (mark on click → confirm on Yes) — our `ABANDON_QUEST` entry on
-- the shared StaticPopup engine (UiPanels.xml, decision 0308 §3). Every field below is
-- StaticPopup's own vocabulary and every value is specified by StaticPopup.lua:749-761:
-- the ABANDON_QUEST_CONFIRM wording 'Abandon "%s"?' (GlobalStrings.lua:5), abandoning on
-- accept, and the igQuestLogAbandonQuest kit. There is no expression left to choose here,
-- which is why this reads the way the specification does. exclusive is v1-inert (engine
-- header note). ─────────────────────────────────────────────────────────────────────────
StaticPopupDialogs["ABANDON_QUEST"] = {
text = "Abandon \"%s\"?", -- ABANDON_QUEST_CONFIRM
button1 = "Yes", -- YES
button2 = "No", -- NO
OnAccept = function()
AbandonQuest()
PlaySound("igQuestLogAbandonQuest")
end,
timeout = 0,
whileDead = 1,
exclusive = 1,
hideOnEscape = 1,
}
-- ── ABANDON_QUEST_WITH_ITEMS — DECLARED, never shown ────────────────────────────────────
-- `GetAbandonQuestItems()` is always nil here (`quest_log.rs`), so nothing in this client
-- reaches `StaticPopup_Show("ABANDON_QUEST_WITH_ITEMS", …)`, and the click handler below
-- says so. That was taken as a reason to omit the *entry*, and it is not one: the
-- reference's StaticPopup.lua declares this table unconditionally at load
-- (StaticPopup.lua:762-775), whatever the client later shows, and an addon reading `_G`
-- reads declarations, not behaviour.
--
-- Questie is the measurement: `QuestieQuest.lua:55` is
-- `StaticPopupDialogs["ABANDON_QUEST_WITH_ITEMS"].OnAccept` at file scope, with no guard —
-- it wraps BOTH abandon dialogs to clear its own quest cache. Absent, that is
-- `attempt to index field 'ABANDON_QUEST_WITH_ITEMS' (a nil value)` and the addon's whole
-- quest module never loads. (#3 of the top-20 corpus; the same shape as `ChatFrame3..7`'s
-- "declared because addons read them", 1497.)
--
-- Byte-identical to ABANDON_QUEST above but for `text` — the reference's two entries differ
-- in exactly that one field. `%s` twice: the quest name, then the item list.
StaticPopupDialogs["ABANDON_QUEST_WITH_ITEMS"] = {
text = "Abandon \"%s\", destroying %s?", -- ABANDON_QUEST_CONFIRM_WITH_ITEMS
button1 = "Yes", -- YES
button2 = "No", -- NO
OnAccept = function()
AbandonQuest()
PlaySound("igQuestLogAbandonQuest")
end,
timeout = 0,
whileDead = 1,
exclusive = 1,
hideOnEscape = 1,
}
function BenillaQuestLogAbandonButton_OnClick()
if GetQuestLogSelection() == 0 then return end
-- pin §4, QuestLogFrame.xml:463-472: SetAbandonQuest() pins the click-time selection;
-- GetAbandonQuestItems() is always nil in v1 (`quest_log.rs`), so only the plain
-- ABANDON_QUEST popup path exists — never ABANDON_QUEST_WITH_ITEMS. (Ref
-- QuestLogFrame.lua's own click handler shows the same dialog with the name arg.)
SetAbandonQuest()
StaticPopup_Show("ABANDON_QUEST", GetAbandonQuestName() or "")
end
]]></Script>
<!--
One reward/choice item row — a byte-for-byte copy of `QuestFrame.xml`'s
`QuestItemTemplate` (ref `QuestItemTemplate`, QuestFrameTemplates.xml l.85-166),
DUPLICATED rather than inherited across files: this window's own tests load only its own
dependency list (Fonts/UiPanels/MerchantFrame — the money helpers), the same per-window
isolation this repo's other `*_tests.rs` already keep (e.g. `bag_tests.rs` loads
`MerchantFrame.xml` for its money helper only, never `QuestFrame.xml`). The real client also
keeps a SEPARATE `QuestLogRewardItemTemplate` from the live `QuestItemTemplate` (pin §3) — so
this mirrors the ref's own duplication, not just an engine workaround. Keep the two shapes in
sync by hand if either is revisited.
-->
<Button name="BenillaQuestLogItemTemplate" virtual="true" hidden="true">
<!-- Ref-verbatim geometry (QuestFrameTemplates.xml:85-138): a bare 39×39 icon at TOPLEFT
(no slot-well texture behind it — the earlier `UI-EmptySlot` here was our own addition
and read as a heavy black box on the parchment), the name plate overlapping the icon's
right edge by 10, and the name in GameFontHighlight (12px — not the Small variant). -->
<Size><AbsDimension x="147" y="41"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIconTexture">
<Size><AbsDimension x="39" y="39"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
</Texture>
<Texture name="$parentNameFrame" file="Interface\QuestFrame\UI-QuestItemNameFrame">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentIconTexture" relativePoint="RIGHT"><Offset><AbsDimension x="-10" y="0"/></Offset></Anchor></Anchors>
</Texture>
<FontString name="$parentName" inherits="GameFontHighlight" justifyH="LEFT">
<Size><AbsDimension x="90" y="36"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentNameFrame"><Offset><AbsDimension x="15" y="0"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentCount" inherits="NumberFontNormal" justifyH="RIGHT">
<Anchors><Anchor point="BOTTOMRIGHT" relativeTo="$parentIconTexture"><Offset><AbsDimension x="-4" y="1"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<!-- No HighlightTexture: the ref QuestLogRewardItemTemplate (QuestLogFrame.xml:108-123)
adds only tooltip/click scripts to QuestItemTemplate — reward rows never glow on
hover, exactly like the giver window's twin template. -->
<Scripts>
<OnLoad>
local n = self:GetName()
self.icon = getglobal(n .. "IconTexture")
self.name = getglobal(n .. "Name")
self.count = getglobal(n .. "Count")
</OnLoad>
<!-- The shared id-keyed item tooltip (ref QuestLogRewardItemTemplate OnEnter →
GameTooltip:SetQuestLogItem — the same C++ renderer every item hover uses). -->
<OnEnter>
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetItemById(self.itemId, self.name:GetText(), self.quality)
GameTooltip:Show()
</OnEnter>
<OnLeave>GameTooltip:Hide()</OnLeave>
<!-- ref QuestLogRewardItemTemplate's own OnClick (ref QuestLogFrame.xml:118-120): the
CTRL dressing-room preview + the SHIFT chat-link post, and nothing else — a plain
click on a quest-log reward row is inert (decisions 1059/1060). -->
<OnClick>QuestLogRewardItem_OnClick()</OnClick>
</Scripts>
</Button>
<!--
One list row (pin §2's QuestLogTitleButtonTemplate, QuestLogFrame.xml:4-107, ~300×16). Text
lives on a NAMED `$parentText` FontString (not the auto ButtonText slot — same convention
`GossipFrame.xml`'s `BenillaGossipRowTemplate` uses for its `$parentLabel`), so it can carry
an explicit LEFT justify. v1 simplification: the ref's Complete/Failed tag rides a SEPARATE
right-justified `$parentTag` FontString the client shrinks the title to make room for; we
append the tag straight onto the one title string instead (`BenillaQuestLogList_Update`) —
simpler, no dynamic width math, costs only the tag not being right-flush.
State art is now ref-verbatim (QuestLogTitleButtonTemplate, ref QuestLogFrame.xml:68-107):
the 16x16 plus/minus fold icon slot at LEFT+3 is the button's NormalTexture (the Lua swaps
Plus/Minus per header fold state and CLEARS it on quest rows — SetNormalTexture("")), the
16x16 UI-PlusButton-Hilight at the same spot is the ONLY hover art (quest rows clear it
too — the ref has NO row-wide hover glow), and the text starts at LEFT+20 (the real
indent; quest rows add the ref's own two-space prefix on top). The PERSISTENT selection bar
is the separate `QuestLogSelectHighlight` frame `BenillaQuestLogList_Update` moves.
`$parentCheck` (pin §2, ref `QuestLogFrame.xml:34-45`) is the per-row watch checkbox — a 16×16
`UI-CheckBox-Check`, no `file=` here (where the ref uses `hidden="true"`): it starts with
NO texture bound and is entirely SetTexture-driven from first paint. That shape dates from
before Texture regions had Show/Hide; `region.rs` has the real pair now and the loader
honours `hidden=` on regions, so the ref's exact shape is available here if this ever wants
it — the bind-driven form works and is left alone. SEAT (the director's
ref A/B — the first build parked it in a left gutter): the ref places the check just past
the TITLE'S INK — `QuestLog.lua:211/224` computes `LEFT of the row + dummyTextWidth + 24`,
and with the ref's text inset of 20 that nets to `ink end + 4`. We get the same geometry
from the anchor graph instead of the Lua dummy-measure hack: the title FontString is
width-less (its resolved rect hugs its host-measured ink — the measure round-trip), and
the check anchors LEFT → the title's RIGHT at +4. Known deviation, unchanged: the ref
shrinks the title width to fit the right-justified tag (its `textWidth` cap); our combined
title+tag string means a "(Complete)" row seats the check after the tag, not between
title and tag.
-->
<Button name="BenillaQuestLogTitleTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="300" y="16"/></Size>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" inherits="GameFontNormal" justifyH="LEFT">
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="20" y="0"/></Offset></Anchor></Anchors>
</FontString>
<Texture name="$parentCheck">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentText" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<NormalTexture file="Interface\Buttons\UI-MinusButton-UP">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</NormalTexture>
<HighlightTexture name="$parentHighlight" file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="3" y="0"/></Offset></Anchor></Anchors>
</HighlightTexture>
<Scripts>
<OnLoad>
self.text = getglobal(self:GetName() .. "Text")
self.check = getglobal(self:GetName() .. "Check")
-- Via the state-texture getter (era API), not getglobal: the loader doesn't yet
-- publish a named state texture's `name=` as a global (known gap — Layers regions
-- publish, state slots don't).
self.foldHighlight = self:GetHighlightTexture()
-- Both buttons must reach OnClick (the default set is LeftButtonUp only) — the same
-- convention `BagFrame.xml`'s `BenillaBagSlot_OnLoad` uses for its own left/right split.
self:RegisterForClicks("LeftButtonUp", "RightButtonUp")
</OnLoad>
<OnClick>BenillaQuestLogTitle_OnClick(self, arg1)</OnClick>
</Scripts>
</Button>
<!-- A panel action button (Abandon/Exit/Yes/No) — the UIPanelButtonTemplate face from
`QuestFrame.xml`'s `BenillaQuestPanelButtonTemplate`, duplicated for the same per-window
test-isolation reason as `BenillaQuestLogItemTemplate` above. UNLIKE the giver window's
template this one is NOT hidden="true": the giver's panel-switch Lua re-Show()s each
panel's pair every switch, but this window's Abandon/Exit are always-visible chrome (ref
QuestLogFrame.xml:451/482) with no code path that would ever Show() them — inheriting
hidden left them invisible (0109 look fix; caught on the ui-questlog capture). -->
<Button name="BenillaQuestLogPanelButtonTemplate" virtual="true">
<Size><AbsDimension x="80" y="22"/></Size>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<DisabledFont inherits="GameFontDisable"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-Panel-Button-Disabled"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
</Button>
<Frame name="QuestLogFrame" hidden="true">
<!-- The real 384×512 window (pin §1). No <Anchors> — ShowUIPanel/SetLeftFrame SetPoints this
to the left slot on open, exactly like Gossip/Merchant/Quest. -->
<Size><AbsDimension x="384" y="512"/></Size>
<Layers>
<Layer level="BACKGROUND">
<!-- pin §1: 64×64 book icon, TOPLEFT (4,-4). -->
<Texture file="Interface\QuestFrame\UI-QuestLog-BookIcon">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="4" y="-4"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<!-- pin §1: the four book-panel quadrants, each pinned to its corner, whole-texture. -->
<Texture file="Interface\QuestFrame\UI-QuestLog-TopLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors></Texture>
<Texture file="Interface\QuestFrame\UI-QuestLog-TopRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"/></Anchors></Texture>
<Texture file="Interface\QuestFrame\UI-QuestLog-BotLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"/></Anchors></Texture>
<Texture file="Interface\QuestFrame\UI-QuestLog-BotRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"/></Anchors></Texture>
<!-- pin §1: title, TOP (0,-15), GameFontHighlight, text token QUEST_LOG → "Quest Log". -->
<FontString name="QuestLogTitleText" inherits="GameFontHighlight" justifyH="CENTER" text="Quest Log">
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
</FontString>
<!-- pin §1, geometry/art byte-for-byte (ref QuestLogFrame.xml:204-243): the quest-count
pill is the Common-Input-Border 3-texture trio + a FontString RIGHT of the right
piece — REPLACING this window's old bare text at (-30,-16), which sat inside the
close X's hit area and visually collided with it. -->
<Texture name="QuestLogCountRight" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-47" y="-41"/></Offset></Anchor></Anchors>
<TexCoords left="0.9375" right="1.0" top="0" bottom="0.625"/>
</Texture>
<Texture name="QuestLogCountMiddle" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="100" y="20"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="QuestLogCountRight" relativePoint="LEFT"/></Anchors>
<TexCoords left="0.0625" right="0.9375" top="0" bottom="0.625"/>
</Texture>
<Texture name="QuestLogCountLeft" file="Interface\Common\Common-Input-Border">
<Size><AbsDimension x="8" y="20"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="QuestLogCountMiddle" relativePoint="LEFT"/></Anchors>
<TexCoords left="0" right="0.0625" top="0" bottom="0.625"/>
</Texture>
<FontString name="QuestLogCount" inherits="GameFontNormalSmall" justifyH="RIGHT">
<Anchors><Anchor point="RIGHT" relativeTo="QuestLogCountRight" relativePoint="RIGHT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor></Anchors>
</FontString>
<!-- v1 empty state (pin §2's EmptyQuestLogFrame, simplified — this file's header
comment): one centered message over the row area. No `text=`/`hidden=` here —
`BenillaQuestLogList_Update` owns this entirely via SetText(""), this file's
blank-text convention (see that function's comment). -->
<FontString name="QuestLogEmptyText" inherits="GameFontNormal" justifyH="CENTER">
<Size><AbsDimension x="300" y="0"/></Size>
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-90"/></Offset></Anchor></Anchors>
</FontString>
<!-- Detail page (pin §3): NOW lives on `QuestLogDetailChild`, a real
ScrollFrame's scroll child (`QuestLogDetailScroll`, below in this window's
own `<Frames>`) — the 0088 §2 flat-frame constraint this file used to quote here
DIED with the 0112 resolver fixpoint (child frames render their own `<Layers>`
regions again), so the whole chain (QuestTitle → ItemReceiveText, the choice/
reward Buttons, the money coins) moved there verbatim; only the FIRST element's
anchor changed (the child's own TOPLEFT, ref :624-635's offset 5,-5 — see the
child's own comment). Nothing detail-page-shaped remains in this window's own
`<Layers>`. -->
</Layer>
</Layers>
<Frames>
<!-- The scroll-wheel catcher (this file's header comment) — MUST be first so every real
widget below out-ranks it in hit-testing. -->
<Button name="QuestLogWheelCatcher">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"/>
</Anchors>
<Scripts><OnMouseWheel>BenillaQuestLogFrame_OnMouseWheel(arg1)</OnMouseWheel></Scripts>
</Button>
<!-- pin §1: the top-right close X (UIPanelCloseButton in the ref; our engine ships no such
template — QuestFrame.xml/BagFrame.xml already draw it by hand on the same art). -->
<Button name="QuestLogCloseButton">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-30" y="-8"/></Offset></Anchor></Anchors>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
<Scripts><OnClick>BenillaQuestLogCloseButton_OnClick()</OnClick></Scripts>
</Button>
<!-- The "All" fold-all tab (ref QuestLogExpandButtonFrame/QuestLogCollapseAllButton,
QuestLogFrame.xml:247-323): the sort-tab art trio behind a row-template button at
(70,-48) — its own +/- NormalTexture slot doubles as the all-state icon. The ref's
sibling "Track Quest" checkbox (QuestLogTrack) is a named deferral: our watch
toggle is the row right-click until the track-checkbox arc lands. -->
<Frame name="QuestLogExpandButtonFrame">
<Size><AbsDimension x="54" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="70" y="-48"/></Offset></Anchor></Anchors>
<Frames>
<Button name="QuestLogCollapseAllButton" inherits="BenillaQuestLogTitleTemplate" hidden="false" text="All">
<Size><AbsDimension x="40" y="22"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="QuestLogExpandTabLeft" file="Interface\QuestFrame\UI-QuestLogSortTab-Left">
<Size><AbsDimension x="8" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="-6" y="8"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="QuestLogExpandTabMiddle" file="Interface\QuestFrame\UI-QuestLogSortTab-Middle">
<Size><AbsDimension x="38" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="QuestLogExpandTabLeft" relativePoint="RIGHT"/></Anchors>
</Texture>
<Texture file="Interface\QuestFrame\UI-QuestLogSortTab-Right">
<Size><AbsDimension x="8" y="32"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="QuestLogExpandTabMiddle" relativePoint="RIGHT"/></Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnClick>QuestLogCollapseAllButton_OnClick()</OnClick>
</Scripts>
<!-- The tab label sits beside its own +/- icon slot (the ref row template's
ButtonText geometry: LEFT+20 next to the 16px icon at LEFT+3), gold —
the engine's default label centered "All" OVER the icon (director's
img-12 misalignment). -->
<ButtonText>
<Size><AbsDimension x="0" y="10"/></Size>
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="20" y="0"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal" justifyH="LEFT"/>
<HighlightFont inherits="GameFontHighlight" justifyH="LEFT"/>
</Button>
</Frames>
</Frame>
<!-- The "Track Quest" watch indicator (ref QuestLogTrack, QuestLogFrame.xml:324-369):
an INDICATOR, not a button — the 20×20 UI-RadioButton pair (base = the left
quarter, the dot = the second quarter) with the dot vertex-colored by
`QuestWatch_Update`: red when nothing is watched, green when ≥1 is (ref
`QuestWatch_Update`'s tracking-indicator tail). Watching itself stays the row
shift-click (`QUEST_WATCH_TOOLTIP`, served on hover). Ref HitRectInsets
(right −80, stretching the hover over the label) is an engine gap — the hover
rect is the 20×20 dot only. -->
<Frame name="QuestLogTrack" enableMouse="true">
<Size><AbsDimension x="20" y="20"/></Size>
<Anchors>
<Anchor point="LEFT" relativeTo="QuestLogExpandButtonFrame" relativePoint="RIGHT">
<Offset><AbsDimension x="5" y="10"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentTitle" inherits="GameFontHighlightSmall" text="Track Quest">
<Anchors><Anchor point="LEFT" relativePoint="RIGHT"/></Anchors>
</FontString>
<Texture file="Interface\Buttons\UI-RadioButton">
<TexCoords left="0" right="0.25" top="0" bottom="1"/>
</Texture>
<Texture name="QuestLogTrackTracking" file="Interface\Buttons\UI-RadioButton">
<TexCoords left="0.25" right="0.5" top="0" bottom="1"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
getglobal("QuestLogTrackTracking"):SetVertexColor(1.0, 0, 0)
</OnLoad>
<OnEnter>
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText("Shift-click a quest to add or remove a quest from your quest watch list.", nil, nil, nil, nil, 1)
</OnEnter>
<OnLeave>
GameTooltip:Hide()
</OnLeave>
</Scripts>
</Frame>
<!-- The persistent selection highlight (ref QuestLogHighlightFrame, QuestLogFrame.xml:
522-534): 293×16, ARTWORK-layer ADD `UI-QuestLogTitleHighlight`, declared BEFORE the
title rows so the row text paints OVER the glow (same parent + same frameLevel ⇒
declaration order is the z-tiebreak; the old `frameLevel="8"` put the glow ON TOP of
the title text and washed it out — 0112's capture gate caught the wash, this scrub
found the cause). Moved onto the selected row (at the ref's (5,0)) and vertex-colored
to the row's difficulty color by `BenillaQuestLogList_Update` (ref lua l.337-339). -->
<Frame name="QuestLogSelectHighlight" hidden="true">
<Size><AbsDimension x="293" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<Layers><Layer level="ARTWORK">
<Texture name="QuestLogSkillHighlight" file="Interface\QuestFrame\UI-QuestLogTitleHighlight" alphaMode="ADD">
<Anchors><Anchor point="TOPLEFT"/><Anchor point="BOTTOMRIGHT"/></Anchors>
</Texture>
</Layer></Layers>
</Frame>
<!-- pin §2, geometry byte-for-byte (ref QuestLogFrame.xml:535-588): 6 title rows,
300×16, row1 TOPLEFT of the frame at (19,-75); rows 2-6 chain TOPLEFT→prior BOTTOMLEFT
at (0,+1) — a TIGHT 1px OVERLAP, not a gap (the ref's own offset is positive). -->
<Button name="QuestLogTitle1" inherits="BenillaQuestLogTitleTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="19" y="-75"/></Offset></Anchor></Anchors>
</Button>
<Button name="QuestLogTitle2" inherits="BenillaQuestLogTitleTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogTitle1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor></Anchors></Button>
<Button name="QuestLogTitle3" inherits="BenillaQuestLogTitleTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogTitle2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor></Anchors></Button>
<Button name="QuestLogTitle4" inherits="BenillaQuestLogTitleTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogTitle3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor></Anchors></Button>
<Button name="QuestLogTitle5" inherits="BenillaQuestLogTitleTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogTitle4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor></Anchors></Button>
<Button name="QuestLogTitle6" inherits="BenillaQuestLogTitleTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogTitle5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor></Anchors></Button>
<!-- The detail pane's real ScrollFrame (decision 0112's mechanism): 300×261, TOPLEFT of
this window at (19,-175) — the ref's own `QuestLogDetailScrollFrame` position, this
file's "Detail chain start" arithmetic that used to live above (list bottom-left
(19,-168) - 7 = (19,-175)). Mouse-wheel-interactive by construction (no wheel-catcher
needed — `widget/mod.rs`'s ScrollFrame ctor comment), and declared AFTER
`QuestLogWheelCatcher` in this `<Frames>` list so it out-ranks the catcher for
any wheel/click landing inside its own rect (this file's header comment on hit-test
z-order) — scrolling over the detail text no longer scrolls the LIST underneath it,
a latent mix-up the flat layout had no way to avoid. -->
<ScrollFrame name="QuestLogDetailScroll">
<Size><AbsDimension x="300" y="261"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="19" y="-175"/></Offset></Anchor></Anchors>
<Frames>
<!-- The scroll child (`SetScrollChild`'d in this ScrollFrame's own OnLoad below):
300 wide, matching the ref's `QuestLogDetailScrollChildFrame`; HEIGHT is
runtime-set by `BenillaQuestLogDetail_ResizeChild` (a Frame's height is only
ever its own SetSize — the resolver never grows one to fit overflowing
content, that function's own comment) — 261 here is just a sane starting size
before the first Detail_Update ever runs. Every widget below is the SAME
detail chain this window used to carry flat on itself (this window's own
"Detail page" comment) — only QuestTitle's own anchor changed, to the CHILD's
TOPLEFT (ref :624-635's offset 5,-5) now that it's the origin of its own
subtree; everything downstream still chains by NAME exactly as before (anchor
resolution is name-keyed, not parent-scoped — unaffected by which frame owns
which region). -->
<Frame name="QuestLogDetailChild">
<Size><AbsDimension x="300" y="261"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString name="QuestLogQuestTitle" inherits="QuestTitleFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="5" y="-5"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="QuestLogObjectivesText" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="275" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogQuestTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor></Anchors>
</FontString>
<!-- The timed-quest countdown row (decision 1150) — the ref's QuestLogTimerText,
QuestLogFrame.xml:648-659, at its own anchor and offset. Untimed quests are the
overwhelming majority, so the row must cost them NOTHING: the ref hides it and
re-SetPoints Objective1 straight onto ObjectivesText (QuestLogFrame.lua:370/374),
and `BenillaQuestLogDetail_Update` keeps exactly that pair of branches. Our
region model has no Show/Hide for a bare FontString (the recurring comment
below), so the untimed case leaves this string EMPTY — it draws nothing, and
nothing anchors through it, so its 12px sits harmlessly behind Objective1's row
rather than displacing anything. -->
<FontString name="QuestLogTimerText" inherits="QuestFontNormalSmall" justifyH="LEFT">
<Size><AbsDimension x="285" y="12"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjectivesText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</FontString>
<!-- No `hidden=` on these 10 (a no-op on a region anyway — same reasoning as the
empty-state text above): each starts textless (no `text=` attribute) and stays
that way until `BenillaQuestLogDetail_Update` fills the ones in use.
Objective1's XML anchor is the UNTIMED one (straight onto ObjectivesText) rather
than the ref's static hop through the timer row, because in this engine that
row cannot be hidden — the timed branch re-SetPoints it through
QuestLogTimerText, which is the ref's own runtime behaviour either way
(QuestLogFrame.lua:370/374; only the XML seed differs, and the ref re-points it
on every populate before anything is drawn).
Height is EXPLICIT 12 (GameFontNormalSmall is 10px, `Fonts.xml`), NOT the ref's
auto "y=0" — a deliberate v1 deviation: a FontString anchored by a single point
with an explicit height of exactly 0 and no text ever measured (this engine only
host-measures a FontString with non-empty text — `UiScript::fontstrings_needing_
measure`'s own doc) resolves its far edge to `combine_edge`'s span==0 branch,
which is UNSET, so `resolve()`'s region pass falls the WHOLE unconstrained edge
back to the OWNER FRAME's edge (`script/mod.rs`'s region-resolution comment) —
not "zero height", the owner's own far edge. The ref's own equivalent never sees
this: it :Hide()s unused objective strings entirely (pin §3, QuestLogFrame.lua:
400-402) — Show/Hide aside — and presumably never lets an anchored-through
FontString sit at a true zero measured height. This engine's region model has no
Show/Hide for a bare FontString (this file's own recurring comment on
`region.rs`), so hiding isn't an option; a fixed nonzero height is the narrowest
fix that keeps the WHOLE ref chain verbatim. Blank slots now occupy a small
fixed 12px instead of true 0 — an accepted, minor v1 footprint difference from
the ref, not a fidelity claim (and exactly why `BenillaQuestLogDetail_ResizeChild`
sums this fixed 12px per row rather than measuring these ten). -->
<FontString name="QuestLogObjective1" inherits="QuestFontNormalSmall" justifyH="LEFT">
<Size><AbsDimension x="285" y="12"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjectivesText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="QuestLogObjective2" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestLogObjective3" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestLogObjective4" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestLogObjective5" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestLogObjective6" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestLogObjective7" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestLogObjective8" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestLogObjective9" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective8" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestLogObjective10" inherits="QuestFontNormalSmall" justifyH="LEFT"><Size><AbsDimension x="285" y="12"/></Size><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjective9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors></FontString>
<!-- Verbatim ref chain continues (QuestLogFrame.xml:785-833), EXCEPT
`QuestLogDescriptionTitle` itself: the ref does NOT anchor its
`QuestLogDescriptionTitle` to a static `QuestLogObjective10` — it re-`SetPoint`s
it at RUNTIME in `QuestLog_UpdateQuestDetails` off the LAST *actually used*
objective slot (`QuestLogFrame.lua:414-424`: `QuestLogObjective`..numObjectives,
or `QuestLogObjectivesText` when numObjectives is 0 — there's also a
RequiredMoney branch we don't model, this file's documented v1 gap).
`BenillaQuestLogDetail_Update` ports the ref's runtime SetPoint; the anchor
below is a load-bearing-free PLACEHOLDER (immediately overwritten whenever a
selection exists) — same convention the reward/choice Buttons use. Height
stays EXPLICIT 18 (QuestTitleFont, `Fonts.xml` — matching `RewardTitleText`'s
own explicit 18 below, which the ref ITSELF gives a nonzero height): the same
zero-height-owner-fallback rationale as the Objective slots above applies
whenever the log has zero rewards+choices+money (`RewardTitleText` blanks to ""
too) — this header must resolve a real span regardless of its own text (and is
why `BenillaQuestLogDetail_ResizeChild` sums a fixed 18 for it too, never
`GetHeight()`). -->
<FontString name="QuestLogDescriptionTitle" inherits="QuestTitleFont" justifyH="LEFT">
<Size><AbsDimension x="285" y="18"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogObjectivesText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</FontString>
<FontString name="QuestLogDescription" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="270" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogDescriptionTitle" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor></Anchors>
</FontString>
<!-- No `text=`/`hidden=` — `BenillaQuestLogRewards_Update` owns this text entirely
(toggles "Rewards" / "" — this file's blank-text convention). -->
<FontString name="QuestLogRewardTitleText" inherits="QuestTitleFont" justifyH="LEFT">
<Size><AbsDimension x="300" y="18"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogDescription" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-15"/></Offset></Anchor></Anchors>
</FontString>
<!-- REWARD_CHOICES (ref QuestLogFrame.xml:821-832) — our literal transcription of the
token's string. Lua-managed (`BenillaQuestLogRewards_Update` toggles the text). -->
<FontString name="QuestLogItemChooseText" inherits="QuestFont" justifyH="LEFT">
<Size><AbsDimension x="295" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogRewardTitleText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor></Anchors>
</FontString>
<!-- QuestLogItemReceiveText (ref :833-837) — unlike the ref's own bare `<Anchor
point="TOPLEFT"/>` (it's entirely SetPoint-driven at runtime, QuestFrame.lua:
460/467/470), `BenillaQuestLogRewards_Update` re-SetPoints this one too — see that
function. No static `relativeTo` here is load-bearing, only a safe placeholder. -->
<FontString name="QuestLogItemReceiveText" inherits="QuestFont" justifyH="LEFT">
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogRewardTitleText" relativePoint="BOTTOMLEFT"/></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- Choice + fixed reward rows (pin §3/§6). Static XML anchors below are placeholders
ONLY (bare TOPLEFT-of-parent-ish, never read while hidden) —
`BenillaQuestLogRewards_Update` SetPoints every shown row every repaint, porting
`QuestFrameItems_Update`'s own SetPoint math verbatim (QuestFrame.lua:311-522,
cited per-line in that function). -->
<Button name="QuestLogChoice1" inherits="BenillaQuestLogItemTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogItemChooseText" relativePoint="BOTTOMLEFT"/></Anchors>
</Button>
<Button name="QuestLogChoice2" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogChoice1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogChoice3" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogChoice2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogChoice4" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogChoice3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogChoice5" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogChoice4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogChoice6" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogChoice5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogReward1" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogItemReceiveText" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogReward2" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogReward1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogReward3" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogReward2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogReward4" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogReward3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogReward5" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogReward4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="QuestLogReward6" inherits="BenillaQuestLogItemTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogReward5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- Reward money (pin §3, GetQuestLogRewardMoney) — ref QuestLogMoneyFrame anchors LEFT
to QuestLogItemReceiveText's RIGHT, offset (15,0) (QuestLogFrame.xml:896-903). -->
<Frame name="QuestLogMoneyCoin1">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="QuestLogItemReceiveText" relativePoint="RIGHT"><Offset><AbsDimension x="15" y="0"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
<Frame name="QuestLogMoneyCoin2">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="QuestLogMoneyCoin1" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
<Frame name="QuestLogMoneyCoin3">
<Size><AbsDimension x="13" y="13"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="QuestLogMoneyCoin2" relativePoint="RIGHT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="$parentIcon" file="Interface\MoneyFrame\UI-MoneyIcons"><Size><AbsDimension x="13" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"/></Anchors></Texture>
<FontString name="$parentNum" inherits="NumberFontNormal" justifyH="RIGHT"><Size><AbsDimension x="20" y="13"/></Size><Anchors><Anchor point="RIGHT" relativePoint="RIGHT"><Offset><AbsDimension x="-13" y="0"/></Offset></Anchor></Anchors></FontString>
</Layer></Layers>
</Frame>
</Frames>
</Frame>
<!-- A minimal ref-shaped scrollbar (ref UIPanelScrollFrameTemplate/
UIPanelScrollBarTemplate, UIPanelTemplates.xml): up/down step buttons + a
thumb texture positioned from GetVerticalScroll/GetVerticalScrollRange. VISUAL
only — no drag: this engine has no Slider widget (no drag-capture affordance
anywhere yet, the same class of gap this file's header comment already notes
for modifier keys), so the ref's real `Slider` track is a named DEFERRED
follow-up, not modeled here; the wheel + the two arrow buttons are the only
interactive paths to scroll, which is already the ENTIRE ask (the ref's own
mouse-drag-the-thumb affordance is the one piece missing). Hidden whenever
nothing overflows (`BenillaQuestLogDetailScroll_SyncBar`). -->
<Frame name="QuestLogDetailScrollBar" hidden="true">
<Size><AbsDimension x="16" y="261"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogDetailScroll" relativePoint="TOPRIGHT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor></Anchors>
<Frames>
<Button name="QuestLogDetailScrollUp">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<NormalTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Up"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Down"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Disabled"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-ScrollBar-ScrollUpButton-Highlight" alphaMode="ADD"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></HighlightTexture>
<Scripts><OnClick>BenillaQuestLogDetailScroll_Step(-BENILLA_QUESTLOG_SCROLL_STEP)</OnClick></Scripts>
</Button>
<Button name="QuestLogDetailScrollDown">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"/></Anchors>
<NormalTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Up"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Down"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></PushedTexture>
<DisabledTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Disabled"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></DisabledTexture>
<HighlightTexture file="Interface\Buttons\UI-ScrollBar-ScrollDownButton-Highlight" alphaMode="ADD"><TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/></HighlightTexture>
<Scripts><OnClick>BenillaQuestLogDetailScroll_Step(BENILLA_QUESTLOG_SCROLL_STEP)</OnClick></Scripts>
</Button>
<!-- The track: BENILLA_QUESTLOG_SCROLL_TRACK_H (229) tall, between the two
arrow buttons (261 - 16 - 16). The thumb texture is a plain child
region SetPoint'd by BenillaQuestLogDetailScroll_SyncBar off the
CURRENT GetVerticalScroll()/GetVerticalScrollRange() fraction. -->
<Frame name="QuestLogDetailScrollTrack">
<Size><AbsDimension x="16" y="229"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="QuestLogDetailScrollUp" relativePoint="BOTTOMLEFT"/></Anchors>
<Layers><Layer level="OVERLAY">
<Texture name="QuestLogDetailScrollThumb" file="Interface\Buttons\UI-ScrollBar-Knob">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<!-- The ref thumb samples the knob art's CENTER quarter
(UIPanelScrollBarButton's TexCoords, UIPanelTemplates.xml
:104-106 + :160-164) — full UVs squash the 32px canvas
into 16px and the knob reads half-size (director's
"scroll cube is too small"). -->
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
</Texture>
</Layer></Layers>
</Frame>
</Frames>
</Frame>
</Frames>
<Scripts>
<OnLoad>self:SetScrollChild(getglobal("QuestLogDetailChild"))</OnLoad>
<OnMouseWheel>
if arg1 > 0 then
BenillaQuestLogDetailScroll_Step(-BENILLA_QUESTLOG_SCROLL_STEP)
else
BenillaQuestLogDetailScroll_Step(BENILLA_QUESTLOG_SCROLL_STEP)
end
</OnMouseWheel>
<OnVerticalScroll>BenillaQuestLogDetailScroll_SyncBar()</OnVerticalScroll>
<OnScrollRangeChanged>BenillaQuestLogDetailScroll_SyncBar()</OnScrollRangeChanged>
<!-- BenillaQuestLogDetail_OnUpdate's own comment: the FIRST populate of a
freshly-selected quest's text sizes off a one-tick-stale (zero, brand-new
text) GetHeight() — nothing else re-asks once the quest log's data itself
stops changing, so this keeps re-checking every frame the window is open
until the real measurement lands and the resize stops changing (its own
guard makes every further call here a no-op). -->
<OnUpdate>BenillaQuestLogDetail_OnUpdate()</OnUpdate>
</Scripts>
</ScrollFrame>
<!-- pin §4, geometry byte-for-byte (ref QuestLogFrame.xml:451-498): Abandon 125×21 at
(17,54), Exit 77×21 at (-43,54). numEntries==0 disables Abandon. -->
<Button name="QuestLogAbandonButton" inherits="BenillaQuestLogPanelButtonTemplate" text="Abandon Quest">
<Size><AbsDimension x="125" y="21"/></Size>
<Anchors><Anchor point="BOTTOMLEFT" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="17" y="54"/></Offset></Anchor></Anchors>
<Scripts><OnClick>BenillaQuestLogAbandonButton_OnClick()</OnClick></Scripts>
</Button>
<Button name="QuestLogExitButton" inherits="BenillaQuestLogPanelButtonTemplate" text="Exit">
<Size><AbsDimension x="77" y="21"/></Size>
<Anchors><Anchor point="BOTTOMRIGHT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="-43" y="54"/></Offset></Anchor></Anchors>
<Scripts><OnClick>HideUIPanel(getglobal("QuestLogFrame"))</OnClick></Scripts>
</Button>
<!-- Share Quest (ref QuestFramePushQuestButton, QuestLogFrame.xml:499-521): 123×21 flush
LEFT of Exit. Party quest-sharing is a named deferral (no QuestLogPushQuest wire yet),
so this ships permanently DISABLED — exactly how the ref presents it solo (its lua
disables it whenever the party can't receive the quest); the button existing is what
fills the bottom row's middle slot the frame art reserves for it. -->
<Button name="QuestLogPushQuestButton" inherits="BenillaQuestLogPanelButtonTemplate" text="Share Quest">
<Size><AbsDimension x="123" y="21"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="QuestLogExitButton" relativePoint="LEFT"/></Anchors>
<Scripts><OnLoad>self:Disable()</OnLoad></Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>BenillaQuestLogFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaQuestLogFrame_OnEvent(self)</OnEvent>
<!-- The countdown's own repaint (ref QuestLog_OnUpdate, QuestLogFrame.lua:96-105) —
the ONLY thing the ref's window OnUpdate does. Gated on hasTimer, so it costs an
untimed selection nothing, and gated on the window being effectively visible by
`UiScript::tick`'s own OnUpdate gate, so it costs a closed window nothing. -->
<OnUpdate>QuestLog_OnUpdate(arg1)</OnUpdate>
<!-- The list wheel lives on the WINDOW, not just the child catcher: the engine bubbles a
spin up the PARENT chain (script/pointer.rs mouse_wheel), so a spin over a quest-title
row (no OnMouseWheel of its own) must reach this window to scroll — a sibling catcher
never sees it (bubbling reaches ancestors, not siblings). The QuestLogWheelCatcher
button now only gives the list's empty tail a hit-target that also bubbles here. (The
detail ScrollFrame keeps its own OnMouseWheel and is unaffected.) -->
<OnMouseWheel>BenillaQuestLogFrame_OnMouseWheel(arg1)</OnMouseWheel>
<!-- pin §1, QuestLog_OnShow/OnHide (QuestLogFrame.lua:85-95), wired via the frame's
OnShow/OnHide (QuestLogFrame.xml:949-954) — the window-sound convention (decision
0090). Note the SoundKit-name vs on-disk-filename trap the pin calls out (§1): the
kit names below are `igQuestLog*`, the shipped waves are `iQuestLogOpenA`/
`iQuestLogCloseA` (no "g") — PlaySound takes the KIT name, so this is correct as
written, flagged only so a future filename-based lookup doesn't "fix" it wrongly.
Never fires at load (hidden="true"). UpdateMicroButtons leads both, as in the ref
(QuestLogFrame.lua:85-95's own first line) — live since the micro-menu slice
(MicroMenu.xml). -->
<OnShow>if UpdateMicroButtons then UpdateMicroButtons() end PlaySound("igQuestLogOpen") BenillaQuestLogFrame_Update()</OnShow>
<OnHide>if UpdateMicroButtons then UpdateMicroButtons() end PlaySound("igQuestLogClose")</OnHide>
</Scripts>
</Frame>
<!--
The quest-watch tracker HUD (pin's `QuestWatchFrame`, ref `QuestLogFrame.xml:968-1146`) — a
SEPARATE always-on-screen frame, not part of `QuestLogFrame` (this file's "The
always-visible quest-watch tracker" script comment): it's screen-anchored, never docks a
UIPanel slot, has no close button/ESC handling, and repaints off its own events whether or
not the log window is even open — a HUD element, exactly like the ref's. Kept in THIS file
rather than a separate `QuestWatchFrame.xml`: the ref itself defines it in `QuestLogFrame.xml`
too (it's the log's own on-screen echo, driven by the same Lua functions/getters as the log
window above), and this file's tests already load nothing else — a second file would need its
own duplicate test-harness load list for zero isolation benefit (contrast the
`BenillaQuestLogItemTemplate`/`BenillaQuestLogPanelButtonTemplate` duplication above, which
buys real per-window test isolation).
v1 placement: TOPRIGHT of the screen, offset (-10,-170) — no minimap frame exists in benilla
yet (the ref anchors 10px below `MinimapCluster`'s bottom edge), so this is a plain guess at
clearing where one would land (a classic-sized minimap runs roughly 140px tall from the
corner). FOR THE DIRECTOR'S EYE: revisit once a minimap ships, or sooner if this reads wrong
on screen.
280 wide (the ref's own `QuestWatchFrame` width) with NO Backdrop (the ref's tracker floats
bare text over the world, no border/background) — matches `UIErrorsFrame`'s bare-text
convention, not `BenillaQuestLogAbandonConfirm`'s dialog chrome. `BENILLA_QUESTLOG_MAX_WATCHES`
(5, `quest_log.rs`'s own `AddQuestWatch` cap) × `BENILLA_QUESTWATCH_MAX_OBJECTIVES` (4, this
file's v1 cap — see that constant's own comment) pre-declares a FIXED 5×(1+4) grid of
FontStrings — the flat-frame constraint (no dynamic creation at runtime), so every slot exists
up front and `QuestWatch_Update` only ever SetTexts/colors them, never creates one.
-->
<!-- The always-on quest-watch tracker HUD (ref QuestWatchFrame, QuestLogFrame.xml:963-1146
+ QuestWatch_Update, QuestLogFrame.lua:602-663), re-expressed: TOPRIGHT hangs off the
MinimapCluster's BOTTOMRIGHT (+10 up — the cluster art overhangs), an empty 13px
GameFontNormal spacer (the ref's never-written QuestWatchQuestName) seats the first line,
and ONE FLAT POOL of 30 lines (MAX_QUESTWATCH_LINES) carries titles and objectives alike —
the Lua re-anchors each line every paint (titles drop -4 extra; the pool squeezes, no
fixed per-quest slots). Lines are the ref's GameFontHighlight; the XML chain offset is -1
because our lines are measured (y=0 -> ink height 12) where the ref pins 13px boxes:
12 + 1 = the ref pitch. Width/height follow the content (SetWidth maxW+10 / SetHeight
lines*13) so the left-aligned column hugs the minimap's right edge like the ref; the
GetWidth reads are extract-measured (one frame stale), so the sizing tail re-runs from
OnUpdate for two frames after each repaint (`resize_pending`) and converges next frame. -->
<Frame name="QuestWatchFrame">
<Size><AbsDimension x="280" y="20"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="MinimapCluster" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="10"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<FontString name="QuestWatchQuestName" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="0" y="13"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</FontString>
<FontString name="QuestWatchLine1" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchQuestName" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine2" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine3" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine4" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine5" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine4" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine6" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine5" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine7" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine6" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine8" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine7" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine9" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine8" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine10" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine9" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine11" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine10" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine12" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine11" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine13" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine12" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine14" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine13" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine15" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine14" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine16" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine15" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine17" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine16" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine18" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine17" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine19" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine18" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine20" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine19" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine21" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine20" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine22" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine21" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine23" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine22" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine24" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine23" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine25" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine24" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine26" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine25" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine27" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine26" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine28" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine27" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine29" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine28" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
<FontString name="QuestWatchLine30" inherits="GameFontHighlight" justifyH="LEFT"><Anchors><Anchor point="TOPLEFT" relativeTo="QuestWatchLine29" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-1"/></Offset></Anchor></Anchors></FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>BenillaQuestWatchFrame_OnLoad(self)</OnLoad>
<OnEvent>BenillaQuestWatchFrame_OnEvent(self)</OnEvent>
<OnUpdate>BenillaQuestWatchFrame_OnUpdate(self, arg1)</OnUpdate>
</Scripts>
</Frame>
</Ui>