Files
benilla-octo/crates/benilla-app/assets/ui/ScrollTemplates.xml
T
SamandClaude Fable 5.1 a8c9bc37a2 the audio crackle is gone, and a trainer answers a click as a trainer
- the audio crackle was our own stall sampler: `/usr/bin/sample` suspends every thread of the task for each of its snapshots, CoreAudio's realtime IO thread included, and coreaudiod times the client out — every HAL overload of the session sat inside a sample of our own process (three for three by timestamp), and the watchdog fires on exactly the stalls the crackle lands on (a desktop switch's `nextDrawable` block, a world entry's compile burst); in-frame sampling now waits for a run nobody can hear; and the output stream is ours — kira's renderer runs 256-frame chunks on a realtime render thread (time-constraint policy, joined to the device's IO workgroup) into a lock-free ring, the IOProc copies one device buffer and touches nothing else (≤ 0.01 ms), the device buffer goes 2048 → 512, a lost or changed default device rebuilds the stream (cpal's no-op error callback on macOS had paused it forever), a debug allocator aborts on any allocation inside either realtime path, and per-cycle meters name a skipped cycle's layer; `SoundBufferSize` is registered at the reference's own FMOD dial, and `audiowatch.py` puts the OS side — thread bands, coreaudiod's overload verdicts — on the same timeline
- right-clicking a trainer, questgiver, innkeeper, spirit healer, petitioner, tabard designer or battlemaster sent `CMSG_GOSSIP_HELLO` — eight of the reference's fourteen service arms collapsing into one packet, because the dispatch keyed on the CURSOR kind and Speak covers eight bits; the reference runs two structurally identical ladders over `UNIT_NPC_FLAGS` (one for the cursor at `0x482336`, one for the send at `0x5f0289`), so the ladder is transcribed, first match wins low to high: questgiver → `CMSG_QUESTGIVER_HELLO`, trainer → `CMSG_TRAINER_LIST`, petitioner → `CMSG_PETITION_SHOWLIST`, and the spirit healer and innkeeper send NOTHING on the click — they cache the guid and raise the confirm, whose Lua accept sends with a zero guid — both ghost-gated at entry, so a living player clicking a menuless spirit healer now gets exactly what the reference gives: nothing; the stable-master conjunct and the banker/auctioneer fork fall out of the bit order for free
- the -1 blood id is unfilled, not none, and the tests now name the creatures: -1 splits families that share a directory (QuillBoar beside QuillBoarWarrior, GnollMelee beside GnollCaster) and splits single creatures shipped under two rows (a Baby Murloc is both), so a "bloodless" reading would put non-bleeding quilboar beside bleeding quilboar warriors in one Razorfen room; `violenceLevel` 2 IS the reference's default — formatted out of a per-region cap table at `0x86c3f8`, and the one capped locale is koKR, whose green-blood row the table predicts from bytes alone; a spurt whose victim authors no attachment point is dropped, not relocated, and no creature-keyed suppression exists anywhere on the spurt path (ten stop conditions, none keyed on the creature)
- twenty dead copies of ours (`PanelTemplates_*`, `TabButtonTemplate`, `FauxScrollFrame_*`, `ScrollFrame_OnLoad`, `ScrollingEdit_*`) that a later chain entry had been shadowing are deleted under a gate that fails any such shadow, and deleting them turned 141 tests red on four bugs live since the kit swap: lists populated before showing (the reference gates every scroll-bar write on `IsVisible()`, so both arrows were dead on every faux list), every faux window wired to a `frame.updateFunc` the reference does not have (the bar moved, the list did not follow), a trainer pitch declared `local` where an `<OnVerticalScroll>` chunk cannot see it, and the armed-wheel guard bypassed by the template's own `<OnMouseWheel>`; nine faux windows (Skill, Craft, TradeSkill, Auction, QuestLog, Friends, ScriptLog) are still on `updateFunc` and their lists do not scroll until each is rewired

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 17:09:38 +07:00

404 lines
27 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 reusable scroll kits — TWO of them, sharing one scrollbar, one pair of arrow
helpers and one optional TROUGH (`BenillaScrollTroughTemplate`, at the foot: the recessed
channel a bar rides in — 1.12 authors that art per window, so an owner declares one beside
its bar and the kit shows and hides the two together).
The **faux-scroll** kit (this header) re-binds a fixed set of rows to a moving data
offset; the **real-scroll** kit (second half of the script block) pans and CLIPS a
variable-height child through the engine's `<ScrollFrame>` (decision 0112) — the one a gossip
menu of wrapped options needs, since its content height isn't known until it is laid out.
Only their OnValueChanged differs, and an instance overrides the template's (the loader
SetScripts every `<Scripts>` block in order, so the instance's handler wins).
The faux kit (decisions 0247/0250/0251): a draggable scroll bar over
the `Slider` primitive (0250) plus the fixed-row "faux scroll" Lua layer — the reference's
`FauxScrollFrame`, which never really scrolls a child: it re-binds a fixed set of row buttons to
a moving data offset. First customer: the class trainer's skill-line list (TrainerFrame.xml).
The quest log's chunk-local faux-scroll upvalue is a later migration onto this.
DESIGN — collapsed from the real client's three-part decomposition (0250: FauxScrollFrame =
ScrollFrame + Slider + Lua). The real `FauxScrollFrameTemplate` routes the scrollbar's value
through its parent `ScrollFrame` (`OnValueChanged → parent:SetVerticalScroll(v)`; the
ScrollFrame's `OnVerticalScroll` then derives `offset = floor(v/itemHeight + 0.5)`). That
ScrollFrame adds exactly one thing over the Slider: a *scroll-range clamp* on the value. We
don't need it — the offset comes straight off the Slider value here — and in benilla it would be
a liability: the clamp reads the scroll child's RESOLVED height, which lags a tick behind the
`SetHeight` the faux update does, so a content resize + scroll in one tick could clamp against a
stale range. So benilla's faux frame is a plain `<Frame>` + a `Slider`: fewer moving parts, no
dependency on within-tick resolved rects, and the identical on-screen result (a smooth,
pixel-granularity thumb whose position maps 1:1 to the scroll offset). See 0251 for the full
rationale + the divergence from 0250's described (real-client) shape.
VALUE MODEL. The scrollbar's value is in PIXELS: `[0, (numItems − numToDisplay) * itemHeight]`,
`valueStep = itemHeight`. The row offset is `floor(value / itemHeight + 0.5)` — sub-pixel
rounding, so the thumb (drawn at the raw value fraction) and the list (drawn at the rounded
offset) never visibly disagree. The frame carries `frame.offset` (the top visible row, 0-based)
and `frame.updateFunc` (the owner's list-repaint, set by the owner's OnLoad).
-->
<Script><![CDATA[
-- Step the bar one row (the slider's valueStep) — the sensible step for an item list (the
-- generic ref scrollbar steps half its height; a discrete row list wants one). Silent by
-- design: the ref plays UChatScrollButton only on the arrow *buttons'* OnClick, never on the
-- wheel (ScrollFrameTemplate_OnMouseWheel is soundless). Both the wheel and the arrows call
-- this, so the click lives on the arrow OnClick, keeping wheel-scroll silent.
function BenillaScrollBar_Step(scrollBar, dir)
local step = scrollBar:GetValueStep()
if step <= 0 then step = 1 end
scrollBar:SetValue(scrollBar:GetValue() + dir * step)
end
-- Grey the up arrow at the top and the down arrow at the bottom (ref
-- ScrollFrame_OnScrollRangeChanged's arrow handling), from the slider's own value/range.
function BenillaScrollBar_UpdateArrows(scrollBar)
local minV, maxV = scrollBar:GetMinMaxValues()
local val = scrollBar:GetValue()
local up = getglobal(scrollBar:GetName() .. "ScrollUpButton")
local down = getglobal(scrollBar:GetName() .. "ScrollDownButton")
if val <= minV then up:Disable() else up:Enable() end
if val >= maxV then down:Disable() else down:Enable() end
end
-- The scrollbar's OnValueChanged: derive the row offset from the (pixel) value and, only when
-- it actually moved, repaint the owner's list. The change-gate is load-bearing: the owner's
-- repaint calls _Update below, which SetValue()s the thumb back to offset*itemHeight — a value
-- that maps to the SAME offset, so this handler no-ops and the cycle stops after one hop (the
-- Slider's own SetValue change-gate stops the value side, decision 0250).
--
-- THE SECOND HALF IS FOR ADDONS, and it is the reference's own wiring. The ref's
-- `UIPanelScrollBarTemplate` routes its value through the ScrollFrame it rides
-- (`<OnValueChanged>this:GetParent():SetVerticalScroll(arg1)</OnValueChanged>`,
-- ref-UIPanelTemplates.xml l.156-158), and that frame's `<OnVerticalScroll>` is where an
-- addon puts `FauxScrollFrame_OnVerticalScroll(itemHeight, MyUpdate)`. Our OWN faux frames
-- are plain `<Frame>`s (the 0251 divergence in the header above), so they have no
-- SetVerticalScroll and this line never fires for them — but an addon's is declared
-- `<ScrollFrame ... inherits="FauxScrollFrameTemplate">`, and the instance's own tag wins
-- over the template's on inherit (framexml.rs `merge`), so it really is one. Duck-typed for
-- exactly that reason.
--
-- GAP, stated where it bites: `SetVerticalScroll` clamps into `[0, GetVerticalScrollRange()]`
-- (scrollframe.rs), and that range comes from the frame's SCROLL CHILD — which the ref's
-- `FauxScrollFrameTemplate` declares as `<ScrollChild><Frame name="$parentScrollChildFrame">`
-- and our XML loader has no `<ScrollChild>` element for. Until it does, an addon frame's
-- range reads 0, so the handler fires with arg1 = 0 and its list stays at the top. The
-- offset our own kit computes above is unaffected — it comes straight off the Slider, which
-- is the whole point of 0251.
function BenillaFauxScrollBar_OnValueChanged(scrollBar, value)
local frame = scrollBar:GetParent()
local step = scrollBar:GetValueStep()
if step <= 0 then step = 1 end
local newOffset = floor(value / step + 0.5)
if newOffset ~= frame.offset then
frame.offset = newOffset
if frame.updateFunc then frame.updateFunc() end
end
BenillaScrollBar_UpdateArrows(scrollBar)
if frame.SetVerticalScroll then frame:SetVerticalScroll(value) end
end
-- Seat a trough ON ITS BAR. THE SOCKETS, and why this is not a free choice: the channel art
-- is not a plain tube — each end carries a 16-tall SOCKET that the arrow button drops into,
-- bounded by the frame's outer cap and an inner crossbar. Measured texel-exact off
-- UI-Character-ScrollBar (64x256):
-- top piece (left column) : rows 0-3 CAP · rows 4-19 SOCKET · rows 20-21 crossbar
-- bottom piece (right column): rows 85-86 crossbar · rows 87-102 SOCKET · rows 104-106 CAP
-- Both pieces draw 1:1 anchored to their own end of the trough, and the arrow buttons sit 16
-- outside the slider's ends — so a socket lands on its arrow only when the trough OVERHANGS
-- the bar by 21 above and 20 below (4 rows of cap at the top against 3+1 at the bottom; that
-- 1 is the whole asymmetry). Those are the ref's own numbers, reached from the other side:
-- ReputationFrame hangs its trough at the scroll frame's TOPRIGHT (-2,+5) / BOTTOMRIGHT
-- (-2,-4) against a bar seated (6,-16)/(6,+16) — 21, 20, and the 8-left hang
-- (ref-ReputationFrame.xml l.573-599 against ref-UIPanelTemplates.xml l.166-181). Miss it by
-- even 4 and the arrows ride OUT of their sockets onto the caps with a sliver of bare socket
-- showing beside them, which is exactly what B224 reported.
--
-- Seated HERE, off the bar, rather than restated in each owner's XML: it is one law, and an
-- owner made to re-derive it in offsets is an owner that can get it wrong — which is how
-- B224 happened. So a trough carries NO anchors of its own; declare it before the bar (so it
-- draws beneath) and name it `<barname>Trough`, and that is the whole contract. Lazy,
-- because the bar is declared after the trough and does not exist at the trough's OnLoad.
function BenillaScrollTrough_Seat(trough, scrollBar)
if trough.seated then return end
trough.seated = true
trough:ClearAllPoints()
trough:SetPoint("TOPLEFT", scrollBar, "TOPLEFT", -8, 21)
trough:SetPoint("BOTTOMRIGHT", scrollBar, "BOTTOMRIGHT", 7, -20)
end
-- Show/hide a bar TOGETHER WITH ITS TROUGH — the recessed channel it rides in, if the
-- owner authored one beside it (a `<barname>Trough` frame on BenillaScrollTroughTemplate
-- below). Every path that shows or hides a bar goes through here, so a window that fits
-- shows neither and one that overflows shows both — and so a trough is seated on its bar
-- before it can ever be visible, whichever way the first call goes.
--
-- Why the trough is OPT-IN rather than part of the bar template: 1.12 authors trough art
-- PER WINDOW, never in the shared scroll kit — `UIPanelScrollBarTemplate` carries only the
-- two arrow buttons and the knob, and the ref's FrameXML then hangs loose trough textures
-- beside 29 individual bars, in two different skins (UI-Character-ScrollBar for the
-- dialog/paperdoll family, UI-ClassTrainer-ScrollBar for the trainer family). The era's own
-- MinimalScrollBar went the other way and made its Track part of the bar; this pair keeps
-- the era's COUPLING (track lives and dies with the bar) on 1.12's per-window SKIN.
function BenillaScrollBar_SetShown(scrollBar, shown)
local trough = getglobal(scrollBar:GetName() .. "Trough")
if trough then BenillaScrollTrough_Seat(trough, scrollBar) end
if shown then
scrollBar:Show()
if trough then trough:Show() end
else
scrollBar:Hide()
if trough then trough:Hide() end
end
end
-- **Our trough, following the REFERENCE's bar.** `BenillaScrollBar_SetShown` above is the
-- Benilla kit's own pairing and nothing in the reference's faux kit calls it — the
-- reference has no trough at all, which is exactly why this template stays ours (1837's
-- manifest note). Since the faux frames took the chain's `FauxScrollFrameTemplate` their
-- bar is `UIPanelScrollBarTemplate`, so an owner that wants its trough asks for it here,
-- right after its `FauxScrollFrame_Update`. Seating is idempotent and the visibility
-- simply mirrors the bar's, which the reference's `_Update` has already decided by
-- showing or hiding the scroll FRAME.
--
-- Decision 1860. Named after the bar rather than the frame because the trough is declared
-- `<barname>Trough` — one law, stated once, exactly as BenillaScrollTrough_Seat wants it.
function BenillaScrollTrough_Follow(scrollFrameName)
local sf = getglobal(scrollFrameName)
if not sf then return end
local bar = getglobal(scrollFrameName .. "ScrollBar")
if not bar then return end
local trough = getglobal(bar:GetName() .. "Trough")
if not trough then return end
BenillaScrollTrough_Seat(trough, bar)
if sf:IsShown() then trough:Show() else trough:Hide() end
end
-- A wheel spin over the list → one row (WoW convention: +1 up, −1 down). Called from the
-- owner window's OnMouseWheel (the engine bubbles the spin up the parent chain, so the handler
-- lives on the window, not a child — TrainerFrame.xml's own note).
--
-- KEEPS the `Benilla` prefix on purpose: the reference has no faux-scroll wheel function to
-- take the name of. Its `ScrollFrameTemplate_OnMouseWheel` is the REAL-scroll one and moves
-- by half the bar's height; its faux owners each write a wheel body of their own inline.
function BenillaFauxScrollFrame_OnMouseWheel(frame, delta)
local scrollBar = getglobal(frame:GetName() .. "ScrollBar")
BenillaScrollBar_Step(scrollBar, -delta)
end
-- ══════════════════════════ The REAL-scroll kit ══════════════════════════════════════════
--
-- The other half of the file: a true `<ScrollFrame>` (engine mechanism, decision 0112) that
-- pans and CLIPS a variable-height child, as against the faux kit above (fixed rows re-bound
-- to a data offset). Windows whose content height is unknown until it is laid out — a gossip
-- menu of wrapped options, a quest's body text — need this one; a uniform list wants the faux.
--
-- SHAPE, from the reference `UIPanelScrollFrameTemplate` (Interface\FrameXML\
-- UIPanelTemplates.xml l.166-211, extracted via mpqx): the scrollbar is a SLIDER that rides
-- its ScrollFrame as a child, so `bar:GetParent()` IS the frame it scrolls — no name
-- derivation, and `$parentBar` gives SyncBar its handle. The ref's OnVerticalScroll drives
-- the bar's value and greys the two arrows at the ends; ours does the same through the
-- shared arrow helper above.
BENILLA_SCROLL_STEP = 20 -- px per arrow click / wheel notch
BENILLA_SCROLL_PAD = 20 -- breathing room under the last element in the child
-- The bar moved (drag, arrow, or SyncBar): pan the frame it rides. The loop this could form
-- (bar → scroll → OnVerticalScroll → SyncBar → bar) dead-ends in one hop — SyncBar writes the
-- value the bar already holds, and the Slider's SetValue change-gate (decision 0250) drops it.
function BenillaScrollBar_OnValueChanged(bar, value)
bar:GetParent():SetVerticalScroll(value)
BenillaScrollBar_UpdateArrows(bar)
end
-- Reconcile a ScrollFrame's bar to its live range (ref ScrollFrame_OnScrollRangeChanged):
-- the bar spans [0, range] in pixels, seats at the current scroll, and hides entirely when
-- the content fits. Wired to both OnScrollRangeChanged and OnVerticalScroll by the owner.
function BenillaScroll_SyncBar(sf)
local bar = getglobal(sf:GetName() .. "Bar")
if not bar then return end
local range = sf:GetVerticalScrollRange()
bar:SetValueStep(BENILLA_SCROLL_STEP)
bar:SetMinMaxValues(0, range)
bar:SetValue(sf:GetVerticalScroll())
BenillaScrollBar_UpdateArrows(bar)
BenillaScrollBar_SetShown(bar, range > 0)
end
-- Pan by `delta` px; the engine clamps to [0, range].
function BenillaScroll_Step(sf, delta)
sf:SetVerticalScroll(sf:GetVerticalScroll() + delta)
BenillaScroll_SyncBar(sf)
end
-- WoW wheel convention: +1 per notch up, -1 down — up lifts the content.
function BenillaScroll_OnMouseWheel(sf, delta)
BenillaScroll_Step(sf, -delta * BENILLA_SCROLL_STEP)
end
-- A widget's resolved bottom edge for the content sum — nil when it isn't contributing
-- (hidden frame / blank FontString / not yet resolved this frame).
function BenillaScroll_WidgetBottom(w)
if not w then return nil end
if w.IsShown then
if not w:IsShown() then return nil end
elseif w.GetText then
local t = w:GetText()
if not t or t == "" then return nil end
end
return w:GetBottom()
end
-- Size a scroll child to its MEASURED content — the resolved bottom of the lowest visible
-- widget, plus padding — instead of a fixed budget, and tell the frame (which fires
-- OnScrollRangeChanged → the owner's SyncBar). Frame-late by nature: rects resolve after Lua
-- runs, so owners re-run this from an OnUpdate until it stops changing. Returns true when it
-- actually resized (only a real change writes: SetHeight invalidates layout, and an
-- every-frame write would fight the resolver).
function BenillaScroll_ResizeChild(scrollName, widgets)
local sf = getglobal(scrollName)
-- ASK THE FRAME, don't guess a name. This used to be
-- `getglobal(scrollName .. "Child")`, which only ever worked because every caller wore
-- a private name in that exact shape — the reference's own convention is
-- `<X>ScrollFrame` / `<X>ScrollChildFrame`, not a suffix, so the derivation broke the
-- moment QuestFrame took the reference's names. A ScrollFrame knows its own child.
local child = sf:GetScrollChild()
if not child then return false end
local top = child:GetTop()
if not top then return false end
local bottom = nil
for i = 1, table.getn(widgets) do
local b = BenillaScroll_WidgetBottom(getglobal(widgets[i]))
if b and (not bottom or b < bottom) then bottom = b end
end
local height = 1
if bottom then height = (top - bottom) + BENILLA_SCROLL_PAD end
if height < 1 then height = 1 end
if math.abs(child:GetHeight() - height) < 1 then return false end
child:SetHeight(height)
sf:UpdateScrollChildRect()
return true
end
]]></Script>
<!--
The scroll bar: a `Slider` (0250) 16px wide, its up/down step buttons pinned above/below the
track, and the knob thumb. The value is in pixels (see the header); the thumb draws at the
value fraction along the track (engine-side, decision 0250 §4), draggable (§5). The step-button
+ knob art is the stock 1.12 scrollbar kit, each texture cropped to its centred 16px (the
`0.25..0.75` TexCoords the ref's `UIPanelScrollBarButton` applies).
-->
<Slider name="BenillaScrollBarTemplate" virtual="true">
<Size><AbsDimension x="16" y="0"/></Size>
<Frames>
<Button name="$parentScrollUpButton">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="BOTTOM" relativePoint="TOP"/></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>BenillaScrollBar_Step(this:GetParent(), -1); PlaySound("UChatScrollButton")</OnClick></Scripts>
</Button>
<Button name="$parentScrollDownButton">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOP" relativePoint="BOTTOM"/></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>BenillaScrollBar_Step(this:GetParent(), 1); PlaySound("UChatScrollButton")</OnClick></Scripts>
</Button>
</Frames>
<Scripts>
<OnValueChanged>BenillaFauxScrollBar_OnValueChanged(self, arg1)</OnValueChanged>
</Scripts>
<ThumbTexture name="$parentThumbTexture" file="Interface\Buttons\UI-ScrollBar-Knob">
<Size><AbsDimension x="16" y="16"/></Size>
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
</ThumbTexture>
</Slider>
<!--
The scroll bar's TROUGH — the recessed channel the bar rides in: the "background and
border" a bare arrows+knob bar is missing. An owner declares one of these BESIDE its bar,
named `<barname>Trough` and WITH NO ANCHORS — it seats itself on the bar the first time
BenillaScrollBar_SetShown touches it (BenillaScrollTrough_Seat above, where the socket law
and the ref citations live). Declare it BEFORE the bar so it draws beneath.
THE ART, and why it is sliced this way. `Interface\PaperDollInfoFrame\UI-Character-ScrollBar`
is a 64x256 sheet holding TWO 31-wide channel pieces, measured texel-exact from the file —
rails at x 2-5 and 25-30 with a 19-wide interior between them, and each piece bearing a
16-tall SOCKET for its arrow button:
· LEFT column (u 0 .. 31/64): rows 0-3 the solid TOP cap, 4-19 the up-arrow socket,
20-21 the crossbar that closes it, then plain channel from row 22 to the bottom edge.
· RIGHT column (u 33/64 .. 1): plain channel, then rows 85-86 the crossbar, 87-102 the
down-arrow socket, 104-106 the solid BOTTOM cap (nothing below 107).
The ref composes a trough from exactly two quads — the whole left column plus the right
column's bottom 108 rows — which reaches 362 units and no further, and only works because
its scroll frames are short enough for the two to OVERLAP. Ours must span a 563-tall page,
so the same art is cut as a THREE-SLICE instead: the two socket ends at their natural
heights and a plain-channel slice stretched between them. The middle is constant down its
whole length, so stretching it is exact — there is no seam to find.
The black `Fill` is the ref's own trough backing, not an invention: ItemTextFrame draws a
0.8-alpha black quad behind its channel art for exactly this (ref-ItemTextFrame.xml's
`Middle`). It sits in BACKGROUND and the channel art in BORDER — two layers, because within
ONE layer the client re-sorts overlapping quads by texture handle and their order is not
ours to depend on (order.rs). The bar's own furniture stays clear above both: button state
textures are ARTWORK, the knob is OVERLAY.
-->
<Frame name="BenillaScrollTroughTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="31"/></Size>
<Layers>
<Layer level="BACKGROUND">
<!-- The channel interior: x 6..24 of the 31-wide piece, i.e. inset 6 from each edge. -->
<Texture name="$parentFill">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="6" y="0"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor>
</Anchors>
<Color r="0" g="0" b="0" a="0.8"/>
</Texture>
</Layer>
<Layer level="BORDER">
<!-- Cap + up-arrow socket + crossbar: left column, rows 0..22 (v 0 .. 23/256). -->
<Texture name="$parentTop" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size><AbsDimension y="23"/></Size>
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="TOPRIGHT"/>
</Anchors>
<TexCoords left="0" right="0.484375" top="0" bottom="0.08984375"/>
</Texture>
<!-- Crossbar + down-arrow socket + cap: right column, rows 82..107
(v 82/256 .. 108/256) — the mirror of the top's 23, cut to put row 107 on the
trough's bottom edge exactly as ReputationFrame's own 108/0.421875 does, which
is what lands the socket on the arrow (BenillaScrollTrough_Seat's law). -->
<Texture name="$parentBottom" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Size><AbsDimension y="26"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<TexCoords left="0.515625" right="1.0" top="0.3203125" bottom="0.421875"/>
</Texture>
<!-- The stretched run between them: left column rows 30..70, plain rails only.
Declared LAST because it anchors to the two above by name, and an anchor whose
target is declared later falls back to the parent (ItemTextFrame's own note). -->
<Texture name="$parentMiddle" file="Interface\PaperDollInfoFrame\UI-Character-ScrollBar">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTop" relativePoint="BOTTOMLEFT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBottom" relativePoint="TOPRIGHT"/>
</Anchors>
<TexCoords left="0" right="0.484375" top="0.1171875" bottom="0.2734375"/>
</Texture>
</Layer>
</Layers>
</Frame>
<!-- `FauxScrollFrameTemplate` and the `FauxScrollFrame_*`/`ScrollFrame_*`/`ScrollingEdit_*`
API are the REFERENCE's, off the player's chain (`Interface\FrameXML\UIPanelTemplates`,
seated below this file — so its declarations win). Our transcriptions stood here dead
from the day that entry landed and are gone (1860). What stays is
`BenillaScrollBarTemplate` + `BenillaScrollTroughTemplate` and their helpers, which are
ours: six windows inherit the bar directly and the reference has no trough at all. -->
</Ui>