The Benilla scroll bar template scrolls its parent itself (#1730)

`BenillaScrollBarTemplate`'s own OnValueChanged was a faux-list handler that both instances replaced with the real scroll, so a new bar that forgot the override wrote `offset`/`updateFunc` on its parent instead of scrolling. The reference's `UIPanelScrollBarTemplate` carries `this:GetParent():SetVerticalScroll(arg1)` itself (`UIPanelTemplates.xml:156-158`).

The template now calls `BenillaScrollBar_OnValueChanged(this, arg1)`; the two instance overrides, the dead faux handler and its header text are gone.

Test: a bare bar from the template scrolls its ScrollFrame parent with no `offset` or `updateFunc` write; it fails with the old template.

Fixes #359

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 142ed718d3a360a5992c78b53540a356cacf6d1e)

# Conflicts:
#	crates/benilla-app/assets/ui/OptionsFrame.xml
#	crates/benilla-app/assets/ui/ScriptLogFrame.xml
#	crates/benilla-app/assets/ui/ScrollTemplates.xml
#	docs/DEVIATIONS.md

Absorbed into the octo fork. Upstream's side taken on all three XML files, and it fixes a live fault in this fork as well: our BenillaScrollBarTemplate called BenillaFauxScrollBar_OnValueChanged, a global defined nowhere in the repo, so every bar built from the template raised on value change — survived only because OptionsFrame and ScriptLogFrame each overrode the handler per instance. The template now carries the real handler and the two overrides are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
Sam Sung
2026-10-25 23:10:12 -06:00
committed by grom
co-authored by Claude Opus 5
parent 132776c197
commit e0895013ed
4 changed files with 866 additions and 2214 deletions
File diff suppressed because it is too large Load Diff
+157 -95
View File
@@ -1,16 +1,20 @@
<Ui>
<!--
The script error log, which 1.12 does not have, so every name carries the `Benilla` prefix
(`tests::reference_surface`); the stock `ScriptErrors` dialog shows one message, this keeps
the log. Shaped like the reference's CraftFrame. It polls the engine:
`BenillaGetNumScriptErrors()` is (retained, total-ever), `BenillaGetScriptErrorInfo(i)` is
`seq, kind, message, count` (1-based, oldest first, nil past the end).
The script error log, which 1.12 does not have, so every name carries the `Benilla` prefix;
the stock `ScriptErrors` dialog shows one message, this keeps the log. Laid out on the
ClassTrainer panel art as the class trainer lays itself out (Blizzard_TrainerUI.xml:214-356
and :481-513, Blizzard_TrainerUI.lua:469-474): its rows, its list, its detail under the
list and its two buttons in the art's sockets. It collects as a 1.12 error-log addon does:
it wraps the handler `geterrorhandler` holds, the stock `_ERRORMESSAGE`
(BasicControls.xml:16), which still runs after it. An addon that later calls
`seterrorhandler` replaces the chain, as in 1.12.
-->
<Script><![CDATA[
-- A pushable above every stock panel's, so a game window opening pushes the log aside.
UIPanelWindows["BenillaScriptLogFrame"] = { area = "left", pushable = 9, whileDead = 1 }
BENILLA_SCRIPTLOG_ROWS = 13
-- The class trainer's rows (`CLASS_TRAINER_SKILLS_DISPLAYED`, Blizzard_TrainerUI.lua:470).
BENILLA_SCRIPTLOG_ROWS = 11
BENILLA_SCRIPTLOG_ROW_HEIGHT = 16
-- A wrapped row would break the faux kit's fixed rows.
BENILLA_SCRIPTLOG_ROW_CHARS = 52
@@ -18,6 +22,39 @@
-- A `seq`, not an index: indices shift when the cap evicts.
BenillaScriptLogSelected = nil
-- Distinct messages kept before the oldest is evicted.
BENILLA_SCRIPTLOG_CAP = 256
-- Rows oldest first, each { seq, message, count }; `total` is the last `seq` handed out,
-- kept apart from the row count so an eviction or a clear never renumbers.
BenillaScriptLog = { rows = {}, total = 0 }
-- A repeat bumps its row's count and keeps its place.
function BenillaScriptLog_Record(message)
if type(message) ~= "string" then message = tostring(message) end
local log = BenillaScriptLog
local found = nil
for i = 1, table.getn(log.rows) do
if log.rows[i].message == message then found = log.rows[i] end
end
if found then
found.count = found.count + 1
else
log.total = log.total + 1
if table.getn(log.rows) >= BENILLA_SCRIPTLOG_CAP then table.remove(log.rows, 1) end
table.insert(log.rows, { seq = log.total, message = message, count = 1 })
end
if BenillaScriptLogFrame and BenillaScriptLogFrame:IsVisible() then
BenillaScriptLog_Update()
end
end
-- The chain: the log first, guarded so a fault in it cannot stop the stock handler.
local benillaStockErrorHandler = geterrorhandler()
seterrorhandler(function(message)
pcall(BenillaScriptLog_Record, message)
return benillaStockErrorHandler(message)
end)
-- `strsub` counts bytes, so the cut backs up over UTF-8 continuation bytes (0x80..0xBF).
function BenillaScriptLog_RowText(message)
local cut = strfind(message, "\n", 1, 1)
@@ -35,18 +72,10 @@
return head
end
-- `load` red (the addon is not running), `error` amber (code raised), `warn` grey (a call
-- was accepted and quietly did nothing).
function BenillaScriptLog_RowColor(kind)
if kind == "load" then return 1.0, 0.3, 0.3 end
if kind == "warn" then return 0.65, 0.65, 0.65 end
return 1.0, 0.82, 0.0
end
function BenillaScriptLog_Update()
local shown, total = BenillaGetNumScriptErrors()
local shown = table.getn(BenillaScriptLog.rows)
local total = BenillaScriptLog.total
-- "Problems", because two of the three kinds are not errors.
local summary
if total == 0 then
summary = "No problems this session."
@@ -68,18 +97,17 @@
for i = 1, BENILLA_SCRIPTLOG_ROWS do
local button = getglobal("BenillaScriptLogRow" .. i)
local index = offset + i
local seq, kind, message, count = BenillaGetScriptErrorInfo(index)
if seq then
button.seq = seq
button.message = message
local row = BenillaScriptLog.rows[index]
if row then
button.seq = row.seq
button.message = row.message
-- `#12 x1113`: one deduplicated failure that happened 1,113 times.
local label = "#" .. seq
if count > 1 then label = label .. " x" .. count end
local label = "#" .. row.seq
if row.count > 1 then label = label .. " x" .. row.count end
button.index:SetText(label)
button.label:SetText(BenillaScriptLog_RowText(message))
button.label:SetTextColor(BenillaScriptLog_RowColor(kind))
button.label:SetText(BenillaScriptLog_RowText(row.message))
button:Show()
if seq == BenillaScriptLogSelected then selectedRow = button end
if row.seq == BenillaScriptLogSelected then selectedRow = button end
else
button.seq = nil
button.message = nil
@@ -99,15 +127,16 @@
BenillaScriptLog_UpdateDetail()
end
-- The selected message in full, traceback and all.
-- The selected message in full, traceback and all, in a box a player can select and copy
-- from. A text that has not changed is left alone: `SetText` collapses the selection, so a
-- new error arriving must not undo one about to be copied. A different text takes the
-- keyboard from the box, as a click on another row does.
function BenillaScriptLog_UpdateDetail()
local text = ""
if BenillaScriptLogSelected then
local shown = BenillaGetNumScriptErrors()
for i = 1, shown do
local seq, _, message = BenillaGetScriptErrorInfo(i)
if seq == BenillaScriptLogSelected then
text = message
for _, row in ipairs(BenillaScriptLog.rows) do
if row.seq == BenillaScriptLogSelected then
text = row.message
break
end
end
@@ -115,12 +144,24 @@
if text == "" then
text = "Select a row above to read it in full."
end
getglobal("BenillaScriptLogDetailText"):SetText(text)
local box = getglobal("BenillaScriptLogDetailText")
if text ~= box.shown then
box.shown = text
box:ClearFocus()
box:SetText(text)
end
end
-- Text rects resolve after Lua runs, so the detail child is re-measured every frame.
function BenillaScriptLogFrame_OnUpdate()
BenillaScroll_ResizeChild("BenillaScriptLogDetailScroll", { "BenillaScriptLogDetailText" })
-- The box is read-only: a player's edit is undone. The restore's own `SetText` raises
-- another change, which finds the text equal and stops; `SetText` of an equal text does
-- nothing. The selection an edit replaced is put back, so Ctrl+C still copies it all.
function BenillaScriptLogDetail_OnTextChanged()
local shown = this.shown or ""
if this:GetText() ~= shown then
this:SetText(shown)
this:HighlightText()
end
ScrollingEdit_OnTextChanged(getglobal("BenillaScriptLogDetailScroll"))
end
function BenillaScriptLogRow_OnClick(button)
@@ -133,7 +174,7 @@
BenillaScriptLog_Update()
end
function BenillaScriptLogFrame_OnLoad(self)
function BenillaScriptLogFrame_OnLoad()
-- Masks the square icon to the portrait ring.
SetPortraitToTexture("BenillaScriptLogFramePortrait",
"Interface\\Icons\\INV_Misc_Note_01")
@@ -157,7 +198,7 @@
-- `/errors clear`: keeps the numbering, so a number never names two failures.
function BenillaScriptLog_Clear()
BenillaClearScriptErrors()
BenillaScriptLog.rows = {}
BenillaScriptLogSelected = nil
if getglobal("BenillaScriptLogFrame"):IsVisible() then
BenillaScriptLog_Update()
@@ -184,15 +225,16 @@
<HighlightTexture file="Interface\Buttons\UI-Listbox-Highlight2" alphaMode="ADD"/>
<Scripts>
<OnLoad>
self.index = getglobal(self:GetName() .. "Index")
self.label = getglobal(self:GetName() .. "Label")
this.index = getglobal(this:GetName() .. "Index")
this.label = getglobal(this:GetName() .. "Label")
</OnLoad>
<OnClick>BenillaScriptLogRow_OnClick(self)</OnClick>
<OnClick>BenillaScriptLogRow_OnClick(this)</OnClick>
</Scripts>
</Button>
<Frame name="BenillaScriptLogFrame" hidden="true">
<!-- The standard left-slot panel size. No <Anchors>: ShowUIPanel seats it. -->
<Frame name="BenillaScriptLogFrame" parent="UIParent" hidden="true">
<!-- The standard left-slot panel size. No <Anchors>: ShowUIPanel seats it. Under UIParent,
as the stock panels are, so it takes the UI scale with them. -->
<Size><AbsDimension x="384" y="512"/></Size>
<Layers>
<Layer level="BACKGROUND">
@@ -232,8 +274,9 @@
</Layer></Layers>
</Frame>
<!-- The class trainer's first row (ClassTrainerSkill1, Blizzard_TrainerUI.xml:214-222). -->
<Button name="BenillaScriptLogRow1" inherits="BenillaScriptLogRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-94"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-100"/></Offset></Anchor></Anchors>
</Button>
<Button name="BenillaScriptLogRow2" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaScriptLogRow3" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
@@ -245,28 +288,29 @@
<Button name="BenillaScriptLogRow9" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaScriptLogRow10" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaScriptLogRow11" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow10" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaScriptLogRow12" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow11" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<Button name="BenillaScriptLogRow13" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow12" relativePoint="BOTTOMLEFT"/></Anchors></Button>
<!-- Only the faux kit's right-hand Slider renders. -->
<!-- The class trainer's list frame (Blizzard_TrainerUI.xml:313-322, 184 tall as its .lua:471
sets it). Only the faux kit's right-hand Slider renders. -->
<ScrollFrame name="BenillaScriptLogListScrollFrame" inherits="FauxScrollFrameTemplate">
<Size><AbsDimension x="293" y="208"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-94"/></Offset></Anchor></Anchors>
<Scripts>
<Size><AbsDimension x="296" y="184"/></Size>
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-67" y="-96"/></Offset></Anchor></Anchors>
<Scripts>
<!-- The stock kit has no `updateFunc`, so the repaint is wired here. -->
<OnVerticalScroll>FauxScrollFrame_OnVerticalScroll(BENILLA_SCRIPTLOG_ROW_HEIGHT, BenillaScriptLog_Update)</OnVerticalScroll>
</Scripts>
</ScrollFrame>
</ScrollFrame>
<!-- The real-scroll kit: a message with its traceback has variable height. -->
<!-- The class trainer's detail pane, 8 under its list and 119 tall (Blizzard_TrainerUI.xml:
346-356, .lua:472), so it ends above the buttons' sockets. The real-scroll kit: a
message with its traceback has variable height. -->
<ScrollFrame name="BenillaScriptLogDetailScroll">
<Size><AbsDimension x="293" y="128"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-314"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="296" y="119"/></Size>
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogListScrollFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
<Scripts>
<OnLoad>self:SetScrollChild(getglobal(self:GetName() .. "Child"))</OnLoad>
<OnMouseWheel>BenillaScroll_OnMouseWheel(self, arg1)</OnMouseWheel>
<OnVerticalScroll>BenillaScroll_SyncBar(self)</OnVerticalScroll>
<OnScrollRangeChanged>BenillaScroll_SyncBar(self)</OnScrollRangeChanged>
<OnLoad>this:SetScrollChild(getglobal(this:GetName() .. "Child"))</OnLoad>
<OnMouseWheel>BenillaScroll_OnMouseWheel(this, arg1)</OnMouseWheel>
<OnVerticalScroll>BenillaScroll_SyncBar(this)</OnVerticalScroll>
<OnScrollRangeChanged>BenillaScroll_SyncBar(this)</OnScrollRangeChanged>
</Scripts>
<Frames>
<Slider name="$parentBar" inherits="BenillaScrollBarTemplate" hidden="true">
@@ -274,64 +318,82 @@
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="6" y="-16"/></Offset></Anchor>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="6" y="16"/></Offset></Anchor>
</Anchors>
<Scripts><OnValueChanged>BenillaScrollBar_OnValueChanged(self, arg1)</OnValueChanged></Scripts>
</Slider>
<Frame name="$parentChild">
<Size><AbsDimension x="293" y="128"/></Size>
<!-- Mouse-enabled and the pane's full height, as SendMailScrollChildFrame is
(MailFrame.xml:604-659): a click below the text focuses the box. -->
<Frame name="$parentChild" enableMouse="true">
<Size><AbsDimension x="296" y="119"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<Layers>
<Layer level="ARTWORK">
<!-- 270 wide so the traceback wraps rather than clips. -->
<FontString name="BenillaScriptLogDetailText" inherits="GameFontHighlightSmall" justifyH="LEFT" justifyV="TOP" text="">
<Size><AbsDimension x="270" y="0"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="6" y="-6"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- A multi-line box in a scroll frame as MailFrame.xml:616-649 hosts the mail
body: `autoFocus="false"`, so the keyboard stays with the game until a
click, and Escape clears the focus. A multi-line box is as tall as its
insets and its wrapped text, so the scroll frame ranges over it as
ScrollingEdit_OnTextChanged (UIPanelTemplates.lua:307) recomputes. The
insets seat the text 6 in and 6 down, 270 wide, and leave 20 below
the last line. No `letters`, so the whole error goes in. Read-only
through OnTextChanged; a click selects it all, so Ctrl+C copies the
error. Not wired: the stock scroll-to-caret (UIPanelTemplates.lua:
314-342), since selecting all leaves the caret at the end and a
click would scroll a long traceback to its bottom. -->
<EditBox name="BenillaScriptLogDetailText" multiLine="true" autoFocus="false">
<Size><AbsDimension x="296" y="119"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
<TextInsets><AbsInset left="6" right="20" top="6" bottom="20"/></TextInsets>
<Scripts>
<OnTextChanged>BenillaScriptLogDetail_OnTextChanged()</OnTextChanged>
<OnEditFocusGained>this:HighlightText()</OnEditFocusGained>
<OnEscapePressed>this:ClearFocus()</OnEscapePressed>
</Scripts>
<FontString inherits="GameFontHighlightSmall"/>
</EditBox>
</Frames>
<Scripts>
<OnMouseUp>BenillaScriptLogDetailText:SetFocus()</OnMouseUp>
</Scripts>
</Frame>
</Frames>
</ScrollFrame>
<!-- The art's two button sockets, where the class trainer's Train and Exit sit
(Blizzard_TrainerUI.xml:481-513). -->
<Button name="BenillaScriptLogClearButton" inherits="UIPanelButtonTemplate" text="Clear">
<Size><AbsDimension x="96" y="22"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-455"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="80" y="22"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="224" y="-420"/></Offset></Anchor></Anchors>
<Scripts><OnClick>BenillaScriptLog_Clear()</OnClick></Scripts>
</Button>
<Button name="BenillaScriptLogCloseBottomButton" inherits="UIPanelButtonTemplate" text="Close">
<Size><AbsDimension x="96" y="22"/></Size>
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="219" y="-455"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="80" y="22"/></Size>
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="305" y="-420"/></Offset></Anchor></Anchors>
<Scripts><OnClick>HideUIPanel(getglobal("BenillaScriptLogFrame"))</OnClick></Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>BenillaScriptLogFrame_OnLoad(self)</OnLoad>
<OnLoad>BenillaScriptLogFrame_OnLoad()</OnLoad>
<!-- A spin bubbles up the parent chain; the detail pane's own handler wins there. -->
<OnMouseWheel>BenillaScriptLogFrame_OnMouseWheel(arg1)</OnMouseWheel>
<OnShow>BenillaScriptLog_Update()</OnShow>
<OnUpdate>BenillaScriptLogFrame_OnUpdate()</OnUpdate>
</Scripts>
</Frame>
<!-- Benilla's slash commands, handed to SubmitChatInput (ui_chat::input); loads after the
stock ChatFrame.xml, which declares SlashCmdList. -->
<!-- The player's commands, as an addon registers them; after the stock ChatFrame.lua, which
declares SlashCmdList. -->
<Script><![CDATA[
local BENILLA_COMMANDS = {
{ "CHATTEST", "/chattest" },
{ "SHOT", "/shot" },
{ "LIQUID", "/liquid" },
{ "CASTVIS", "/castvis" },
{ "PARTYTEST", "/partytest" },
{ "REACTION", "/reaction", "/react" },
{ "RELOAD", "/reload" },
{ "ERRORS", "/errors", "/err" },
{ "CONVERTRAID", "/convertraid" },
}
for _, row in ipairs(BENILLA_COMMANDS) do
local key = "BENILLA_" .. row[1]
for i = 2, table.getn(row) do
setglobal("SLASH_" .. key .. (i - 1), row[i])
end
SlashCmdList[key] = function(msg)
SubmitChatInput(row[2] .. " " .. (msg or ""))
-- Deviation: 1.12 has no `/reload` (later clients added it; 1.12 reloads through
-- `/console reloadui`); kept so a player can reload the interface after toggling addons.
SLASH_BENILLA_RELOAD1 = "/reload"
SlashCmdList["BENILLA_RELOAD"] = function()
ReloadUI()
end
-- `/errors` toggles the log and `/errors clear` empties it; `/err` is the short form.
SLASH_BENILLA_ERRORS1 = "/errors"
SLASH_BENILLA_ERRORS2 = "/err"
SlashCmdList["BENILLA_ERRORS"] = function(msg)
local arg = string.lower((string.gsub(msg or "", "^%s*(.-)%s*$", "%1")))
if arg == "clear" then
BenillaScriptLog_Clear()
else
BenillaScriptLog_Toggle()
end
end
]]></Script>
@@ -3,8 +3,8 @@
Benilla's scroll parts: a `Slider` bar with its arrow helpers, an optional trough, and a
real-scroll kit that pans a `<ScrollFrame>` child whose height is known only after layout.
Faux lists use the stock `FauxScrollFrameTemplate`; this file declares only `Benilla` names.
A real-scroll bar overrides the template's OnValueChanged on its instance, which wins
because the loader runs every `<Scripts>` block in order.
The bar template carries the real-scroll OnValueChanged itself, as the reference's
`UIPanelScrollBarTemplate` does (UIPanelTemplates.xml:156-158); an instance adds nothing.
-->
<Script><![CDATA[
-- One valueStep per step, where the reference's arrows and wheel move half the bar
@@ -26,23 +26,6 @@
if val >= maxV then down:Disable() else down:Enable() end
end
-- The value is pixels in steps of a row; the parent holds the 0-based top row in
-- `frame.offset` and its repaint in `frame.updateFunc`, which runs only when that row
-- changes, so a repaint that re-seats the thumb ends the loop. A ScrollFrame parent also
-- gets SetVerticalScroll, as UIPanelTemplates.xml:156-158 routes it.
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
-- ReputationFrame's hang, 8 left, 21 above, 20 below (ReputationFrame.xml:573-599 against
-- UIPanelTemplates.xml:166-181). Lazy: the bar does not exist at the trough's OnLoad.
function BenillaScrollTrough_Seat(trough, scrollBar)
@@ -96,11 +79,18 @@
BenillaScrollBar_UpdateArrows(bar)
end
-- The range the bar follows: the engine's, the reference's union of the child's whole
-- subtree, unless the frame measures its own content (`sf.contentRange`).
function BenillaScroll_Range(sf)
if sf.contentRange then return sf.contentRange(sf) end
return sf:GetVerticalScrollRange()
end
-- Hides the bar when the content fits (reference: ScrollFrame_OnScrollRangeChanged).
function BenillaScroll_SyncBar(sf)
local bar = getglobal(sf:GetName() .. "Bar")
if not bar then return end
local range = sf:GetVerticalScrollRange()
local range = BenillaScroll_Range(sf)
bar:SetValueStep(BENILLA_SCROLL_STEP)
bar:SetMinMaxValues(0, range)
bar:SetValue(sf:GetVerticalScroll())
@@ -111,7 +101,7 @@
-- Clamped here: SetVerticalScroll stores any value, as the reference's does.
function BenillaScroll_Step(sf, delta)
local target = sf:GetVerticalScroll() + delta
local range = sf:GetVerticalScrollRange()
local range = BenillaScroll_Range(sf)
if target > range then target = range end
if target < 0 then target = 0 end
sf:SetVerticalScroll(target)
@@ -183,7 +173,7 @@
</Button>
</Frames>
<Scripts>
<OnValueChanged>BenillaFauxScrollBar_OnValueChanged(self, arg1)</OnValueChanged>
<OnValueChanged>BenillaScrollBar_OnValueChanged(this, arg1)</OnValueChanged>
</Scripts>
<ThumbTexture name="$parentThumbTexture" file="Interface\Buttons\UI-ScrollBar-Knob">
<Size><AbsDimension x="16" y="16"/></Size>
@@ -171,3 +171,57 @@ fn the_corpus_favourite_template_composes_parent_two_levels_deep() {
assert!(s.take_errors().is_empty());
assert_eq!(s.take_warnings(), Vec::<String>::new());
}
/// `BenillaScrollBarTemplate` carries the real-scroll `OnValueChanged` itself, as
/// `UIPanelScrollBarTemplate` does (`UIPanelTemplates.xml:156-158`): a bare instance, with no
/// handler of its own, pans its `ScrollFrame` parent and greys the arrow at the range's end.
#[test]
fn the_benilla_scroll_bar_template_scrolls_its_parent_by_itself() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_ui(&s);
let _ = s.take_warnings();
let _ = s.take_errors();
s.run(
r#"
BarHost = CreateFrame("ScrollFrame", "BenillaTemplateProbeHost", UIParent)
BarHost.updateFunc = function() UpdateCalls = (UpdateCalls or 0) + 1 end
Bar = CreateFrame("Slider", "BenillaTemplateProbeBar", BarHost, "BenillaScrollBarTemplate")
Bar:SetMinMaxValues(0, 100)
Bar:SetValueStep(20)
Bar:SetValue(40)
"#,
)
.expect("a bare instance of the bar template must not error");
assert_eq!(
s.eval::<f32>("return BarHost:GetVerticalScroll()").unwrap(),
40.0,
"the template's own OnValueChanged scrolls the parent"
);
assert!(
s.eval::<bool>("return BarHost.offset == nil and UpdateCalls == nil")
.unwrap(),
"a scroll is not a faux list's `offset`/`updateFunc` write on the parent"
);
assert!(
s.eval::<bool>(r#"return BenillaTemplateProbeBarScrollDownButton:IsEnabled() == 1"#)
.unwrap(),
"mid-range, the down arrow is live"
);
s.run("Bar:SetValue(100)").unwrap();
assert_eq!(
s.eval::<f32>("return BarHost:GetVerticalScroll()").unwrap(),
100.0
);
assert!(
s.eval::<bool>(r#"return BenillaTemplateProbeBarScrollDownButton:IsEnabled() ~= 1"#)
.unwrap(),
"at the end of the range the down arrow greys"
);
assert!(s.take_errors().is_empty());
assert_eq!(s.take_warnings(), Vec::<String>::new());
}