Files
benilla-octo/crates/benilla-app/assets/ui/UIParent.xml
T
SamandClaude Fable 5.1 f0d1bacf0a the help window is the reference's own, and a ding carries nine gains
- the help window is the reference's own `HelpFrame.xml`, and ours was less than half its size, so this is mostly arrival: the category list, the per-category GM and harassment pages, the stuck page, the bug report, the PvP policy line; four things we had built go with the swap — one-click filing from Home, the Home unstick button, the downed-queue refusal message, and ticket abandonment, a capability loss rather than a look change, since `AbandonTicket` exists in neither the binding table nor the 1.12 FrameXML corpus and any return of it goes on top of the stock window; `GMSURVEY_DISPLAY` arrives with no producer, a real 1.12 event (`SignalEvent` `0x5e797b`) whose whole UI is a LoadOnDemand addon not on our chain; and the stock file needs two orderings ours did not, the micro-button row before its GM status ask and the buff frame before its repoll timer (1889, superseding 1687)
- the cast bar, the action-button regions and the font registry are the reference's own files too: our `CastingBar.xml` was `CastingBarFrame.xml` under another name, with the literal "Failed" where the reference reads the `FAILED` GlobalString (1886); `ActionButtonTemplate.xml` seats above our `ActionBar.xml` because the later-registered template wins, and it declares zero scripts, so the regions migrate while the handlers stay ours until `ActionButton.lua`'s verbs do (1887); `Fonts.xml` is manifest entry 0, all 51 fonts match by name, and its one real difference, `ITEM_QUALITY_COLORS`, moves to UIParent where the reference declares it (1888); the harnesses were the real cost — those reading our files off disk could not name a chain file and now read through the manifest's own rule — and the event-producer gate, having cried wolf at the cast bar's `SPELLCAST_INTERRUPTED` fired through a variable, now matches the fired tuple's shape
- of the 189 events benilla fires, three are not in the client — `BAG_UPDATE_DELAYED` (a Classic Era name), `LOOT_UPDATE` and `UPDATE_LOOT_ROLL` (invented here, no listener anywhere) — found against the reference binary's own string table, and a gate now fails any fire of a name the client cannot dispatch unless it wears a `BENILLA_` prefix (1883); two payloads the reference gives that we did not: `UNIT_HAPPINESS` carries its unit token, without which no `UNIT_*` consumer reads it, and `CHARACTER_POINTS_CHANGED` carries the point deltas `ChatFrame.lua` compares unguarded, so nil raised (1884); and `PLAYER_LEVEL_UP` carries all nine — level, health and mana gain, talent points (not on the wire: the client derives `(newLevel >= 10) ? 1 : 0` at `0x5e407c`) and the five stat gains — matched to the ding by level, a demotion's absent gains sent as zeros since a nil raises where a zero reads as no line
- the chain gap report could not run — one of its six probes had no player seated where a stock addon formats `UnitName("player")` at load, the other five each carrying an identical copy-pasted seed — and the repaired instrument prices each window's cost: 13 stock files load clean with nothing missing (the action bars, the cast bar, the fonts, the help, friends, game-menu and sound-options windows among them) and the near ones name their missing verb, with "loads clean" still not "swapped" (1885); and `UpdateNameplates`, recorded as the missing engine verb blocking the UIParent keystone, is FrameXML — defined in `UIOptionsFrame.lua`, absent from the binding table, its whole body four nameplate verbs that are already ours — so it is twelve lines transcribed into our options frame, the reference's own Show-then-Hide no-op included (1890 corrects 1878)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 20:50:36 +07:00

1114 lines
68 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 UIParent_OnLoad registers a long list of globally-routed events and its OnEvent
is a chain of `if event == ...` arms; ours ships exactly the ONE arm that has a consumer
(the extra bars' login seed, below) and grows an arm at a time as consumers appear. A
transcription of the whole routing table would be a list of no-ops that reads as coverage.
Known seam, stated: the real client PARENTS every FrameXML frame under UIParent (scale/
visibility cascade). benilla is PART WAY there - 41 of our top-level frames declare
parent="UIParent" where the reference declares 104, and 32 frames the reference parents
(MainMenuBar, PlayerFrame, TargetFrame, the four MultiBars, UIErrorsFrame, the panels)
still attach to the screen root because the transcription dropped the attribute. Same
rect, so anchors agree.
The cascade consumer this note was waiting for HAS appeared (decision 1724): a cinematic
hides the HUD by doing `UIParent:Hide()` in the reference, and benilla substitutes an
engine-side draw-kill that also switches the mouse feed off - which is how the world came
to take every click of a race intro. Restoring the dropped parents is the fix; it is a
whole-UI arc (draw order, strata, ALT-Z, the world map's own full-screen row) and so is
proposed rather than taken.
-->
<Frame name="UIParent" setAllPoints="true" frameStrata="MEDIUM">
<Scripts>
<OnLoad>
this:RegisterEvent("VARIABLES_LOADED")
this:RegisterEvent("MIRROR_TIMER_START")
this:RegisterEvent("PLAYER_ENTERING_WORLD")
this:RegisterEvent("READY_CHECK")
this:RegisterEvent("START_LOOT_ROLL")
this:RegisterEvent("CONFIRM_LOOT_ROLL")
</OnLoad>
<OnEvent>UIParent_OnEvent(event)</OnEvent>
</Scripts>
</Frame>
<!--
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 )`.
- Every frame these flags read now exists, so every flag is a real read — the nil guards
stay because the ref reads them bare only because it always creates them, and our
per-window test harnesses load file SUBSETS. `reputation` fires the moment a watched
faction raises ReputationWatchBar over a shown MainMenuExpBar (decision 1258; it could
not fire at all until that page landed, and `reputation_frame_tests` pins the 9px it
moves the bottom stack by). `maxLevel` fires exactly when the level-60 rail replaces the
XP bar — which is also why the two never fire together: at 60 the watch bar REPLACES
MainMenuExpBar rather than stacking above it, so `MainMenuExpBar:IsShown()` is false and
the stack stays put. Both are the ref's own arithmetic.
- 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:1 — the tooltip/range re-check cadence. It lived in our ActionBar.xml,
-- beside the range timer that reads it; the reference's own `BuffFrame.lua:32` reads it too
-- and, now that BuffFrame is the reference's own file (1751), a kit can want the buff row
-- without wanting the action bars. So it moves to the reference's home, one line above the
-- constant below it.
TOOLTIP_UPDATE_TIME = 0.2
-- 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
-- ── The login seed for the extra action bars ──────────────────────────────────────────
--
-- The four extra bars' visibility is stored SERVER-SIDE, in `PLAYER_FIELD_BYTES` byte 2,
-- and the client never writes that byte itself — `SetActionBarToggles` only posts
-- `CMSG_SET_ACTIONBAR_TOGGLES` and waits for the field to come back (wow-re
-- `system/ui/scratch/action-bar-toggles.md` §5: the setter's every store is into its own
-- stack frame, and nothing in the binary fires on a change to that byte). So the live UI
-- state is the four Lua globals `MultiBars.xml` owns, and `GetActionBarToggles()` is read
-- exactly ONCE, here, to seed them. Reading it again later would fight the optimistic
-- value the options rows just set and lose a round trip's worth of clicks.
--
-- This is the reference's own seam, arm for arm (its UIParent_OnEvent does the same two
-- statements on the same event). `MultiActionBar_Update` is existence-guarded because the
-- ~25 per-window harnesses that load this file load no bars; the binding itself is engine
-- level and always installed. Decision 1500.
-- ── ITEM_QUALITY_COLORS (ref UIParent.lua l.65-72) ───────────────────────────────
-- Its reference home is THIS file. It lived in our own `Fonts.xml` until 1888 put
-- that file on the chain — the reference's `Fonts.xml` does not declare it, so the
-- table had to come back here or vanish. Every reader is below UIParent.xml in the
-- manifest (GameTooltip 169, UnitPopup 203, AuctionFrame 928; UnitPopup's is a
-- FILE-SCOPE read inside a table literal, which is the one that makes the order
-- load-bearing rather than incidental).
-- The item-quality palette — **built from the engine verb, exactly as the reference
-- builds it** (ref UIParent.lua l.65-72, verbatim shape; decision 1199):
--
-- ITEM_QUALITY_COLORS = { }
-- for i = -1, 6 do … = GetItemQualityColor(i) end
--
-- It used to be seven hand-typed rows here. They were CLOSE and not exact — 0.62 where
-- the client's byte is 0x9d/255 = 0.6157, 0.64/0.21/0.93 where Epic is
-- 0xa3/0x35/0xee — and they carried no `.hex`, which the reference's table has and
-- which addons splice straight into a chat link. `GetItemQualityColor` is now a real
-- engine binding over wow-5875-re RF-0055's byte-verified table, so the rounding and
-- the second source both go.
--
-- The loop starts at -1 like the reference's, and the -1 row is not a curiosity: it is the
-- colour a loot row wears while its item template is uncached, which is the value
-- `GetLootSlotInfo` answers there. `GetItemQualityColor`'s clamp is UNSIGNED, so -1 lands
-- on Common/white with 7-and-up (decision 1805, correcting 1199's "reads before the
-- array").
ITEM_QUALITY_COLORS = { }
for i = -1, 6 do
ITEM_QUALITY_COLORS[i] = { }
ITEM_QUALITY_COLORS[i].r,
ITEM_QUALITY_COLORS[i].g,
ITEM_QUALITY_COLORS[i].b,
ITEM_QUALITY_COLORS[i].hex = GetItemQualityColor(i)
end
-- ── UIParentLoadAddOn (ref UIParent.lua l.638-647, and now our slot for it) ──────
-- It lived in our own BasicControls.xml until 1881 migrated that file to the chain;
-- the reference never put it there, so the move is back to where it belongs.
-- The on-demand loader every "open the auction house / tradeskill / talent panel" path
-- goes through, and the one 5 corpus addons name — FuBar_MCPFu, Fubar_RareTrackerFu, MCP,
-- MikScrollingBattleText, SpecialTalent: five separate addons, not one library replicated
-- (1207/1240).
--
-- Every piece was already here and only the join was missing: `LoadAddOn` answers the
-- reference's own `(1, nil)` / `(nil, reason)` pair, `message` and `TEXT` are directly
-- above, and the `ADDON_*` reason words come from the real `GlobalStrings.lua` we RUN off
-- the patch chain rather than copy (1234).
--
-- **`FailedAddOnLoad` is the reference's own latch, and it is not decoration.** Without it
-- an addon that retries the load from a handler raises a modal on every attempt; with it
-- the failure shows once per addon name per session. It is exactly the detail this would
-- have lacked had it been reconstructed from the name instead of read.
--
-- Every reason our `LoadAddOn` can answer resolves to a string: MISSING, DISABLED,
-- NOT_DEMAND_LOADED, DEP_MISSING and DEP_DISABLED all have an `ADDON_..reason` word
-- (GlobalStrings l.16-31), the same reason set wow-re's `ui.md` l.2396 carves off
-- `0x51e780`. So the `getglobal` below cannot hand `format` a nil.
FailedAddOnLoad = {};
function UIParentLoadAddOn(name)
local loaded, reason = LoadAddOn(name)
if not loaded then
if not FailedAddOnLoad[name] then
message(format(TEXT(ADDON_LOAD_FAILED), name, TEXT(getglobal("ADDON_" .. reason))))
FailedAddOnLoad[name] = true
end
end
return loaded
end
-- ref UIParent.lua:223, whose slot this is — immediately above `UIParent_OnEvent`, the
-- same neighbour it has there. It moved here from our own InspectFrame.xml when 1832
-- retired that file: the name belongs to UIParent in the reference, and the window it
-- opens is now the stock LoadOnDemand `Blizzard_InspectUI` off the player's chain.
--
-- The reference's body is `InspectFrame_LoadUI()` and then a guarded `InspectFrame_Show`.
-- We have no load-on-demand — benilla.toc loads that addon with everything else — so the
-- LoadUI call has nothing to do and is left out rather than stubbed (1203). The guard is
-- kept exactly as the reference writes it, and it is not decoration: it is what makes this
-- safe in the ~25 per-window harnesses that load UIParent.xml without the inspect window.
function InspectUnit(unit)
if ( InspectFrame_Show ) then
InspectFrame_Show(unit);
end
end
-- ref UIParent.lua's own `ToggleTalentFrame`, moved here from our TalentFrame.xml when
-- 1833 retired that file — the same move `InspectUnit` made just above, for the same
-- reason: the reference keeps the opener in UIParent and the window in a LoadOnDemand
-- addon, so deleting our window file would have taken the name with it.
--
-- The LEVEL GATE is the reference's, and it is real behaviour rather than a guard: below
-- level 10 the key and the micro button do nothing at all. `TalentFrame_LoadUI` is left
-- out (we load the addon eagerly, 1203 — build it or leave it out, never stub it); the
-- `TalentFrame_Toggle` guard is kept verbatim, and it is what lets the ~25 harnesses that
-- load UIParent.xml without the talent window do so safely.
function ToggleTalentFrame()
if ( UnitLevel("player") < 10 ) then
return;
end
if ( TalentFrame_Toggle ) then
TalentFrame_Toggle();
end
end
-- ref UIParent.lua l.194-196, verbatim — the fifth squatter, and the smallest. Stock
-- `HelpFrame.xml` calls it when a GM survey is waiting (its l.1487), and its whole body is
-- `UIParentLoadAddOn("Blizzard_GMSurveyUI")` — a verb 1881 already moved here and an addon
-- that really is on the player's chain (patch-2.MPQ). So this is the reference's own
-- function, not a stand-in: 1889 needed it because the stock help window is on the chain
-- now and ours is gone.
function GMSurveyFrame_LoadUI()
UIParentLoadAddOn("Blizzard_GMSurveyUI")
end
-- ref UIParent.lua's own `ShowMacroFrame`, moved here from our MacroFrame.xml when 1848
-- retired that file — the third such squatter after `InspectUnit` (1832) and
-- `ToggleTalentFrame` (1833), and by now the expected shape.
--
-- `MacroFrame_LoadUI` is left out: the addon loads eagerly from benilla.toc, so there is
-- nothing for it to do, and 1203 says leave it out rather than stub it. The
-- `MacroFrame_Show` guard is the reference's own.
function ShowMacroFrame()
if ( MacroFrame_Show ) then
MacroFrame_Show();
end
end
-- ref UIParent.lua's own `RaidFrame_LoadUI` (l.182-184), verbatim — the fourth squatter,
-- after `InspectUnit` (1832), `ToggleTalentFrame` (1833) and `ShowMacroFrame` (1848).
--
-- **Unlike `MacroFrame_LoadUI`, this one cannot be left out.** That one was omitted because
-- nothing on the chain calls it; the reference's own `RaidFrame.lua` calls THIS on
-- `RAID_ROSTER_UPDATE` and again at `PLAYER_LOGIN` with a raid up (l.34, l.39), so its
-- absence is a raise every time the roster moves. It is a FrameXML function, not an engine
-- verb, and this file is our stand-in for `UIParent.lua` — the same relationship the three
-- above have. `UIParentLoadAddOn` is just above, and it no-ops on an addon that is
-- already loaded, which ours is: benilla.toc takes `Blizzard_RaidUI` eagerly (1874).
function RaidFrame_LoadUI()
UIParentLoadAddOn("Blizzard_RaidUI");
end
function UIParent_OnEvent(event)
-- The reference's own first arm (UIParent.lua l.230-235). `LocalizeFrames` comes from
-- the sourced `Interface\FrameXML\Localization.xml` (1751 window 5) and is the file
-- that sets `TwentyFourHourTime` and the locale's frame adjustments; nothing called it
-- here before, which is why GameTime.xml used to set that global itself and why
-- HelpFrame.xml's header records a family of strings that never arrived. Guarded like
-- every other cross-file reach in this function: the per-window harnesses load
-- UIParent.xml with no chain behind it. `UpdateNameplates` is the ref's other call in
-- this arm and is not bound here — left absent rather than stubbed (1203).
if ( event == "VARIABLES_LOADED" ) then
if ( LocalizeFrames ) then
LocalizeFrames()
end
return
end
-- The mirror bars' START driver (ref UIParent.lua l.97 + l.374-377). It lived on a
-- hidden `MirrorTimerDriver` frame in our own MirrorTimer.xml until 1751 window 7,
-- on the reasoning that "our UIParent.xml is script-only — it has no frame of its own
-- to register events on". That stopped being true when this file grew a real UIParent
-- frame with a Scripts block, so the driver came home and the extra frame is gone.
-- `MirrorTimer_Show` is the sourced `Interface\FrameXML\MirrorTimer.lua`'s.
if ( event == "MIRROR_TIMER_START" ) then
if ( MirrorTimer_Show ) then
MirrorTimer_Show(arg1, arg2, arg3, arg4, arg5, arg6)
end
return
end
if ( event == "PLAYER_ENTERING_WORLD" ) then
SHOW_MULTI_ACTIONBAR_1, SHOW_MULTI_ACTIONBAR_2, SHOW_MULTI_ACTIONBAR_3,
SHOW_MULTI_ACTIONBAR_4 = GetActionBarToggles()
if ( MultiActionBar_Update ) then
MultiActionBar_Update()
end
end
-- The group-roll opener, ref UIParent.lua l.513-515 verbatim. This arm is why our
-- own GroupLootFrame.xml existed at all: that file's header explained it was carrying
-- a dedicated hidden driver frame *because* "this engine doesn't ship" UIParent's
-- monolithic OnEvent. It does now, so the router comes home to the reference's own
-- slot and the driver goes with the file (decision 1838).
--
-- `GroupLootFrame_OpenNewFrame` and the four frames it walks are the CHAIN's, out of
-- `Interface\FrameXML\LootFrame.xml` and its `.lua` — already on the manifest, and
-- already declaring every name our copy re-declared. Existence-guarded for
-- `MultiActionBar_Update`'s reason, not the reference's: the per-window harnesses load
-- this file with no loot window behind it.
if ( event == "START_LOOT_ROLL" ) then
if ( GroupLootFrame_OpenNewFrame ) then
GroupLootFrame_OpenNewFrame(arg1, arg2)
end
return
end
-- ref UIParent.lua l.517-524, the arm directly below the one above and the second
-- router our GroupLootFrame.xml was carrying. The dialog it raises now lives in
-- UiPanels.xml beside DELETE_ITEM; `arg1`/`arg2` are the roll id and roll type, and
-- `StaticPopup_Show` answering nil (the popup pool being full) is the reference's own
-- guarded case, not ours.
if ( event == "CONFIRM_LOOT_ROLL" ) then
local dialog = StaticPopup_Show("CONFIRM_LOOT_ROLL")
if ( dialog ) then
dialog.data = arg1
dialog.data2 = arg2
end
return
end
-- The ready-check popup's opener (decision 1549), ref UIParent.lua l.570-572. The
-- event is the engine's own `0x218`, fired by the `MSG_RAID_READY_CHECK` open
-- handler; the frame and `ShowReadyCheck` live in RaidFrame.xml, so this is the one
-- place the two meet. Existence-guarded for `MultiActionBar_Update`'s reason: the
-- per-window harnesses load this file without the raid pane.
if ( event == "READY_CHECK" ) then
if ( ShowReadyCheck ) then
ShowReadyCheck()
end
end
end
-- ── Everything that has to move when the bottom of the screen fills up ──────────────────
--
-- One row per thing whose seat depends on which bars are raised. A row reads:
--
-- baseY where it sits on the y axis with nothing in the way
-- baseX / xOffset the same for the x axis, under two names: the rows that write a var
-- spell it `baseX`, the rows that seat a frame spell it `xOffset`, and
-- baseX wins if a row ever names both
-- bottomLeft, bottomRight, bottomEither, pet, reputation, maxLevel (y axis)
-- rightLeft, rightRight (x axis)
-- what this row owes each raised bar, ADDED for every flag standing
-- anchorTo / point / rpoint who to hang off; defaulted to UIParent BOTTOM-to-BOTTOM
-- isVar the row is not a frame at all: it names a GLOBAL, and the pass writes
-- the computed offset into it for other files to read
--
-- The KEY is the global name — a frame to seat, or the var to write — and other windows'
-- OnShow/OnHide drive the pass by it, so both the keys and the fields are a published
-- shape. Written as one constructor rather than a dozen index assignments: it is a
-- constant, and a constant reads better as a table than as a program that builds one.
UIPARENT_MANAGED_FRAME_POSITIONS = {
-- Frames to seat
MultiBarBottomLeft = {baseY = 17, reputation = 9, maxLevel = -5, anchorTo = "ActionButton1", point = "BOTTOMLEFT", rpoint = "TOPLEFT"},
GroupLootFrame1 = {baseY = 60, bottomEither = 42, pet = 42, reputation = 9},
TutorialFrameParent = {baseY = 55, bottomEither = 47, pet = 42, reputation = 9},
FramerateLabel = {baseY = 64, bottomEither = 42, pet = 42, reputation = 9},
CastingBarFrame = {baseY = 60, bottomEither = 40, pet = 40, reputation = 9},
ChatFrame1 = {baseY = 85, bottomLeft = 17, pet = 17, reputation = 9, maxLevel = -5, anchorTo = "UIParent", point = "BOTTOMLEFT", rpoint = "BOTTOMLEFT", xOffset = 32},
-- ChatFrame2 is NOT managed here, and its absence is the fix for the latent
-- decision 1499 flagged in the ClearAllPoints comment below. The reference's row
-- seats an UNDOCKED ChatFrame2 against the screen's bottom-right; ours is DOCKED to
-- ChatFrame1, and a docked frame takes its rect from the dock, never from this pass.
-- With the row present the pass cleared ChatFrame2's authored anchors and re-seated it
-- with a single BOTTOMRIGHT point and no size — an unresolvable rect, so the window
-- rendered nothing at all. That was invisible while ChatFrame2 had no tab; the moment
-- one existed (1575) it became "the Combat Log tab shows an empty screen".
-- Re-add this row the day windows can be undocked, which is chat *settings* (0288 §2).
ShapeshiftBarFrame = {baseY = 0, bottomLeft = 45, reputation = 9, maxLevel = -5, anchorTo = "MainMenuBar", point = "BOTTOMLEFT", rpoint = "TOPLEFT", xOffset = 30},
-- Globals to write
CONTAINER_OFFSET_X = {baseX = 0, rightLeft = 90, rightRight = 45, isVar = "xAxis"},
CONTAINER_OFFSET_Y = {baseY = 70, bottomEither = 27, bottomRight = 0, reputation = 9, isVar = "yAxis", pet = 23},
BATTLEFIELD_TAB_OFFSET_Y = {baseY = 210, bottomRight = 40, reputation = 9, isVar = "yAxis"},
PETACTIONBAR_YPOS = {baseY = 97, bottomLeft = 43, reputation = 9, maxLevel = -5, isVar = "yAxis"},
}
-- ── Consumers of the VAR rows ──────────────────────────────────────────────────────────
-- A row with `isVar` leaves a NUMBER in a global and anchors nothing, so whoever seats
-- itself from that number has to be told when the number changes — otherwise it keeps the
-- seat it computed the first time and the bars grow underneath it. That is not a
-- hypothetical: the open-bag stack read a file-local copy of CONTAINER_OFFSET_X/Y and sat
-- at the no-bars corner forever, so a bottom multibar drew straight through the lowest bag
-- window (director-caught, decision 1499).
--
-- The reference closes the same loop by hard-calling `updateContainerFrameAnchors()` from
-- the end of its own pass (UIParent.lua:1774). This is that call generalized: a consumer
-- registers itself and the pass calls it after every recompute, so the NEXT var consumer
-- does not have to edit this file to be noticed. Order of registration is the order of
-- call; listeners are independent (nothing here reads what another writes).
--
-- The law, stated once so it can be pointed at: **nothing outside this file may keep its
-- own copy of a managed offset.** Read the global fresh at use, and if you cache a SEAT
-- derived from it, register here. `bottom_hud_tests.rs` enforces both halves.
UIPARENT_MANAGED_POSITION_LISTENERS = {}
function UIParent_RegisterManagedPositionListener(fn)
table.insert(UIPARENT_MANAGED_POSITION_LISTENERS, fn)
end
-- ── The model-pane rotation handlers (ref UIParent.lua l.1421-1462) ──────────────────
--
-- `Model_OnLoad` / `Model_RotateLeft` / `Model_RotateRight` / `Model_OnUpdate` — the four
-- the reference's dress-up room, pet paper doll, stable and character sheet all drive their
-- turntable with. Verbatim, in this file because this file is our `UIParent.lua`.
--
-- Everything they reach for is already here: `ROTATIONS_PER_SECOND` at the top of this
-- file (the reference puts it on its own line 2), `PI` as an engine global, and
-- `SetRotation` on the model panes that chain through `PlayerModel`
-- (`script/modelframe.rs` — a plain `<Model>` does NOT acquire it, and neither does the
-- reference's).
--
-- **What this does and does not buy, said plainly.** It removes these four names from the
-- gap list of `DressUpFrame.xml`, `PetPaperDollFrame.xml`, `PetStable.xml` and
-- `PaperDollFrame.xml`, and it makes the rotation STATE correct — the angle the API reads
-- back is the angle the reference would have. It does not make a model appear: this engine
-- holds the model pane's scene and draws no pixels into it, which is the one gap those four
-- windows still stand behind. Correct state under an unrendered pane is worth having on its
-- own — it is what an addon reading `model.rotation` gets — and it is not a stub, because
-- nothing here pretends the pane is drawn.
--
-- The two button-driven arms read `<name>RotateLeftButton`/`RotateRightButton` by
-- concatenation, so they work for any pane that follows the reference's naming and raise
-- loudly for one that does not. That is the reference's own coupling, not ours.
function Model_OnLoad()
this.rotation = 0.61
this:SetRotation(this.rotation)
end
function Model_RotateLeft(model, rotationIncrement)
if ( not rotationIncrement ) then
rotationIncrement = 0.03
end
model.rotation = model.rotation - rotationIncrement
model:SetRotation(model.rotation)
PlaySound("igInventoryRotateCharacter")
end
function Model_RotateRight(model, rotationIncrement)
if ( not rotationIncrement ) then
rotationIncrement = 0.03
end
model.rotation = model.rotation + rotationIncrement
model:SetRotation(model.rotation)
PlaySound("igInventoryRotateCharacter")
end
function Model_OnUpdate(elapsedTime, model, rotationsPerSecond)
if ( not rotationsPerSecond ) then
rotationsPerSecond = ROTATIONS_PER_SECOND
end
if ( getglobal(model:GetName() .. "RotateLeftButton"):GetButtonState() == "PUSHED" ) then
model.rotation = model.rotation + (elapsedTime * 2 * PI * rotationsPerSecond)
if ( model.rotation < 0 ) then
model.rotation = model.rotation + (2 * PI)
end
model:SetRotation(model.rotation)
end
if ( getglobal(model:GetName() .. "RotateRightButton"):GetButtonState() == "PUSHED" ) then
model.rotation = model.rotation - (elapsedTime * 2 * PI * rotationsPerSecond)
if ( model.rotation > (2 * PI) ) then
model.rotation = model.rotation - (2 * PI)
end
model:SetRotation(model.rotation)
end
end
-- ── SecondsToTimeAbbrev (ref UIParent.lua l.1034-1049) ───────────────────────────────
--
-- "2 m", "45 s" — the one-letter duration the buff bar prints under a timed aura. It lived
-- in our BuffFrame.xml behind an `if not SecondsToTimeAbbrev` guard, with the four
-- GlobalStrings it formats defined there too, on the reasoning that "we load no
-- UIParent/GlobalStrings" — which stopped being true a long time ago and is why the guard
-- reads as a leftover rather than a decision. It belongs in this file because it belongs
-- in `UIParent.lua`, and 1751's eighteenth window made the buff bar the reference's own,
-- which reaches for it by name and defines nothing.
--
-- Verbatim, including the `>` rather than `>=` on all three boundaries: exactly 60 seconds
-- prints "60 s", not "1 m". That is the reference's own arithmetic.
function SecondsToTimeAbbrev(seconds)
local tempTime
if ( seconds > 86400 ) then
tempTime = ceil(seconds / 86400)
return format(DAY_ONELETTER_ABBR, tempTime)
end
if ( seconds > 3600 ) then
tempTime = ceil(seconds / 3600)
return format(HOUR_ONELETTER_ABBR, tempTime)
end
if ( seconds > 60 ) then
tempTime = ceil(seconds / 60)
return format(MINUTE_ONELETTER_ABBR, tempTime)
end
return format(SECOND_ONELETTER_ABBR, seconds)
end
-- ── GetMaterialTextColors (ref UIParent.lua l.1880-1888) ─────────────────────────────
--
-- The page ink and the title ink for a readable's material. Transcribed here rather than
-- left absent because the sourced `Interface\FrameXML\ItemTextFrame.xml` (1751 window 9)
-- calls it, and it belongs in this file because it belongs in `UIParent.lua`. The two
-- tables it reads are the reference's own, in the reference's own home — our Fonts.xml,
-- which grew them with the same window.
--
-- The fallback is BOTH-or-neither on purpose (the reference's own `not(a and b)`), so an
-- unrecognised material never gets one table's colour and the other's default.
function GetMaterialTextColors(material)
local textColor = MATERIAL_TEXT_COLOR_TABLE[material]
local titleColor = MATERIAL_TITLETEXT_COLOR_TABLE[material]
if ( not (textColor and titleColor) ) then
textColor = MATERIAL_TEXT_COLOR_TABLE["Default"]
titleColor = MATERIAL_TITLETEXT_COLOR_TABLE["Default"]
end
return textColor, titleColor
end
-- ── ValidateFramePosition (ref UIParent.lua l.1513-1557) ─────────────────────────────
--
-- Shove a frame the player has dragged mostly off the screen back on. Transcribed here
-- rather than left absent because its one caller in 1.12 is the sourced
-- `Interface\FrameXML\ItemRef.xml`'s OnDragStop (l.48) — so with that window migrated
-- (1751, window 6) a player dragging a chat-link tooltip would otherwise hit a nil global,
-- which is a raise on an ordinary gesture rather than a loud missing feature. It belongs
-- in this file because it belongs in `UIParent.lua`, which this file is our copy of.
--
-- Verbatim, quirks included. Two are worth naming so nobody "fixes" them later:
-- · the two axes are exclusive, not additive — a frame off BOTH corners takes the
-- `not newAnchorX` branch and re-derives x from `left`, so only one axis moves per
-- drop. That is the reference's own control flow (l.1544-1548), not a transcription
-- slip.
-- · the bottom test is against `MainMenuBar:GetHeight()`, so this raises with no action
-- bar loaded. The reference has the same dependency; the harnesses that drive
-- ItemRefTooltip load ActionBar.xml for it.
function ValidateFramePosition(frame, offscreenPadding, returnOffscreen)
if ( not frame ) then
return
end
local left = frame:GetLeft()
local right = frame:GetRight()
local top = frame:GetTop()
local bottom = frame:GetBottom()
local newAnchorX, newAnchorY
if ( not offscreenPadding ) then
offscreenPadding = 15
end
if ( top < (0 + MainMenuBar:GetHeight() + offscreenPadding) ) then
-- Off the bottom of the screen
newAnchorY = MainMenuBar:GetHeight() + frame:GetHeight() - GetScreenHeight()
elseif ( bottom > GetScreenHeight() ) then
-- Off the top of the screen
newAnchorY = 0
end
if ( right < 0 ) then
-- Off the left of the screen
newAnchorX = 0
elseif ( left > GetScreenWidth() ) then
-- Off the right of the screen
newAnchorX = GetScreenWidth() - frame:GetWidth()
end
if ( newAnchorX or newAnchorY ) then
if ( returnOffscreen ) then
return 1
else
if ( not newAnchorX ) then
newAnchorX = left
elseif ( not newAnchorY ) then
newAnchorY = top - GetScreenHeight()
end
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", nil, "TOPLEFT", newAnchorX, newAnchorY)
end
else
if ( returnOffscreen ) then
return nil
end
end
end
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
-- Walk the rows and settle each one against the flags now standing. Rows are
-- independent — nothing here reads a var another row writes — so the arbitrary order
-- `pairs` hands back is not a hazard.
for index, row in pairs(UIPARENT_MANAGED_FRAME_POSITIONS) do
local frame = getglobal(index)
if ( frame ) then
-- Start from where the row sits with nothing in its way. A zero base is a real
-- base, not an absent one, which is why `or` chains do the right thing here.
local xOffset = row.baseX or row.xOffset or 0
local yOffset = row.baseY or 0
-- Then add what the row owes each raised bar, keeping note of which ones it
-- actually paid — two of them interact below.
local paid = {}
for _, flag in pairs(yOffsetFrames) do
if ( row[flag] ) then
yOffset = yOffset + row[flag]
paid[flag] = true
end
end
for _, flag in pairs(xOffsetFrames) do
if ( row[flag] ) then
xOffset = xOffset + row[flag]
end
end
-- The bottom-left bar and the pet/stance row do not simply stack: clearing both
-- costs 23px more than the two figures added together. The reference carries the
-- same correction (l.1657-1658) with no derivation, and it is a fact about the
-- bars' art rather than arithmetic we can recompute.
if ( paid.bottomLeft and paid.pet ) then
yOffset = yOffset + 23
end
-- A row either NAMES A GLOBAL to leave the number in, or is a frame to seat.
if ( row.isVar == "xAxis" ) then
_G[index] = xOffset
elseif ( row.isVar ) then
_G[index] = yOffset
elseif ( frame.IsUserPlaced and frame:IsUserPlaced() ) then
-- **A frame the PLAYER placed is not this pass's to seat.** benilla's rule,
-- not the reference's: 1.12 has no *movable* managed frame — its ChatFrame1
-- is docked and its tab click refuses to drag a docked window — so the case
-- cannot arise there. Ours does, since an unlocked chat window drags and
-- resizes, and without this the next pass would `ClearAllPoints` the window
-- and drop it back on the bottom-stack seat. That is not a rare path: the
-- stance bar fires this on every show and hide.
--
-- `IsUserPlaced` is exactly the right question rather than a chat-specific
-- one: the engine sets that bit at the drag that moved the frame, and the
-- layout cache (`benilla_app::ui_layout`) sets it again for every frame it
-- seats out of the player's file — so "the player owns this frame's
-- position" and "do not manage it" are the same fact.
--
-- The `isVar` rows above are untouched: they write a NUMBER and seat no
-- frame, so there is no position for a player to own.
else
-- ClearAllPoints first: the row states the frame's WHOLE seat, so the pass
-- must own it outright. `SetPoint` only replaces the same-NAMED point, so
-- any other anchor the frame was authored with survives and fights this
-- one — ChatFrame2 is declared in XML with both a TOPLEFT and a
-- BOTTOMRIGHT onto ChatFrame1, and the row's BOTTOMRIGHT replaced only
-- the second, leaving the pane stretched from ChatFrame1's top-left
-- corner to the screen's bottom-right. Masked today only because
-- ChatFrame2 ships hidden, which is the worst kind of latent (decision
-- 1499). Every row here supplies a single complete anchor, so this is a
-- no-op for the other eight and the reference's plain SetPoint is
-- behaviour-identical on its own frames.
frame:ClearAllPoints()
frame:SetPoint(row.point or "BOTTOM", row.anchorTo or "UIParent",
row.rpoint or "BOTTOM", 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
-- Last: everything that seats itself from a var row this pass just rewrote. After the
-- frames, so a listener may measure one (the bag stack reads GetScreenHeight and its
-- own windows' heights, not another managed frame — but the ordering costs nothing and
-- removes a class of question).
for _, fn in ipairs(UIPARENT_MANAGED_POSITION_LISTENERS) do
fn()
end
-- …and the reference's own consumer, at the reference's own position
-- (UIParent.lua:1774). The open-bag stack re-anchors from here because the bags are
-- the reference's `ContainerFrame1..12` now (decision 1751) and this is the line that
-- used to be generalized into the listener table above — 1499's regression, closed the
-- way the real client closes it. Guarded because a machine with no client data does
-- not get `ContainerFrame.lua` at all, and then there is no bag stack to anchor.
if updateContainerFrameAnchors then
updateContainerFrameAnchors()
end
end
-- The published binding-label API — benilla's implementation of the behaviour ref
-- UIParent.lua:1819-1878 specifies (decision 1008, which closed 0997's "abbreviates
-- through KEY_ABBR" residue with the REAL mechanism: there never was a KEY_ABBR set, the
-- whole abbreviation is this one function). Addons call it by name, so the contract is:
--
-- * a nil name reads as the empty string;
-- * "CTRL-SHIFT-2" splits at its LAST dash — up to and including that dash is the
-- modifier run ("CTRL-SHIFT-"), what follows is the key ("2"). A name with no dash is
-- all key;
-- * the key renders through the `prefix..key` global (the KEY_* family), preferring a
-- `_MAC` twin on a Mac client, and falls back to the raw key name;
-- * returnAbbr — what the action bar's HotKey corners pass — shrinks ONE modifier to
-- its initial, so SHIFT-2 reads "s-2" (the director's ask), and collapses TWO OR MORE
-- to "·". The full form (tooltips, the Keybindings page) keeps the whole prefix.
--
-- Two knowing departures from the reference, both about its code rather than its
-- behaviour: its `gsub(tempName, "CAPSLOCK", "Caps")` line is dead — gsub returns a new
-- string and that result is discarded, so nothing can observe it — and has no counterpart
-- here; and its GetLocale()/IsMacClient() calls ride behind existence guards because this
-- VM defines neither yet, with the getglobal fallback covering the gap.
-- What a single modifier shrinks to on a button corner. German calls Control "STRG" (the
-- deDE swap below runs first), which is the one that needs two letters to stay legible.
-- The order is fixed so the pass is deterministic, but it does not matter: no abbreviation
-- occurs inside another modifier's name, so no substitution can feed the next.
local MODIFIER_ABBREVIATIONS = {
{ "CTRL", "c" },
{ "SHIFT", "s" },
{ "ALT", "a" },
{ "STRG", "st" },
}
function GetBindingText(name, prefix, returnAbbr)
if not name then
return ""
end
local modifiers, key = "", name
local modifierCount = 0
-- The greedy `.*` walks to the LAST dash, which is the one that divides the modifier
-- run from the key; gsub's second return then counts the dashes, one per modifier —
-- plus one more when the key ITSELF is a dash ("CTRL--"), which is why that binding
-- abbreviates as if it had two. The reference's dash-walking loop reaches both of the
-- same answers, and both are pinned by keybindings_tests.
local _, lastDash = strfind(name, "^.*%-")
if lastDash then
modifiers = strsub(name, 1, lastDash)
key = strsub(name, lastDash + 1)
_, modifierCount = gsub(modifiers, "%-", "-")
if GetLocale and GetLocale() == "deDE" then
modifiers = gsub(modifiers, "CTRL", "STRG")
end
end
if returnAbbr then
if modifierCount > 1 then
-- Two modifiers never fit a corner: the whole label becomes the dot.
return "·"
end
for _, abbreviation in ipairs(MODIFIER_ABBREVIATIONS) do
modifiers = gsub(modifiers, abbreviation[1], abbreviation[2])
end
end
prefix = prefix or ""
local label
if IsMacClient and IsMacClient() then
label = getglobal(prefix .. key .. "_MAC")
end
label = label or getglobal(prefix .. key) or key
return modifiers .. label
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
-- ── The 1024x768 scale ratios (ref UIParent.lua l.1794-1802) ────────────────────────────
--
-- Two FrameXML helpers addons call by name (the raid pullouts and a dozen third-party
-- drag handlers), so they exist for the reason `MouseIsOver` and `RaiseFrameLevel` do:
-- missing, the caller dies on `attempt to call global`. The reference's constants are the
-- 1024x768 reference resolution, and its own callers multiply a `GetCursorPosition()`
-- reading by them to convert into pixel space.
--
-- **benilla's own drag code does NOT use them**, and that is deliberate rather than an
-- oversight: our UI space is 1:1 logical px and `GetCursorPosition` already reports in
-- the same space `SetPoint` offsets are read in (the same identity `MouseIsOver`'s
-- comment below records for `GetEffectiveScale`). Multiplying here would place a dragged
-- frame at 1.5x the cursor on a 1600-wide window. Kept for the addons, unused by us.
function GetScreenHeightScale()
return GetScreenHeight() / 768
end
function GetScreenWidthScale()
return GetScreenWidth() / 1024
end
-- ── MouseIsOver: is the cursor inside this frame, with optional per-edge padding ─────────
--
-- The idiom every hover-driven addon writes, and **9 corpus addons call it** by name.
-- Missing, each is `attempt to call global 'MouseIsOver'`. So the CONTRACT is 1.12's, quirks
-- and all (ref UIParent.lua l.1388-1417) — three of which look like bugs and are not:
--
-- · each offset is ADDED to its own edge, in that axis's positive direction. A positive
-- `topOffset` raises the ceiling (growing the box upward) while a positive
-- `bottomOffset` raises the floor (shrinking it from below). The reference's own caller
-- proves the intent: `MouseIsOver(chatFrame, 45, -10, -5, 5)` is 45 up into the tab
-- strip, 10 in from the bottom, 5 out each side.
-- · the padding is ALL OR NOTHING on `topOffset`: pass none and the other three are
-- dropped with it, so `MouseIsOver(f, nil, 5)` silently ignores the 5. Pass a topOffset
-- and omit any of the rest and it is an arithmetic error, not a zero — 1.12 raises
-- there too, and an addon written against it may lean on either half.
-- · the bounds are strict, so a cursor exactly on an edge is outside.
--
-- A frame that has never been laid out answers nil for its rect; nil is the right answer to
-- hand back for it, and the reference's own comment on that guard ("a symptom not the real
-- issue") is about where the unresolved frame came from, not about the guard.
--
-- 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, right = frame:GetLeft(), frame:GetRight()
local top, bottom = frame:GetTop(), frame:GetBottom()
if not left then
return
end
if not topOffset then
topOffset, bottomOffset, leftOffset, rightOffset = 0, 0, 0, 0
end
-- All four edges are padded before any of them is tested, and deliberately so: folding
-- the additions into the comparison would let a short circuit swallow the arithmetic
-- error that a half-supplied offset set is supposed to raise.
left, right = left + leftOffset, right + rightOffset
top, bottom = top + topOffset, bottom + bottomOffset
if x > left and x < right and y > bottom and y < top then
return 1
end
return nil
end
]]></Script>
</Ui>