Files
benilla-octo/crates/benilla-app/assets/ui/UIParent.xml
T
SamandClaude Fable 5 b5bc569156 the addon arc: the AddOns folder opens
- benilla loads real 1.12 addons from Interface/AddOns — TOC, XML and Lua through one loader, with per-addon saved variables, Bindings.xml and the lifecycle events addons expect
- both AddOns screens are in: the character-select list with per-addon enable state, and the ESC menu's in-game list
- the API surface is measured, not guessed — a corpus harness runs hundreds of real 1.12 addons and ranks exactly which globals, templates and verbs they demand
- the Lua dialect is the client's own, read off the binary — 5.0 semantics with the 1.12 additions, and UI source files are bytes, not UTF-8

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

572 lines
33 KiB
XML

<Ui>
<!--
UIParent itself (ref UIParent.xml l.5, quoted): the real, full-screen, invisible root
frame the reference UI hangs everything on. Until now benilla shipped only this file's
Lua (the manage pass below) and no frame - so the Lua global `UIParent` was nil, and
every ref-quoted `SetOwner(UIParent, ...)` / `GameTooltip_SetDefaultAnchor(this,
UIParent)` died before it could place anything (the world tooltip stayed parked on its
load-time anchor - the "tooltip on my character" bug). String anchors ("UIParent" in
SetPoint) had silently fallen back to the screen root, which has the same rect, so only
the *global* was ever missing. No enableMouse: the hit test gates on it (pointer.rs), so
the full-screen plate swallows nothing. The ref's OnLoad/OnEvent (UIParent_OnLoad's
global event routing) is not ours to ship - no consumer yet.
Known seam, stated: the real client PARENTS every FrameXML frame under UIParent (scale/
visibility cascade); benilla's top-level frames still attach to the screen root. Same
rect, so anchors agree - re-parenting is a separate arc if a cascade consumer appears.
-->
<Frame name="UIParent" setAllPoints="true" frameStrata="MEDIUM"/>
<!--
WorldFrame — the reference's full-screen, bottom-of-the-strata frame that the 3D world
renders into. **Ours renders nothing**: benilla draws the world through Bevy's render
graph, not through a FrameScript frame, so this is a NAMED HANDLE and nothing more. That
is a disclosed divergence, not a pretence — and it is what makes the two things addons
actually do with it work: `WorldFrame:GetWidth()`/`GetHeight()` (the screen rect, which
this frame really has) and `frame:SetParent(WorldFrame)` / `SetPoint(..., WorldFrame,
...)` (parking a tooltip or an overlay under everything). Measured demand: 29 of the 218
vanilla-corpus addons die on their first load with `attempt to index global 'WorldFrame'`
without it (the corpus harness, `benilla_app::addon_harness`).
**No `enableMouse`, deliberately** — the same reason UIParent above ships without it, and
here it is load-bearing rather than incidental. benilla's `PointerOverUi` is "is the
topmost mouse-enabled frame under the cursor a UI frame", and it gates camera look, world
clicks and hover targeting (`player/camera.rs`, `target/hover.rs`); a full-screen
mouse-enabled plate would answer yes for every world pixel and kill all three. The
reference gets away with it because *its* WorldFrame IS the world, so a click landing
there is a world click by construction. Reproducing that here means teaching the arbiter
that a WorldFrame hit is not a UI hit — engine work with its own tests, not a rename — so
until then the addon-facing `OnMouseDown`/`OnMouseUp` hooks on this frame never fire.
-->
<Frame name="WorldFrame" setAllPoints="true" frameStrata="BACKGROUND"/>
<!--
The UIParent bottom-stack position manager - UIPARENT_MANAGED_FRAME_POSITIONS and
UIParent_ManageFramePositions(), transcribed from the extracted 1.12 UIParent.lua
l.1560-1697 (patch-2.MPQ via mpqx, byte-diffed against this install's own chain;
decision 0272). The reference never leaves a bottom-anchored HUD frame on its static
XML anchor: every layout pass re-anchors it to a base offset plus per-bar deltas -
either bottom multibar, the pet/stance bar, the reputation watch bar - fired from those
bars' OnShow/OnHide (ref BonusActionBarFrame.xml:350-355 et al.) and applied once at
startup. The loader's post-load call (ui_script/mod.rs) is our startup seam; the stance
bar's OnShow/OnHide (StanceBar.xml) is the one live re-fire today.
Departures, each named:
- Lua 5.1 dialect (the 1.12 client ran 5.0): pairs() iteration, table.insert for
tinsert, _G[name] = v for setglobal, and the ref's undeclared `hasBottomRight`
declared local (it is never read).
- The ref gates the bottom flags on its interface-option globals
SHOW_MULTI_ACTIONBAR_1/2; benilla's bottom bars are always-on with no options state
(decision 0270), so the flags read the bars' own visibility - identical values, no
parallel global to drift.
- Frame names: our stance bar, pet bar and bottom multibars wear the reference's own
names (ShapeshiftBarFrame, PetActionBarFrame, MultiBarBottomLeft/Right), so their
rows and flags in the table below are LIVE, not the no-ops they were while those
frames carried a `Benilla` prefix. That is the payoff the bare names buy — the same
one `group_loot_tests::managed_positions_engage_for_the_bare_frame_name` pins for
GroupLootFrame1 — and it costs nothing on screen: each frame's static XML anchor was
already written to the value the row computes (the stance bar's XML (30,45) is the
row's xOffset 30 + baseY 0 + bottomLeft 45, and MultiBarBottomLeft's XML (0,17) is
its row's baseY 17), so the pass now re-derives what the XML asserted. TutorialFrameParent
and FramerateLabel still have no frame here and no-op through the ref's `if ( frame )`.
- MultiBarLeft/MultiBarRight (the vertical bars) and ReputationWatchBar don't exist yet:
nil-guarded (the ref reads them bare only because it always creates them). MainMenuExpBar
and MainMenuBarMaxLevelBar DO exist now, so the `reputation` and `maxLevel` flags are
real reads — `reputation` still cannot fire (it needs ReputationWatchBar too) and
`maxLevel` fires exactly when the level-60 rail replaces the XP bar, which is the ref's
own rule.
- No IsUserPlaced/IsObjectType/SIMPLE_CHAT legs on the SetPoint: benilla has no
user-placed frames and no simple-chat option, so the ref's guarded SetPoint
collapses to the plain one.
- Of the ref function's trailing custom positioning, the pet-bar half IS ours (the
sliding shelf art + the PETACTIONBAR_XPOS/YPOS re-seat, at the end of the pass,
decision 0988). BattlefieldMinimapTab and the shapeshift-bar art swap stay out of
scope - neither frame exists here, our stance bar shipping no end-caps at all (0270).
- The isVar globals (CONTAINER_OFFSET_*, PETACTIONBAR_YPOS, BATTLEFIELD_TAB_OFFSET_Y)
are pre-declared to their base values below - the ref declares them in
ContainerFrame.lua/PetActionBarFrame.lua, which we don't ship - so the ref's
`if ( frame )` guard (getglobal on a var row yields the number) behaves identically.
Nothing consumes them yet; they are set for the day the bag anchors do.
-->
<Script><![CDATA[
-- Declared by the ref's ContainerFrame.lua / PetActionBarFrame.lua / WorldStateFrame.lua
-- (files we don't ship); the manage pass below overwrites them every run.
CONTAINER_OFFSET_X = 0
CONTAINER_OFFSET_Y = 70
BATTLEFIELD_TAB_OFFSET_Y = 210
PETACTIONBAR_YPOS = 97
-- ref PetActionBarFrame.lua:3. Not a managed row (the manage pass computes only the Y);
-- the custom block at the end of the pass recomputes it, for the reason recorded there.
PETACTIONBAR_XPOS = 36
-- ref UIParent.lua:2 — the model panes' held-button spin rate (half a turn per second),
-- read by every `Model_OnUpdate` transcription (the character window's + inspect's).
ROTATIONS_PER_SECOND = .5
-- ref SecondsToTime (UIParent.lua:1004-1031) and its GlobalStrings, in the ref's own home
-- file — two consumers now (the quest-timer window and the quest log's "Time Remaining:"
-- row, decision 1150), so it lives here rather than being redefined per file the way
-- BuffFrame.xml had to for SecondsToTimeAbbrev when it was the only caller.
--
-- Departures, both named:
-- - `GetText(TOKEN, nil, n)` is the ref's plural-aware GlobalString lookup, a C binding we
-- don't ship. Its enUS rule is the singular token for exactly 1 and the `_P1` twin
-- otherwise — the same rule already codified in `tooltip_item/render.rs` and MailFrame.xml
-- — so it collapses to the local `plural()` below and the four shipped string pairs
-- (GlobalStrings.lua:895-896, 2296-2297, 2693-2694, 3404-3405).
-- - Lua 5.1 dialect: `math.mod`/`math.floor` for the 5.0 bare `mod`/`floor`.
--
-- Everything else is verbatim, INCLUDING the two behaviours that look like bugs and are
-- not: the thresholds are strict `>` (86400 s reads as hours, not "1 Day"), and `count < 2`
-- stops after two units, so a 3-day quest reads "3 Days 4 Hrs" and never mentions minutes.
DAYS_ABBR = DAYS_ABBR or "Day"
DAYS_ABBR_P1 = DAYS_ABBR_P1 or "Days"
HOURS_ABBR = HOURS_ABBR or "Hr"
HOURS_ABBR_P1 = HOURS_ABBR_P1 or "Hrs"
MINUTES_ABBR = MINUTES_ABBR or "Min"
MINUTES_ABBR_P1 = MINUTES_ABBR_P1 or "Mins"
SECONDS_ABBR = SECONDS_ABBR or "Sec"
SECONDS_ABBR_P1 = SECONDS_ABBR_P1 or "Secs"
if not SecondsToTime then
local function plural(singular, pluralForm, n)
if n == 1 then return singular end
return pluralForm
end
function SecondsToTime(seconds, noSeconds)
local time = ""
local count = 0
local tempTime
seconds = math.floor(seconds)
if seconds > 86400 then
tempTime = math.floor(seconds / 86400)
time = tempTime .. " " .. plural(DAYS_ABBR, DAYS_ABBR_P1, tempTime) .. " "
seconds = math.mod(seconds, 86400)
count = count + 1
end
if seconds > 3600 then
tempTime = math.floor(seconds / 3600)
time = time .. tempTime .. " " .. plural(HOURS_ABBR, HOURS_ABBR_P1, tempTime) .. " "
seconds = math.mod(seconds, 3600)
count = count + 1
end
if count < 2 and seconds >= 60 then
tempTime = math.floor(seconds / 60)
time = time .. tempTime .. " " .. plural(MINUTES_ABBR, MINUTES_ABBR_P1, tempTime) .. " "
seconds = math.mod(seconds, 60)
count = count + 1
end
if count < 2 and seconds > 0 and not noSeconds then
time = time .. seconds .. " " .. plural(SECONDS_ABBR, SECONDS_ABBR_P1, seconds) .. " "
end
return time
end
end
UIPARENT_MANAGED_FRAME_POSITIONS = {}
-- Frames
UIPARENT_MANAGED_FRAME_POSITIONS["MultiBarBottomLeft"] = {baseY = 17, reputation = 9, maxLevel = -5, anchorTo = "ActionButton1", point = "BOTTOMLEFT", rpoint = "TOPLEFT"}
UIPARENT_MANAGED_FRAME_POSITIONS["GroupLootFrame1"] = {baseY = 60, bottomEither = 42, pet = 42, reputation = 9}
UIPARENT_MANAGED_FRAME_POSITIONS["TutorialFrameParent"] = {baseY = 55, bottomEither = 47, pet = 42, reputation = 9}
UIPARENT_MANAGED_FRAME_POSITIONS["FramerateLabel"] = {baseY = 64, bottomEither = 42, pet = 42, reputation = 9}
UIPARENT_MANAGED_FRAME_POSITIONS["CastingBarFrame"] = {baseY = 60, bottomEither = 40, pet = 40, reputation = 9}
UIPARENT_MANAGED_FRAME_POSITIONS["ChatFrame1"] = {baseY = 85, bottomLeft = 17, pet = 17, reputation = 9, maxLevel = -5, anchorTo = "UIParent", point = "BOTTOMLEFT", rpoint = "BOTTOMLEFT", xOffset = 32}
UIPARENT_MANAGED_FRAME_POSITIONS["ChatFrame2"] = {baseY = 85, bottomRight = 17, rightLeft = -88, rightRight = -43, reputation = 9, maxLevel = -5, anchorTo = "UIParent", point = "BOTTOMRIGHT", rpoint = "BOTTOMRIGHT", xOffset = -32}
UIPARENT_MANAGED_FRAME_POSITIONS["ShapeshiftBarFrame"] = {baseY = 0, bottomLeft = 45, reputation = 9, maxLevel = -5, anchorTo = "MainMenuBar", point = "BOTTOMLEFT", rpoint = "TOPLEFT", xOffset = 30}
-- Vars
UIPARENT_MANAGED_FRAME_POSITIONS["CONTAINER_OFFSET_X"] = {baseX = 0, rightLeft = 90, rightRight = 45, isVar = "xAxis"}
UIPARENT_MANAGED_FRAME_POSITIONS["CONTAINER_OFFSET_Y"] = {baseY = 70, bottomEither = 27, bottomRight = 0, reputation = 9, isVar = "yAxis", pet = 23}
UIPARENT_MANAGED_FRAME_POSITIONS["BATTLEFIELD_TAB_OFFSET_Y"] = {baseY = 210, bottomRight = 40, reputation = 9, isVar = "yAxis"}
UIPARENT_MANAGED_FRAME_POSITIONS["PETACTIONBAR_YPOS"] = {baseY = 97, bottomLeft = 43, reputation = 9, maxLevel = -5, isVar = "yAxis"}
function UIParent_ManageFramePositions()
-- Frames that affect offsets in y axis
local yOffsetFrames = {}
-- Frames that affect offsets in x axis
local xOffsetFrames = {}
-- Set up flags. Departure: the bottom flags read the always-on bars' visibility,
-- not the ref's SHOW_MULTI_ACTIONBAR_1/2 option globals (0270 ships no such state).
local bottomLeftShown = MultiBarBottomLeft and MultiBarBottomLeft:IsShown()
local bottomRightShown = MultiBarBottomRight and MultiBarBottomRight:IsShown()
if ( bottomLeftShown or bottomRightShown ) then
table.insert(yOffsetFrames, "bottomEither")
end
if ( bottomRightShown ) then
table.insert(yOffsetFrames, "bottomRight")
end
if ( bottomLeftShown ) then
table.insert(yOffsetFrames, "bottomLeft")
end
if ( MultiBarLeft and MultiBarLeft:IsShown() ) then
table.insert(xOffsetFrames, "rightLeft")
elseif ( MultiBarRight and MultiBarRight:IsShown() ) then
table.insert(xOffsetFrames, "rightRight")
end
if ( ( PetActionBarFrame and PetActionBarFrame:IsShown() ) or ( ShapeshiftBarFrame and ShapeshiftBarFrame:IsShown() ) ) then
table.insert(yOffsetFrames, "pet")
end
if ( ReputationWatchBar and MainMenuExpBar and ReputationWatchBar:IsShown() and MainMenuExpBar:IsShown() ) then
table.insert(yOffsetFrames, "reputation")
end
if ( MainMenuBarMaxLevelBar and MainMenuBarMaxLevelBar:IsShown() ) then
table.insert(yOffsetFrames, "maxLevel")
end
-- Iterate through frames and set anchors according to the flags set
local frame, xOffset, yOffset, anchorTo, point, rpoint
for index, value in pairs(UIPARENT_MANAGED_FRAME_POSITIONS) do
frame = getglobal(index)
if ( frame ) then
-- Always start with base as the base offset or default to zero if no "none" specified
xOffset = 0
if ( value["baseX"] ) then
xOffset = value["baseX"]
elseif ( value["xOffset"] ) then
xOffset = value["xOffset"]
end
yOffset = 0
if ( value["baseY"] ) then
yOffset = value["baseY"]
end
-- Iterate through frames that affect y offsets
local hasBottomLeft, hasPetBar, hasBottomRight
for flag, flagValue in pairs(yOffsetFrames) do
if ( value[flagValue] ) then
if ( flagValue == "bottomLeft" ) then
hasBottomLeft = 1
elseif ( flagValue == "pet" ) then
hasPetBar = 1
elseif ( flagValue == "bottomRight" ) then
hasBottomRight = 1
end
yOffset = yOffset + value[flagValue]
end
end
if ( hasBottomLeft and hasPetBar ) then
yOffset = yOffset + 23
end
-- Iterate through frames that affect x offsets
for flag, flagValue in pairs(xOffsetFrames) do
if ( value[flagValue] ) then
xOffset = xOffset + value[flagValue]
end
end
-- Set up anchoring info
anchorTo = value["anchorTo"]
point = value["point"]
rpoint = value["rpoint"]
if ( not anchorTo ) then
anchorTo = "UIParent"
end
if ( not point ) then
point = "BOTTOM"
end
if ( not rpoint ) then
rpoint = "BOTTOM"
end
-- Anchor frame
if ( value["isVar"] ) then
if ( value["isVar"] == "xAxis" ) then
_G[index] = xOffset
else
_G[index] = yOffset
end
else
frame:SetPoint(point, anchorTo, rpoint, xOffset, yOffset)
end
end
end
-- Custom positioning the row table cannot express (ref l.1705-1738). Both rules
-- belong to the pet/stance ROW — the one row two different bars share — and without
-- them the pet bar lands on top of the bottom-left multibar's buttons
-- (director-caught: the bar drew across a row of spells, border and all).
--
-- (a) The shelf art IS the pet bar's border, and it only makes sense when the bar
-- sits directly on the main bar. Raised a row it would draw across whatever is
-- underneath, so the ref hides both strips (l.1707-1708) — the same move it makes
-- for the stance bar's own end-caps, which is why benilla's stance bar has none
-- (0270 anchors it statically at the raised position).
-- (b) The pet bar is the only managed entry whose row is a VAR: the loop above writes
-- the NUMBER PETACTIONBAR_YPOS (97, +43 with the bottom-left bar up) and anchors
-- nothing, so the ref re-seats the frame with it here (l.1735-1736).
--
-- Departure: the ref computes PETACTIONBAR_XPOS in ShowPetActionBar() and reads it
-- here, which only works because its bar slides in over several frames. Ours appears
-- at once, so the value is computed where it is used. Same rule
-- (PetActionBarFrame.lua:170-174): the two bars share this row, so a pet bar shown
-- while the stance bar is up starts 20px past the last stance button. That case is
-- rare but real — a possessed or charmed unit gives a pet bar to a class that also
-- has forms.
local bottomLeftUp = MultiBarBottomLeft and MultiBarBottomLeft:IsShown()
if ( SlidingActionBarTexture0 and SlidingActionBarTexture1 ) then
if ( bottomLeftUp ) then
SlidingActionBarTexture0:Hide()
SlidingActionBarTexture1:Hide()
else
SlidingActionBarTexture0:Show()
SlidingActionBarTexture1:Show()
end
end
if ( PetActionBarFrame ) then
PETACTIONBAR_XPOS = 36
local forms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
if ( forms > 0 and ShapeshiftBarFrame and ShapeshiftBarFrame:IsShown() ) then
local last = getglobal("ShapeshiftButton"..forms)
local right = last and last:GetRight()
if ( right ) then
PETACTIONBAR_XPOS = right + 20
end
end
-- The ref guards this on IsShown() because a hidden bar of its is mid-slide and
-- owns its own anchor; ours has no slide, so re-seating a hidden frame is a
-- no-op and the guard would only add an ordering trap (OnShow fires the pass).
PetActionBarFrame:SetPoint("TOPLEFT", "MainMenuBar", "BOTTOMLEFT", PETACTIONBAR_XPOS, PETACTIONBAR_YPOS)
end
-- The under-minimap right stack (ref l.1740-1770): anchorY walks down capture bars →
-- quest timer → durability → quest watch, each shown frame pushing the next one down
-- by its own height. benilla ships the quest-timer, durability and quest-watch arms
-- (below); the capture bars above them don't exist yet, so anchorY starts at the ref's
-- initial 0. The reference never leaves DurabilityFrame on its static XML anchor
-- (whose +40 hangs the side glyphs off-screen — director-caught): every pass seats it
-- at -CONTAINER_OFFSET_X, minus 20 more when a showSeparate glyph
-- (shield/off-weapon/ranged) extends the art to the right — then drops the quest
-- tracker to the running anchorY so the two never collide at the same corner.
local anchorY = 0
-- ref l.1753-1757 verbatim (decision 1150). Seated whether shown or not — the ref
-- SetPoints unconditionally and only the height accrual is gated — so the frame is
-- already in place the frame it appears. Nil-guarded like every row here: the
-- durability-only test harness ships no quest-timer frame.
if ( QuestTimerFrame ) then
QuestTimerFrame:SetPoint("TOPRIGHT", "MinimapCluster", "BOTTOMRIGHT", -CONTAINER_OFFSET_X, anchorY)
if ( QuestTimerFrame:IsShown() ) then
anchorY = anchorY - QuestTimerFrame:GetHeight()
end
end
if ( DurabilityFrame ) then
local durabilityOffset = 0
if ( DurabilityShield:IsShown() or DurabilityOffWeapon:IsShown() or DurabilityRanged:IsShown() ) then
durabilityOffset = 20
end
DurabilityFrame:SetPoint("TOPRIGHT", "MinimapCluster", "BOTTOMRIGHT", -CONTAINER_OFFSET_X-durabilityOffset, anchorY)
if ( DurabilityFrame:IsShown() ) then
anchorY = anchorY - DurabilityFrame:GetHeight()
end
end
-- QuestWatchFrame is last in the ref's walk (l.1770), seated at the running anchorY so
-- it drops below the durability guy when he shows instead of overlapping him (both
-- anchor this same MinimapCluster corner — director-caught). benilla's tracker is
-- QuestWatchFrame; nil-guarded because the durability-only test harness (and
-- any config without a quest log) ships no such frame.
if ( QuestWatchFrame ) then
QuestWatchFrame:SetPoint("TOPRIGHT", "MinimapCluster", "BOTTOMRIGHT", -CONTAINER_OFFSET_X, anchorY)
end
end
-- ref UIParent.lua:1819-1878, transcribed 1:1 (decision 1008 — closes 0997's
-- "abbreviates through KEY_ABBR" residue with the REAL mechanism: it never was a
-- KEY_ABBR set; the whole abbreviation is this Lua). The action bar's HotKey corners
-- call it with returnAbbr=1: ONE modifier abbreviates (CTRL→c, SHIFT→s, ALT→a — so
-- SHIFT-2 reads "s-2", the director's ask), TWO OR MORE collapse the whole label to
-- "·" — the ref's own multi-modifier dot. The full form (tooltips, the Keybindings
-- page) keeps the canonical prefixes and reads the base key's KEY_* global.
-- Transcription notes: the ref's dead `gsub(tempName, "CAPSLOCK", "Caps")` line
-- (result discarded — gsub doesn't mutate) is dropped; its GetLocale() deDE
-- CTRL→STRG swap and IsMacClient() KEY_*_MAC lookup ride behind existence guards
-- (this VM defines neither yet, and the getglobal fallback covers the gap).
function GetBindingText(name, prefix, returnAbbr)
if ( not name ) then
return "";
end
local tempName = name;
local i = strfind(name, "-");
local dashIndex = nil;
local count = 0;
while ( i ) do
if ( not dashIndex ) then
dashIndex = i;
else
dashIndex = dashIndex + i;
end
count = count + 1;
tempName = strsub(tempName, i + 1);
i = strfind(tempName, "-");
end
local modKeys = '';
if ( not dashIndex ) then
dashIndex = 0;
else
modKeys = strsub(name, 1, dashIndex);
if ( GetLocale and GetLocale() == "deDE" ) then
modKeys = gsub(modKeys, "CTRL", "STRG");
end
end
if ( returnAbbr ) then
if ( count > 1 ) then
return "·";
else
modKeys = gsub(modKeys, "CTRL", "c");
modKeys = gsub(modKeys, "SHIFT", "s");
modKeys = gsub(modKeys, "ALT", "a");
modKeys = gsub(modKeys, "STRG", "st");
end
end
if ( not prefix ) then
prefix = "";
end
local localizedName = nil;
if ( IsMacClient and IsMacClient() ) then
localizedName = getglobal(prefix..tempName.."_MAC");
end
if ( not localizedName ) then
localizedName = getglobal(prefix..tempName);
end
if ( not localizedName ) then
localizedName = tempName;
end
return modKeys..localizedName;
end
-- ref UIParent.lua:1051-1057, transcribed 1:1 (decision 1062). The reference's own
-- list joiner, and it is exactly as plain as it looks: a bare ", " between every item,
-- with NO "and" before the last one and NO localization string of any kind — 1.12 has
-- no LIST_DELIMITER/AND global for it to reach for, and it does not build one. (The
-- richer `BuildColoredListString` right below it in the reference — the per-item
-- met/unmet red — already lives in TradeSkillFrame.xml as a guarded global, 0437 phase
-- 3; folding that one up here beside this is a follow-up, not this slice's churn.)
--
-- Callers: the pet paper doll's diet tooltip and PetStable.lua's, both as
-- `format(PET_DIET_TEMPLATE, BuildListString(GetPetFoodTypes()))`.
--
-- ZERO arguments returns NIL, not "" — the ref reads `arg[1]` off an empty vararg and
-- returns it unchanged. That is load-bearing: `format("%s", nil)` errors, so a caller
-- that can pass nothing (a warlock minion's empty diet) must be gated by its own UI, and
-- it is — the diet icon is hunter-only. Quietly returning "" here would hide that.
--
-- Lua-5.0 `arg`/`arg.n` written as `select("#", ...)`/`select(i, ...)`, the 5.1-native
-- spelling — TradeSkillFrame.xml's own substitution.
function BuildListString(...)
local n = select("#", ...);
if ( n == 0 ) then
return nil;
end
local list = select(1, ...);
for i = 2, n do
local item = select(i, ...);
list = list..", "..item;
end
return list;
end
-- BenillaChatEdit_InsertLink(link) → true if it went in, else nil (decision 1059).
--
-- Every shift-click site in this UI does the same two things before inserting: check the
-- chat edit box is up, and — ours alone — check the link is not nil. That nil case is
-- **benilla's, not the reference's**: our item templates resolve asynchronously
-- (`CMSG_ITEM_QUERY_SINGLE`, answered a frame or many later), so a `Get…Link` getter
-- legitimately answers nil for a slot whose name and quality have not landed, where the real
-- client's synchronous item cache always has one. `EditBox:Insert` takes a string (the
-- byte-modelled `0x77bee0`), so an unguarded nil is a script error — which is exactly the
-- bug this function exists to make impossible to reintroduce at the next site. A dozen call
-- sites were growing a dozen copies of the same two-line guard, and three of them shipped
-- without it.
--
-- The RETURN is what lets a caller fork on "did chat take it?" — a bag slot's shift-click
-- posts the link when chat is open and opens the stack splitter when it is not (ref
-- ContainerFrame.lua:567-577). Blizzard themselves eventually factored this exact helper out
-- for this exact reason: 2.0's `ChatEdit_InsertLink(text)`, returning true/false, called from
-- every one of these sites. 1.12 has no such function, so ours carries a benilla name rather
-- than pretending to be reference API.
function BenillaChatEdit_InsertLink(link)
if not link then
return nil
end
if not ChatFrameEditBox or not ChatFrameEditBox:IsVisible() then
return nil
end
ChatFrameEditBox:Insert(link)
return true
end
-- ── LowerFrameLevel / RaiseFrameLevel (ref UIParent.lua l.1890-1896), verbatim ────────
--
-- Two lines each, and the reason to have them is that they are FRAMEXML, not engine: an
-- addon calling `RaiseFrameLevel(f)` is calling a function the client's own UI defines, and
-- without it that is `attempt to call global` at load (BetterCharacterStats dies there).
-- Our own ActionBar.xml already describes this operation in prose at three sites while
-- doing it by hand; those can move onto the real name whenever that file is next opened.
function LowerFrameLevel(frame)
frame:SetFrameLevel(frame:GetFrameLevel() - 1)
end
function RaiseFrameLevel(frame)
frame:SetFrameLevel(frame:GetFrameLevel() + 1)
end
-- ── MouseIsOver (ref UIParent.lua l.1388-1417), verbatim ──────────────────────────────
--
-- "is the cursor inside this frame, optionally with per-edge padding" — the idiom every
-- hover-driven addon writes, and **9 corpus addons call it**. Missing, each is
-- `attempt to call global 'MouseIsOver'`.
--
-- Transcribed rather than approximated, including the two things that look like bugs and
-- are not:
-- · the offsets are ADDED to every edge, so a positive `bottomOffset` shrinks the box
-- from below while a positive `topOffset` grows it upward. The reference's own caller
-- proves the intent — `MouseIsOver(chatFrame, 45, -10, -5, 5)` means 45 up into the
-- tab strip, 10 in from the bottom, 5 out each side.
-- · a nil `topOffset` zeroes ALL FOUR, so `MouseIsOver(f, nil, 5)` silently ignores the 5.
-- That is the reference's `if ( not topOffset ) then` block exactly; an addon written
-- against 1.12 may depend on it.
-- The reference's `-- Hack to fix a symptom not the real issue` guard on an unresolved rect
-- is kept with its comment, because a frame that has never been laid out really does answer
-- nil here and returning nil is what callers expect.
--
-- ONE DIVERGENCE, and it is a no-op: the reference divides the cursor by
-- `frame:GetEffectiveScale()` because 1.12 hands Lua screen pixels. benilla's UI space is
-- 1:1 logical px and `GetEffectiveScale` is the constant 1 (`script/pointer.rs`,
-- `object.rs` l.271), so the division is written out rather than left in to divide by one
-- forever. If a real UI scale ever lands, this is one of the sites that needs it back.
function MouseIsOver(frame, topOffset, bottomOffset, leftOffset, rightOffset)
local x, y = GetCursorPosition()
local left = frame:GetLeft()
local right = frame:GetRight()
local top = frame:GetTop()
local bottom = frame:GetBottom()
if not topOffset then
topOffset = 0
bottomOffset = 0
leftOffset = 0
rightOffset = 0
end
-- Hack to fix a symptom not the real issue
if not left then
return
end
left = left + leftOffset
right = right + rightOffset
top = top + topOffset
bottom = bottom + bottomOffset
if (x > left and x < right) and (y > bottom and y < top) then
return 1
else
return nil
end
end
]]></Script>
</Ui>