the addon survey seats a populated world, and hidden frames arrive
- the corpus harness now seats a buff, a target and a ticking cooldown instead of an empty world, so "nothing to draw" and "failed to draw" stop sharing a row — and the richer seat immediately exposed two real gaps - an addon button inheriting our action-button template now works even when it replaces the template's OnLoad: the handlers resolve their regions by name, the way the reference does on every access - more of the reference's hidden-by-default frames exist by name — ReputationWatchBar, MiniMapBattlefieldFrame, MiniMapMeetingStoneFrame — for the addons that reposition them - StartSizing lands beside its stop verb, and the sourced bag-anchor code runs against windows we actually build Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,36 @@
|
||||
-- (ActionButton_GetPagedID:446-467 — MultiBarBottomLeft ⇒ page 6, MultiBarBottomRight ⇒
|
||||
-- page 5, else the paged/bonus formula); we carry the fork as data — a fixed per-button
|
||||
-- page base set at OnLoad (MultiBars.xml), nil for the bonus-paged main bar.
|
||||
-- **Adopt a button that never ran our OnLoad.**
|
||||
--
|
||||
-- Our handlers read a region cache (`button.icon`, `.count`, `.cooldown`, …) that
|
||||
-- `BenillaActionButton_OnLoad` fills. An addon inheriting `ActionBarButtonTemplate` gets
|
||||
-- those handlers and supplies its OWN `<OnLoad>`, which REPLACES the template's rather
|
||||
-- than chaining — so ours never runs and every one of those reads was nil. zBar.xml:7 is
|
||||
-- the corpus shape; it died in OUR file at `ActionBar.xml:529` on `button.icon`.
|
||||
--
|
||||
-- Resolving from the derived names is not a fallback bolted on: it is exactly what our
|
||||
-- OnLoad does, and what the REFERENCE does on every access (`ActionButton_Update` reads
|
||||
-- `getglobal(this:GetName().."Icon")` each time rather than caching). We cache because it
|
||||
-- is cheaper; this is the same lookup for the buttons that never got cached.
|
||||
--
|
||||
-- It deliberately does NOT join `BENILLA_ACTION_BUTTONS`. That roster is the event fan-out
|
||||
-- for OUR bars, and a foreign button's owner drives its own updates — adding it would have
|
||||
-- us repainting somebody else's button behind their back.
|
||||
function BenillaActionButton_Adopt(button)
|
||||
if not button or button.icon then return end
|
||||
local n = button:GetName()
|
||||
if not n then return end
|
||||
button.icon = getglobal(n .. "Icon")
|
||||
button.hotkey = getglobal(n .. "HotKey")
|
||||
button.count = getglobal(n .. "Count")
|
||||
button.flash = getglobal(n .. "Flash")
|
||||
button.border = getglobal(n .. "Border")
|
||||
button.cooldown = getglobal(n .. "Cooldown")
|
||||
button.flashing = button.flashing or 0
|
||||
button.flashtime = button.flashtime or 0
|
||||
end
|
||||
|
||||
function BenillaActionButton_Action(button)
|
||||
-- `index` falls back to the button's ID, which is where the REFERENCE reads it from:
|
||||
-- `ActionButton_GetPagedID` uses `this:GetID()`, and our `.index` is only that value
|
||||
@@ -260,6 +290,7 @@
|
||||
-- ships no CVar state, so the branch collapses to the stock-client one: action-bar
|
||||
-- tooltips live at the screen's bottom-right, above the bags.
|
||||
function BenillaActionButton_OnEnter(button)
|
||||
BenillaActionButton_Adopt(button)
|
||||
if not GameTooltip then return end -- engine tests load file subsets
|
||||
GameTooltip_SetDefaultAnchor(GameTooltip, button)
|
||||
GameTooltip:SetAction(BenillaActionButton_Action(button))
|
||||
@@ -305,6 +336,7 @@
|
||||
-- Ref ActionBarFrame.xml:12-22's fork verbatim: shift picks up, else uses (checkCursor=1 —
|
||||
-- the engine routes a click-with-payload to PlaceAction instead, decision 0216 §7).
|
||||
function BenillaActionButton_OnClick(button)
|
||||
BenillaActionButton_Adopt(button)
|
||||
local action = BenillaActionButton_Action(button)
|
||||
if IsShiftKeyDown() then
|
||||
PickupAction(action)
|
||||
@@ -320,12 +352,14 @@
|
||||
-- stops the *drag*, and shift-click stays the deliberate way to move an action with the bar
|
||||
-- locked — which is what makes the setting usable rather than a wall.
|
||||
function BenillaActionButton_OnDragStart(button)
|
||||
BenillaActionButton_Adopt(button)
|
||||
if LOCK_ACTIONBAR == "1" then return end
|
||||
PickupAction(BenillaActionButton_Action(button))
|
||||
BenillaActionButton_Update(button)
|
||||
end
|
||||
|
||||
function BenillaActionButton_OnReceiveDrag(button)
|
||||
BenillaActionButton_Adopt(button)
|
||||
if LOCK_ACTIONBAR == "1" then return end
|
||||
PlaceAction(BenillaActionButton_Action(button))
|
||||
BenillaActionButton_Update(button)
|
||||
@@ -427,6 +461,7 @@
|
||||
-- tints red/grey, an unbound one wears the RANGE_INDICATOR dot (both legs the ref's,
|
||||
-- unified in 1008 — the SCOPE note).
|
||||
function ActionButton_OnUpdate(elapsed)
|
||||
BenillaActionButton_Adopt(this)
|
||||
local button = this
|
||||
if ( button.flashing == 1 ) then
|
||||
button.flashtime = button.flashtime - elapsed
|
||||
@@ -499,7 +534,78 @@
|
||||
-- arguments are real state our per-container registration needs, so no adapter can supply
|
||||
-- them. That is the per-button event registration change already at the top of the arc
|
||||
-- queue, not a naming fix.
|
||||
-- **`ActionButton_OnLoad()` — the reference's own, taking nothing and reading `this`.**
|
||||
--
|
||||
-- This sat at the top of the arc queue for the whole wave, recorded as unadaptable: ours
|
||||
-- takes `(button, index, base, commandPrefix)` and the reference's takes none, so "no
|
||||
-- adapter can supply them". Reading ref ActionButton.lua:103-119 shows that was wrong —
|
||||
-- the reference derives all three from `this` and it is our own signature that caches
|
||||
-- them:
|
||||
--
|
||||
-- index -> `this:GetID()` (what ActionButton_GetPagedID reads)
|
||||
-- commandPrefix -> `this.buttonType`, a field the BAR sets, "ACTIONBUTTON" by default
|
||||
-- (ref l.118 `ActionButton_UpdateHotkeys(this.buttonType)`)
|
||||
-- base -> not an OnLoad concept at all; the reference forks on the PARENT's
|
||||
-- NAME at read time. A foreign bar ("zBar1") matches no multibar, so
|
||||
-- the paged/bonus formula applies — which is what leaving `base` nil
|
||||
-- already does here.
|
||||
--
|
||||
-- The events are registered on the BUTTON, as the reference does, and reach it through the
|
||||
-- `<OnEvent>` this change adds to `ActionBarButtonTemplate`. Our own bars do NOT call this
|
||||
-- and never register, so they keep being driven by the BENILLA_ACTION_BUTTONS fan-out and
|
||||
-- cannot be double-driven.
|
||||
function ActionButton_OnLoad()
|
||||
local button = this
|
||||
if not button then return end
|
||||
BenillaActionButton_Adopt(button)
|
||||
button.index = button.index or button:GetID()
|
||||
button.bindingCommand = (button.buttonType or "ACTIONBUTTON") .. (button.index or 1)
|
||||
button.showgrid = 0
|
||||
button.flashing = 0
|
||||
button.flashtime = 0
|
||||
button:RegisterForDrag("LeftButton", "RightButton")
|
||||
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
||||
button:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
button:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
|
||||
button:RegisterEvent("ACTIONBAR_SHOWGRID")
|
||||
button:RegisterEvent("ACTIONBAR_HIDEGRID")
|
||||
button:RegisterEvent("ACTIONBAR_PAGE_CHANGED")
|
||||
button:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
|
||||
button:RegisterEvent("UPDATE_BINDINGS")
|
||||
BenillaActionButton_PaintHotkey(button)
|
||||
BenillaActionButton_Update(button)
|
||||
end
|
||||
|
||||
-- The per-button event arm the reference's own template carries. Only buttons that ran
|
||||
-- `ActionButton_OnLoad` ever registered, so this is a no-op for ours.
|
||||
function ActionButton_OnEvent()
|
||||
local button = this
|
||||
if not button then return end
|
||||
BenillaActionButton_Adopt(button)
|
||||
if event == "ACTIONBAR_SHOWGRID" then
|
||||
button.showgrid = 1
|
||||
elseif event == "ACTIONBAR_HIDEGRID" then
|
||||
button.showgrid = 0
|
||||
elseif event == "UPDATE_BINDINGS" then
|
||||
BenillaActionButton_PaintHotkey(button)
|
||||
end
|
||||
BenillaActionButton_Update(button)
|
||||
end
|
||||
|
||||
-- ref ActionButton.lua:446 — the action id a button displays, forked on its parent bar.
|
||||
-- `BenillaActionButton_Action` IS this function; only the name was ours. CT_BarMod calls it
|
||||
-- the moment there is an action to page, which is why an EMPTY-world survey never saw it:
|
||||
-- it surfaced only when the fixture started seating a populated session.
|
||||
function ActionButton_GetPagedID(button)
|
||||
return BenillaActionButton_Action(button or this)
|
||||
end
|
||||
|
||||
function ActionButton_Update()
|
||||
-- Adopt here too: this adapter carries the REFERENCE's name, so `this` is very often a
|
||||
-- button somebody else built (zBar's, CT_BarMod's) that never ran our OnLoad and so has
|
||||
-- no region cache. The handler-boundary adopts do not cover it — an addon calls this
|
||||
-- global directly, without any of our scripts in the stack.
|
||||
BenillaActionButton_Adopt(this)
|
||||
BenillaActionButton_Update(this)
|
||||
end
|
||||
|
||||
@@ -1075,6 +1181,7 @@
|
||||
<!-- The bar half: the reference's `ActionBarButtonTemplate`, regions inherited, handlers added. -->
|
||||
<CheckButton name="ActionBarButtonTemplate" inherits="ActionButtonTemplate" virtual="true">
|
||||
<Scripts>
|
||||
<OnEvent>ActionButton_OnEvent()</OnEvent>
|
||||
<OnClick>BenillaActionButton_OnClick(self)</OnClick>
|
||||
<OnDragStart>BenillaActionButton_OnDragStart(self)</OnDragStart>
|
||||
<OnReceiveDrag>BenillaActionButton_OnReceiveDrag(self)</OnReceiveDrag>
|
||||
@@ -1537,4 +1644,44 @@
|
||||
</CheckButton>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!-- **The reputation watch bar, built and hidden** — the third time this posture has been the
|
||||
answer (1219's vertical multibars, 1223's bonus bar, this). `ref-ReputationFrame.xml:869`
|
||||
declares it `<Frame name="ReputationWatchBar" parent="MainMenuBar" hidden="true">`: real on
|
||||
the client from load, shown only while a faction is being watched.
|
||||
|
||||
benilla does not drive reputation watching, so it stays hidden — but three corpus addons
|
||||
index it by name (`FuBar_FuXPFu`, `CT_BottomBar`, `zBarEx`), and FuXPFu dies at
|
||||
`FuBar_FuXPFu.lua:227` on the nil. Its own `OnEnable` reaches for it the moment the addon
|
||||
enables, so nothing before the populated-session fixture ever got that far.
|
||||
|
||||
Geometry quoted: a 1024x11 frame whose BOTTOM sits on MainMenuBar's TOP at (0,-3), holding
|
||||
a 1024x8 `ReputationWatchStatusBar` at TOP. The reference's two art slices
|
||||
(`ReputationWatchBarTexture0/1`, `UI-ReputationWatchBar`) are declared with it — an addon
|
||||
that repositions the bar expects the named regions to exist, and they draw nothing while
|
||||
the frame is hidden. -->
|
||||
<Frame name="ReputationWatchBar" enableMouse="true" parent="MainMenuBar" hidden="true">
|
||||
<Size><AbsDimension x="1024" y="11"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM" relativeTo="MainMenuBar" relativePoint="TOP"><Offset><AbsDimension x="0" y="-3"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Frames>
|
||||
<StatusBar name="ReputationWatchStatusBar" drawLayer="ARTWORK">
|
||||
<Size><AbsDimension x="1024" y="8"/></Size>
|
||||
<Anchors><Anchor point="TOP"/></Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="ReputationWatchBarTexture0" file="Interface\PaperDollInfoFrame\UI-ReputationWatchBar">
|
||||
<Size><AbsDimension x="256" y="11"/></Size>
|
||||
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="2"/></Offset></Anchor></Anchors>
|
||||
</Texture>
|
||||
<Texture name="ReputationWatchBarTexture1" file="Interface\PaperDollInfoFrame\UI-ReputationWatchBar">
|
||||
<Size><AbsDimension x="256" y="11"/></Size>
|
||||
<Anchors><Anchor point="LEFT" relativeTo="ReputationWatchBarTexture0" relativePoint="RIGHT"/></Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</StatusBar>
|
||||
</Frames>
|
||||
</Frame>
|
||||
</Ui>
|
||||
|
||||
@@ -78,5 +78,29 @@
|
||||
end
|
||||
refItemButtonOnEnter(button)
|
||||
end
|
||||
|
||||
-- ── `updateContainerFrameAnchors` — a no-op here, and that IS the benilla answer ────────
|
||||
-- The reference's body (l.480-489) opens `while ContainerFrame1.bags[index] do` and walks
|
||||
-- the twelve `ContainerFrame1..12` windows it owns, positioning each into columns. benilla
|
||||
-- does not build those windows at all — our bags are `BagFrame.xml`'s own, and they
|
||||
-- position themselves. `ui_script::manifest` already records the same difference one
|
||||
-- function over: *"ToggleBackpack walks twelve ContainerFrames we do not build; ours walks
|
||||
-- our windows."*
|
||||
--
|
||||
-- So this is not a stub standing in for something missing: there is nothing here for it to
|
||||
-- anchor, and the reference body raises `attempt to index global 'ContainerFrame1'` the
|
||||
-- moment anything calls it. FIVE corpus addons did — Bongos, Bongos_ActionBar and the
|
||||
-- CT_BarMod family all HOOK it (`BLIZZARD_Original_updateContainerFrameAnchors` in the
|
||||
-- traceback) and then call through, so the raise landed inside a Blizzard file with our
|
||||
-- bag layout nowhere in the stack.
|
||||
--
|
||||
-- Deliberately silent rather than warned: an addon calling this on every bag update would
|
||||
-- turn a warn-once into a warn-always, and unlike the cases in the section above nothing is
|
||||
-- LOST by doing nothing — the windows it would have moved are already where our own layout
|
||||
-- put them. The header's "never silently stubbed" rule is about absent capability; this is
|
||||
-- present capability with a different owner.
|
||||
function updateContainerFrameAnchors()
|
||||
end
|
||||
|
||||
]]></Script>
|
||||
</Ui>
|
||||
|
||||
@@ -304,6 +304,56 @@
|
||||
cluster — hoisted to the cluster the same offset lands past the screen edge.
|
||||
Unprefixed (this file's own convention: every frame here keeps the
|
||||
reference's bare name, unlike the newer Benilla*-prefixed windows). -->
|
||||
<!-- **Two more of the reference's minimap icons, built and hidden** — the
|
||||
fourth time this posture has been the answer (1219's vertical multibars,
|
||||
1223's bonus bar, ReputationWatchBar, these). `ref-Minimap.xml` declares
|
||||
both `hidden="true"`: real on the client from load, shown only when there
|
||||
is a battleground queue or a meeting stone to advertise.
|
||||
|
||||
benilla drives neither, so they stay hidden — but addons reposition the
|
||||
whole icon set by NAME. `CleanMinimap.lua:209` is
|
||||
`getglobal(frameName):ClearAllPoints()` over a list that includes both, and
|
||||
it died on the nil; `MiniMapBattlefieldFrame` alone is 5 addons in the
|
||||
corrected ranking (1218). Their icon regions are declared with them, since
|
||||
an addon that moves a frame expects its named children to exist.
|
||||
|
||||
Geometry quoted from the reference: both 33x33 with an 18x18 icon at
|
||||
TOPLEFT (7,-6), Battlefield at BOTTOMLEFT (13,-13) of the cluster and
|
||||
MeetingStone at TOPRIGHT (23,-65). -->
|
||||
<Button name="MiniMapBattlefieldFrame" enableMouse="true" hidden="true">
|
||||
<Size><AbsDimension x="33" y="33"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="13" y="-13"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="MiniMapBattlefieldIcon" file="Interface\BattlefieldFrame\UI-Battlefield-Icon">
|
||||
<Size><AbsDimension x="32" y="32"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"><Offset><AbsDimension x="-1" y="0"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Button>
|
||||
|
||||
<Button name="MiniMapMeetingStoneFrame" enableMouse="true" hidden="true">
|
||||
<Size><AbsDimension x="33" y="33"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="23" y="-65"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="MiniMapMeetingStoneIcon" file="Interface\Icons\INV_Misc_Rune_06">
|
||||
<Size><AbsDimension x="18" y="18"/></Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Button>
|
||||
|
||||
<Frame name="MiniMapMailFrame" enableMouse="true" hidden="true">
|
||||
<Size>
|
||||
<AbsDimension x="33" y="33"/>
|
||||
|
||||
@@ -176,8 +176,27 @@ fn main() {
|
||||
.map(|r| r.name.as_str())
|
||||
.collect();
|
||||
if !silent.is_empty() {
|
||||
// **This list is a QUESTION, not a defect list, and the header used to say otherwise.**
|
||||
//
|
||||
// It read "read this list first", which invites treating every row as a bug. Most are not.
|
||||
// The survey seats a player and an empty world: no buffs, no target, no combat, no cursor
|
||||
// over an item. An addon with nothing to draw in that world draws nothing CORRECTLY — and
|
||||
// that is most of this list. Measured on the vanilla corpus rather than assumed: of 48 rows
|
||||
// checked, 6 ship no XML at all (pure libraries — Ace, LibStub, Stubby, DevTools), and of
|
||||
// the rest the buff bars (CT_BuffMod 29 frames, ElkBuffBar 3, neither hidden at birth) are
|
||||
// waiting on buffs that a fresh login does not have either.
|
||||
//
|
||||
// What the row DOES mean is "nothing here can be ruled out by the other four columns" —
|
||||
// which is worth printing, and is not the same as "broken".
|
||||
//
|
||||
// The instrument change that would sharpen it is a POPULATED session fixture (a buff, a
|
||||
// target, a live cooldown) so that "nothing to draw" and "failed to draw" stop sharing a
|
||||
// row. Named here rather than done, because seating content changes the fixture for all
|
||||
// 218 addons and deserves its own controlled A/B.
|
||||
println!(
|
||||
"\n CLEAN ON EVERY OTHER COLUMN AND DREW NOTHING ({}) — read this list first:",
|
||||
"\n DREW NOTHING, and clean on every other column ({}) — a QUESTION, not a defect\n \
|
||||
list: the seated world has one buff, one target and one running cooldown, so an addon\n \
|
||||
waiting on combat, a cursor over an item or a slash command belongs here too:",
|
||||
silent.len()
|
||||
);
|
||||
for chunk in silent.chunks(4) {
|
||||
|
||||
@@ -832,13 +832,26 @@ fn drive_session_start(script: &mut UiScript, name: &str, deps: &[String]) -> Ve
|
||||
// (`load_dependencies`) and there is no reason session errors should differ — charging it here
|
||||
// would count one library's fault once per addon that embeds it, which is the whole reason
|
||||
// one-VM-per-addon exists.
|
||||
//
|
||||
// **Attribution is by the RAISING CHUNK now, not by which event window the raise fell in**
|
||||
// (decision 1226 — recorded when this was still window-based, fixed here). The window proxy
|
||||
// broke on the shape it was written for: AceAddon drains its ENTIRE `nextAddon` queue on any
|
||||
// `ADDON_LOADED` it sees (`AceAddon-2.0.lua:104-105`) and calls each consumer's
|
||||
// `OnInitialize` there (`:230`). So the SURVEYED addon's own code runs inside a DEPENDENCY's
|
||||
// window, and firing deps outside the mark charged those raises to nobody — the whole
|
||||
// FuBar/Ace family was silently OVER-reported as surviving. `Region:SetParent` landing made
|
||||
// FuBar_FuXPFu flip `ok -> fail` and that looked like a regression; it was this.
|
||||
//
|
||||
// Every `ADDON_LOADED` fires inside the mark now, and the dependency exemption is applied
|
||||
// afterwards by asking WHOSE FILE raised. Chunk names have been truthful since 1217
|
||||
// (`@Interface\AddOns\<Folder>\<File>`), which is what makes the honest key available at all.
|
||||
let before = script.errors().len();
|
||||
for dep in deps {
|
||||
script.fire_event(
|
||||
"ADDON_LOADED",
|
||||
vec![benilla_ui::script::ScriptValue::Str(dep.clone())],
|
||||
);
|
||||
}
|
||||
let before = script.errors().len();
|
||||
script.fire_event(
|
||||
"ADDON_LOADED",
|
||||
vec![benilla_ui::script::ScriptValue::Str(name.to_string())],
|
||||
@@ -849,7 +862,37 @@ fn drive_session_start(script: &mut UiScript, name: &str, deps: &[String]) -> Ve
|
||||
for _ in 0..10 {
|
||||
script.tick(0.1);
|
||||
}
|
||||
script.errors().split_off(before)
|
||||
let raised = script.errors().split_off(before);
|
||||
raised
|
||||
.into_iter()
|
||||
.filter(|e| !raised_inside_a_dependencys_own_file(e, name, deps))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Does this raise belong to a DEPENDENCY's file rather than the surveyed addon's?
|
||||
///
|
||||
/// The rule `load_dependencies` already states for load errors, now enforceable for session errors
|
||||
/// too: *a library that fails is its own row; blaming its consumers would count one fault once per
|
||||
/// addon that embeds it.* Only the FIRST line is consulted — that is the raise site; the frames
|
||||
/// below it are the call path, and a consumer calling into a library that then raises is still the
|
||||
/// library's fault, exactly as it is at load time.
|
||||
///
|
||||
/// **Conservative on purpose.** A chunk that names no addon folder at all — our own FrameXML, an
|
||||
/// `[string "Frame:OnEvent"]` handler — is KEPT, because the surveyed addon is what drove it. Only
|
||||
/// a chunk that positively names one of this addon's own dependencies is dropped. Getting that
|
||||
/// backwards would swing the column the other way, and the whole point of 1226 is that a proxy
|
||||
/// which errs silently in one direction is how the number drifted in the first place.
|
||||
fn raised_inside_a_dependencys_own_file(err: &str, name: &str, deps: &[String]) -> bool {
|
||||
let Some(first) = err.lines().next() else {
|
||||
return false;
|
||||
};
|
||||
// The surveyed addon's own folder always wins, even when a dependency's name is a substring of
|
||||
// a path inside it.
|
||||
if first.contains(&format!("\\{name}\\")) || first.starts_with(&format!("{name}\\")) {
|
||||
return false;
|
||||
}
|
||||
deps.iter()
|
||||
.any(|d| first.contains(&format!("\\{d}\\")) || first.starts_with(&format!("{d}\\")))
|
||||
}
|
||||
|
||||
/// Invoke the reference's own UI entry points, so an addon's OVERRIDES actually execute.
|
||||
@@ -1206,6 +1249,69 @@ fn seat_a_session(script: &mut UiScript) {
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
// ── A POPULATED world, not merely an inhabited one ──────────────────────────────────────
|
||||
//
|
||||
// The columns above answer "did it raise"; the render column answers "did it draw". Until
|
||||
// now the survey seated a player into an EMPTY world — no buffs, no target, no live
|
||||
// cooldown — so a buff bar, a target frame or a cooldown-text addon drew nothing and landed
|
||||
// in the drew-nothing list beside the pure libraries. `CT_BuffMod` declares 29 frames with
|
||||
// none hidden at birth and `ElkBuffBar` 3: neither was failing to build, both had nothing to
|
||||
// put in them.
|
||||
//
|
||||
// That made "nothing to draw" and "failed to draw" share a row, and the second is the one
|
||||
// worth finding. Seating content separates them — and, more usefully, exposes the addons
|
||||
// that only fail WHEN there is something to draw, which no empty-world column can reach.
|
||||
//
|
||||
// Deliberately minimal and deliberately ordinary: one buff, one target, one occupied action
|
||||
// with a running cooldown. Not a stress fixture — a Tuesday.
|
||||
script.set_auras(
|
||||
"player",
|
||||
Some(vec![benilla_ui::script::AuraState {
|
||||
spell_id: 1243,
|
||||
name: Some("Power Word: Fortitude".into()),
|
||||
icon: Some("Interface\\Icons\\Spell_Holy_WordFortitude".into()),
|
||||
count: 1,
|
||||
helpful: true,
|
||||
cancelable: true,
|
||||
..Default::default()
|
||||
}]),
|
||||
);
|
||||
script.set_unit(
|
||||
"target",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
exists: true,
|
||||
name: Some("Target Dummy".into()),
|
||||
health: 80,
|
||||
max_health: 100,
|
||||
level: 60,
|
||||
power_type: 0,
|
||||
power: 50,
|
||||
max_power: 100,
|
||||
class: Some("Warrior".into()),
|
||||
class_file: Some("WARRIOR".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
script.set_action(
|
||||
1,
|
||||
Some(benilla_ui::script::ActionSlot {
|
||||
texture: Some("Interface\\Icons\\Ability_SteelMelee".into()),
|
||||
kind: 0x00,
|
||||
action: 100,
|
||||
count: 0,
|
||||
}),
|
||||
);
|
||||
script.set_action_state(
|
||||
1,
|
||||
Some(benilla_ui::script::ActionState {
|
||||
usable: true,
|
||||
// (startTime ms, duration ms, enable) — a cooldown with time left on it, which is
|
||||
// what a cooldown-text addon (OmniCC, CooldownCount) needs before it draws anything.
|
||||
cooldown: Some((1, 30_000, true)),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Every string key in the VM's `_G`.
|
||||
@@ -2841,6 +2947,68 @@ mod dependency_tests {
|
||||
/// dependency's OWN handler raising is the dependency's row, never its consumers'. Those
|
||||
/// events fire outside the error-capture window, so one library's fault cannot be counted once
|
||||
/// per addon that embeds it.
|
||||
/// **A consumer's OWN code raising inside a DEPENDENCY's window is the consumer's row.**
|
||||
///
|
||||
/// 1226's finding, now enforced. `AceAddon-2.0.lua:104-105` drains its entire `nextAddon` queue
|
||||
/// on ANY `ADDON_LOADED` it sees and calls each consumer's `OnInitialize` there (`:230`). So
|
||||
/// the surveyed addon's own file runs inside a dependency's window — and while attribution was
|
||||
/// by window, those raises were charged to nobody and the whole FuBar/Ace family read as
|
||||
/// surviving when it was not.
|
||||
///
|
||||
/// Attribution is by the raising CHUNK now, which 1217 made truthful. The fixture is AceAddon's
|
||||
/// shape reduced: the library drains a queue on the first ADDON_LOADED it sees, whoever it
|
||||
/// names, and the consumer's callback raises from the consumer's own file.
|
||||
///
|
||||
/// The second half is the half that must not regress: the LIBRARY's own raise, in the same
|
||||
/// window, still belongs to the library.
|
||||
#[test]
|
||||
fn a_consumers_raise_inside_a_dependency_window_is_the_consumers_row() {
|
||||
let tmp =
|
||||
std::env::temp_dir().join(format!("benilla-harness-attrib-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
let write = |name: &str, toc: &str, file: &str, body: &str| {
|
||||
let dir = tmp.join(name);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join(format!("{name}.toc")), toc).unwrap();
|
||||
std::fs::write(dir.join(file), body).unwrap();
|
||||
};
|
||||
// AceAddon's shape: drain every queued consumer on the FIRST ADDON_LOADED, whoever it names.
|
||||
write(
|
||||
"QueueLib",
|
||||
"## Interface: 11200\nlib.lua\n",
|
||||
"lib.lua",
|
||||
"QueueLibQueue = {}\n\
|
||||
QueueLibFrame = CreateFrame(\"Frame\")\n\
|
||||
QueueLibFrame:RegisterEvent(\"ADDON_LOADED\")\n\
|
||||
QueueLibFrame:SetScript(\"OnEvent\", function()\n\
|
||||
while table.getn(QueueLibQueue) > 0 do\n\
|
||||
local f = table.remove(QueueLibQueue, 1) f()\n\
|
||||
end\n\
|
||||
end)\n",
|
||||
);
|
||||
// The consumer queues a callback that raises from ITS OWN file.
|
||||
write(
|
||||
"QueueUser",
|
||||
"## Interface: 11200\n## Dependencies: QueueLib\nuse.lua\n",
|
||||
"use.lua",
|
||||
"table.insert(QueueLibQueue, function() error(\"consumer init blew up\") end)\n",
|
||||
);
|
||||
|
||||
let reports = survey(&tmp);
|
||||
let of = |n: &str| reports.iter().find(|r| r.name == n).unwrap();
|
||||
|
||||
assert!(
|
||||
of("QueueUser")
|
||||
.session_errors
|
||||
.iter()
|
||||
.any(|e| e.contains("consumer init blew up")),
|
||||
"the consumer's own file raised — window or not, it is the consumer's row: {:?}",
|
||||
of("QueueUser").session_errors
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_loaded_addon_gets_its_own_addon_loaded_event() {
|
||||
let tmp = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -24,9 +24,9 @@ fn shipped_action_bar_drives_end_to_end() {
|
||||
);
|
||||
if file == "ActionBar.xml" {
|
||||
assert_eq!(
|
||||
report.frames, 59,
|
||||
report.frames, 61,
|
||||
"bar + XP StatusBar (+ its numerals overlay) + exhaustion tick + max-level rail + art frame + 12 buttons (each with a Cooldown child) + 2 page buttons + the performance meter and its hover button, \
|
||||
+ BonusActionBarFrame and its 12 buttons with their Cooldown children (25 — hidden, as the reference's is; decision 1223)"
|
||||
+ BonusActionBarFrame and its 12 buttons with their Cooldown children (25 — hidden, as the reference's is; decision 1223), + ReputationWatchBar and its status bar (2 — hidden, ref ReputationFrame.xml:869)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,6 +523,9 @@ pub(crate) struct Model {
|
||||
/// with nothing moving, and a move outlives the mouse button (the reference's mouse-up
|
||||
/// auto-stop skips the Lua drag type).
|
||||
pub(crate) moving: Option<super::object::FrameMove>,
|
||||
/// The in-flight `StartSizing` drag — the resize twin of [`Self::moving`], cleared by the same
|
||||
/// `StopMovingOrSizing` (`0x776990`).
|
||||
pub(crate) sizing: Option<super::object::FrameSizing>,
|
||||
/// The in-flight Slider thumb drag: set when a LeftButton press lands on a Slider's thumb, held
|
||||
/// until release / pointer-leave (decision 0250 §5, the engine's C++-equivalent thumb drag —
|
||||
/// like a scrollbar dragging in the real client, no Lua involved). `None` between drags
|
||||
@@ -1038,6 +1041,7 @@ impl Model {
|
||||
drag_registered: HashMap::new(),
|
||||
drag: None,
|
||||
moving: None,
|
||||
sizing: None,
|
||||
slider_drag: None,
|
||||
gossip: None,
|
||||
gossip_selects: Vec::new(),
|
||||
|
||||
@@ -31,7 +31,7 @@ mod layout_methods;
|
||||
mod movable;
|
||||
pub(crate) mod toplevel;
|
||||
pub(crate) use layout_methods::anchor_bits_eq;
|
||||
pub(crate) use movable::{advance_move, FrameMove};
|
||||
pub(crate) use movable::{advance_move, advance_size, FrameMove, FrameSizing};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// id ↔ lightuserdata
|
||||
|
||||
@@ -274,8 +274,18 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
/// **97 of the 108 addons that drew and then raised on being touched died on that one line.** No
|
||||
/// missing verb: `GetLeft` was always there, and always answered nil.
|
||||
///
|
||||
/// Cheap: `resolve_layout`'s tier-1 gate is one epoch comparison that returns immediately when
|
||||
/// nothing has been touched, so a settled tree pays a compare per getter.
|
||||
/// Cheap on a SETTLED tree: `resolve_layout`'s tier-1 gate is one epoch comparison that returns
|
||||
/// immediately when nothing has been touched, so a run of getters pays a compare each.
|
||||
///
|
||||
/// **The interleaved case is not, and this is measured rather than asserted.** A write bumps the
|
||||
/// epoch, so `SetPoint; GetLeft; SetPoint; GetLeft; …` resolves the whole graph once per iteration.
|
||||
/// Timed on a 200-frame tree: 30 alternating pairs cost **1.41 ms** against **54 µs** for the same
|
||||
/// writes with a single read at the end — ~26x, about 47 µs per settle, and the per-settle half
|
||||
/// scales with the GRAPH, not the loop.
|
||||
///
|
||||
/// That is exactly the shape Dewdrop's menu builder has, so opening a menu pays it once. It buys a
|
||||
/// menu that works at all, which is the trade taken here. If it ever reads as a hitch, the fix is a
|
||||
/// narrower resolve (the queried frame's subtree), not a return to the stale cache.
|
||||
///
|
||||
/// **It deliberately does NOT fire `OnSizeChanged`.** That drain runs Lua handlers, and re-entering
|
||||
/// Lua from inside a binding is how a borrow panic or an unbounded recursion happens. The
|
||||
|
||||
@@ -103,6 +103,19 @@ pub(crate) struct FrameMove {
|
||||
sample: (f32, f32),
|
||||
}
|
||||
|
||||
/// An in-flight `StartSizing` drag: which frame, which grip, and the cursor at the last pump.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct FrameSizing {
|
||||
frame: FrameHandle,
|
||||
/// The grip the caller named — the EDGES this drag moves. `LEFT` moves the left edge and
|
||||
/// leaves the right where it is, `BOTTOMRIGHT` moves both of those, and so on.
|
||||
left: bool,
|
||||
right: bool,
|
||||
top: bool,
|
||||
bottom: bool,
|
||||
sample: (f32, f32),
|
||||
}
|
||||
|
||||
/// Populate `m`'s movable/resizable methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// SetMovable(flag) / IsMovable() — flag word bit 0x100 (`0x776420`/`0x7764d0` through the
|
||||
@@ -177,6 +190,57 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
start_moving(&mut model, h)
|
||||
})?,
|
||||
)?;
|
||||
// StartSizing(point) — begin a resize drag from a named grip (`0x776830`, verified in wow-re's
|
||||
// ledger; the reference's own caller is `FloatingChatFrame.lua:600`,
|
||||
// `this:GetParent():StartSizing(anchorPoint)`).
|
||||
//
|
||||
// **What is verified and what is read, kept apart on purpose.** Verified: the verb exists, it
|
||||
// takes the grip name, it returns nothing, and `StopMovingOrSizing` ends it (the same slot
|
||||
// clear as a move). NOT recorded anywhere in wow-re — the ledger has it as ORCHESTRATION with
|
||||
// no inline math — is WHICH EDGES a given grip moves. Taken here as the plain meaning of an
|
||||
// anchor point, which is how the reference's own caller uses it (its resize grips pass the
|
||||
// corner they sit in): the named edges follow the cursor and the opposite ones stay put. If an
|
||||
// RE pass ever contradicts that, this comment is where to correct it.
|
||||
//
|
||||
// Four corpus addons reach it through ONE line — `FuBar_Panel.lua:980`, replicated into
|
||||
// FuBar_CorkFu, FuBar_FuXPFu, FuBar_SpellStatusFu and oRA2 — which is 1207's rule and why the
|
||||
// count is not four independent votes.
|
||||
m.set(
|
||||
"StartSizing",
|
||||
lua.create_function(|lua, (this, point): (Table, Option<String>)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if !model.arena.frame(h).is_some_and(|f| f.resizable) {
|
||||
return Err(not_flagged(&model, h, "resizable"));
|
||||
}
|
||||
// Same "do not start a second one" guard `start_moving` carries.
|
||||
if model.sizing.is_some() || model.moving.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let p = point.unwrap_or_default().to_ascii_uppercase();
|
||||
let (left, right) = (p.contains("LEFT"), p.contains("RIGHT"));
|
||||
let (top, bottom) = (p.contains("TOP"), p.contains("BOTTOM"));
|
||||
// A grip naming no edge would resize nothing; the reference has no such call and we
|
||||
// refuse rather than invent one.
|
||||
if !(left || right || top || bottom) {
|
||||
return Ok(());
|
||||
}
|
||||
super::toplevel::raise(&mut model, h);
|
||||
if let Some(f) = model.arena.frame_mut(h) {
|
||||
f.user_placed = true;
|
||||
}
|
||||
let sample = model.cursor_pos;
|
||||
model.sizing = Some(FrameSizing {
|
||||
frame: h,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
sample,
|
||||
});
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
// StopMovingOrSizing() — leave the drag (`0x776990` → `0x765640`): a state clear, and only
|
||||
// when `self` IS the frame in the slot. Nothing is written back — the pump has been moving the
|
||||
// anchors all along, so the frame simply keeps the last position it was dragged to. Harmless
|
||||
@@ -187,6 +251,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if model.sizing.is_some_and(|sz| sz.frame == h) {
|
||||
model.sizing = None;
|
||||
}
|
||||
if model.moving.is_some_and(|mv| mv.frame == h) {
|
||||
model.moving = None;
|
||||
}
|
||||
@@ -283,6 +350,62 @@ fn start_moving(model: &mut Model, h: FrameHandle) -> mlua::Result<()> {
|
||||
/// Applies `(pos − sample)` scaled into the frame's anchor offsets and re-centers the sample; a
|
||||
/// zero delta does nothing at all, and a frame that died mid-move ends the move rather than
|
||||
/// writing to a dead handle.
|
||||
/// Pump an in-flight `StartSizing`: move the gripped edges to the cursor, leave the others.
|
||||
///
|
||||
/// The mirror of [`advance_move`] — same sample-and-recentre, same dead-frame bail, same
|
||||
/// local-unit scaling (offsets are pre-scale, so a cursor delta divides by the frame's own scale on
|
||||
/// the way in). Where a move translates every anchor, a resize moves the gripped edges only: the
|
||||
/// width/height change, and the anchor offsets follow only for the edges that actually moved, which
|
||||
/// is what keeps the OPPOSITE edge planted.
|
||||
pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
let Some(sz) = model.sizing else { return };
|
||||
if model.arena.frame(sz.frame).is_none() {
|
||||
model.sizing = None;
|
||||
return;
|
||||
}
|
||||
let (dx, dy) = (pos.0 - sz.sample.0, pos.1 - sz.sample.1);
|
||||
if dx == 0.0 && dy == 0.0 {
|
||||
return;
|
||||
}
|
||||
let inv = 1.0 / eff_scale(model, sz.frame);
|
||||
let (dx, dy) = (dx * inv, dy * inv);
|
||||
let Some(input) = model.layout_inputs.get_mut(&sz.frame) else {
|
||||
return;
|
||||
};
|
||||
// Width grows when the RIGHT grip goes right, or the LEFT grip goes left. Height likewise with
|
||||
// y-up: the TOP grip going up grows it, the BOTTOM grip going down grows it.
|
||||
let dw = if sz.right {
|
||||
dx
|
||||
} else if sz.left {
|
||||
-dx
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let dh = if sz.top {
|
||||
dy
|
||||
} else if sz.bottom {
|
||||
-dy
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
input.width = (input.width + dw).max(1.0);
|
||||
input.height = (input.height + dh).max(1.0);
|
||||
// The planted edge: a frame anchored by its LEFT that is gripped on the LEFT has to move too,
|
||||
// or the resize would push the right edge instead. Only the gripped axis shifts.
|
||||
if !input.anchors.is_empty() && (sz.left || sz.bottom) {
|
||||
for a in &mut input.anchors {
|
||||
if sz.left {
|
||||
a.x_off += dx;
|
||||
}
|
||||
if sz.bottom {
|
||||
a.y_off += dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
model.touch_layout();
|
||||
model.sizing = Some(FrameSizing { sample: pos, ..sz });
|
||||
}
|
||||
|
||||
pub(crate) fn advance_move(model: &mut Model, pos: (f32, f32)) {
|
||||
let Some(mv) = model.moving else { return };
|
||||
if model.arena.frame(mv.frame).is_none() {
|
||||
|
||||
@@ -204,6 +204,7 @@ impl UiScript {
|
||||
// crosses and a pump behind the early return would only ever advance when the cursor
|
||||
// left the frame it is dragging.
|
||||
super::object::advance_move(&mut model, (x, y));
|
||||
super::object::advance_size(&mut model, (x, y));
|
||||
let drag_start = cursor::maybe_start_drag(&mut model, (x, y));
|
||||
let new_handle = new_id.and_then(|id| model.id_to_frame.get(&id).copied());
|
||||
if new_handle == model.mouseover {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
//! Region method-table cluster: **layout** — size, anchors and the resolved-rect readers.
|
||||
//! Split out of `region.rs` at the 0716 file-size budget.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use crate::layout::{Anchor, Point};
|
||||
use crate::script::object::anchor_bits_eq;
|
||||
use crate::script::Model;
|
||||
|
||||
/// Resolve `self` (a region wrapper) to its live [`RegionHandle`].
|
||||
use super::{
|
||||
measured_wh, region_handle_of, region_owner_id, region_set_point, resolve_target, size_bits_eq,
|
||||
};
|
||||
|
||||
/// Populate `m`'s layout methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// Region explicit size (drawn centered on the owner; region anchors come later).
|
||||
m.set(
|
||||
"SetWidth",
|
||||
lua.create_function(|lua, (this, w): (Table, f32)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
let new = Some((w, d.size.map_or(0.0, |s| s.1)));
|
||||
let changed = !size_bits_eq(d.size, new);
|
||||
d.size = new;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"SetHeight",
|
||||
lua.create_function(|lua, (this, h): (Table, f32)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
let new = Some((d.size.map_or(0.0, |s| s.0), h));
|
||||
let changed = !size_bits_eq(d.size, new);
|
||||
d.size = new;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"SetSize",
|
||||
lua.create_function(|lua, (this, w, h): (Table, f32, f32)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
let new = Some((w, h));
|
||||
let changed = !size_bits_eq(d.size, new);
|
||||
d.size = new;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"GetWidth",
|
||||
lua.create_function(|lua, this: Table| Ok(measured_wh(lua, &this)?.0))?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"GetHeight",
|
||||
lua.create_function(|lua, this: Table| Ok(measured_wh(lua, &this)?.1))?,
|
||||
)?;
|
||||
|
||||
// GetLeft/GetRight/GetTop/GetBottom — the region's RESOLVED edges (y-up UI units; frame twin
|
||||
// in object.rs). An anchored region reads its own resolved rect; an unanchored one has no
|
||||
// rect of its own (it draws relative to its owner at extract) → nil, same as pre-resolve.
|
||||
for (name, pick) in [
|
||||
("GetLeft", 0u8),
|
||||
("GetRight", 1u8),
|
||||
("GetTop", 2u8),
|
||||
("GetBottom", 3u8),
|
||||
] {
|
||||
m.set(
|
||||
name,
|
||||
lua.create_function(move |lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model.region_resolved.get(&rh).map(|r| match pick {
|
||||
0 => r.left,
|
||||
1 => r.right,
|
||||
2 => r.top,
|
||||
_ => r.bottom,
|
||||
}))
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Region anchors: SetPoint/ClearAllPoints/SetAllPoints mirror the frame versions
|
||||
// ([`super::object`]) but write [`super::RegionData::anchors`]. An unspecified `relativeTo`
|
||||
// defaults to the **owner frame**; a named one may be a frame or a sibling region (the real
|
||||
// XML anchors regions to sibling regions everywhere — merchant label plate → `$parentSlot`).
|
||||
m.set(
|
||||
"SetPoint",
|
||||
lua.create_function(
|
||||
|lua, (this, p, a2, a3, a4, a5): (Table, String, Value, Value, Value, Value)| {
|
||||
region_set_point(lua, &this, &p, [a2, a3, a4, a5])
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"ClearAllPoints",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
let changed = !d.anchors.is_empty();
|
||||
d.anchors.clear();
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"SetAllPoints",
|
||||
lua.create_function(|lua, (this, target): (Table, Value)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let owner = region_owner_id(&mut model, rh);
|
||||
let rel_id = resolve_target(&mut model, &target, owner);
|
||||
let pair = [
|
||||
Anchor::new(Point::TopLeft, rel_id, Point::TopLeft, 0.0, 0.0),
|
||||
Anchor::new(Point::BottomRight, rel_id, Point::BottomRight, 0.0, 0.0),
|
||||
];
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
let same = data.anchors.len() == 2
|
||||
&& data
|
||||
.anchors
|
||||
.iter()
|
||||
.zip(&pair)
|
||||
.all(|(a, b)| anchor_bits_eq(a, b));
|
||||
if !same {
|
||||
data.anchors.clear();
|
||||
data.anchors.extend_from_slice(&pair);
|
||||
model.touch_layout();
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
//! Region method-table cluster: **paint** — what a Texture shows and how it is tinted,
|
||||
//! blended, cropped and layered. Split out of `region.rs` at the 0716 file-size budget.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use crate::script::object::{as_f32, draw_layer_from_str};
|
||||
use crate::script::{Model, TexCoords};
|
||||
|
||||
/// Resolve `self` (a region wrapper) to its live [`RegionHandle`].
|
||||
use super::region_handle_of;
|
||||
|
||||
/// Populate `m`'s paint methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// SetAlpha/GetAlpha — the region's own alpha, distinct from the owner frame's. The ref kit reads
|
||||
// it back to ramp a texture (CastingBarFrame's completion flash does
|
||||
// `CastingBarFlash:SetAlpha(GetAlpha() + CASTING_BAR_FLASH_STEP)`), so the getter must return the
|
||||
// region's value, never the frame's. Draw law + the open question: [`RegionData::alpha`].
|
||||
m.set(
|
||||
"SetAlpha",
|
||||
lua.create_function(|lua, (this, alpha): (Table, f32)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.region_data.entry(rh).or_default().alpha = Some(alpha.clamp(0.0, 1.0));
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"GetAlpha",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
Ok(model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.alpha)
|
||||
.unwrap_or(1.0))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"SetVertexColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, f32, f32, f32, Option<f32>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.vertex_color = Some([r, g, b, a.unwrap_or(1.0)]);
|
||||
// The same slot `SetTextColor` writes on a FontString, so it is the same explicit
|
||||
// colour set as far as font-object inheritance is concerned.
|
||||
d.font_explicit.color = true;
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// GetVertexColor — the setter's own pair, a real 5875 binding (`0x79aa50`, wow-re
|
||||
// `system/ui/ledger.tsv`; it sits directly above `SetVertexColor 0x79abd0` in the same region
|
||||
// method family). Never set = the untinted white every region draws at by default.
|
||||
m.set(
|
||||
"GetVertexColor",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let c = model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.vertex_color)
|
||||
.unwrap_or([1.0, 1.0, 1.0, 1.0]);
|
||||
Ok((c[0], c[1], c[2], c[3]))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetGradientAlpha(orientation, r1,g1,b1,a1, r2,g2,b2,a2) and its alpha-less twin
|
||||
// SetGradient(orientation, r1,g1,b1, r2,g2,b2) — the two-stop linear gradient the client
|
||||
// generates into the same `+0xcc` texture slot the colour form of SetTexture fills.
|
||||
//
|
||||
// These were missing, and they were the single wall in front of the corpus's largest family:
|
||||
// `FuBar\FuBar_Panel.lua:144` calls SetGradientAlpha while building the bar, so all 20 FuBar
|
||||
// plugins died there — after the debugstack/chunk-name fix got them that far.
|
||||
//
|
||||
// The orientation token is matched case-insensitively and anything that is not "VERTICAL" is
|
||||
// horizontal, which is the client's own leniency and matters because addons spell it both ways.
|
||||
// The gradient is stored WHOLE (see `RegionData::gradient`); the paint folds it to its midpoint
|
||||
// because a quad carries one tint today. That approximation is visible and is recorded at the
|
||||
// field, not hidden here.
|
||||
for (name, with_alpha) in [("SetGradientAlpha", true), ("SetGradient", false)] {
|
||||
m.set(
|
||||
name,
|
||||
lua.create_function(move |lua, args: mlua::MultiValue| {
|
||||
let mut it = args.into_iter();
|
||||
let this: Table = match it.next() {
|
||||
Some(Value::Table(t)) => t,
|
||||
_ => return Err(mlua::Error::runtime("expected a region")),
|
||||
};
|
||||
let orientation = match it.next() {
|
||||
Some(Value::String(s)) => s.to_str()?.to_string(),
|
||||
// A missing/!string orientation is horizontal, like any non-"VERTICAL" token.
|
||||
_ => String::new(),
|
||||
};
|
||||
let n = if with_alpha { 8 } else { 6 };
|
||||
let mut c = [0.0f32; 8];
|
||||
for slot in c.iter_mut().take(n) {
|
||||
*slot = it.next().as_ref().map(as_f32).unwrap_or(0.0);
|
||||
}
|
||||
let (start, end) = if with_alpha {
|
||||
([c[0], c[1], c[2], c[3]], [c[4], c[5], c[6], c[7]])
|
||||
} else {
|
||||
// SetGradient has no alpha stops: both ends are opaque.
|
||||
([c[0], c[1], c[2], 1.0], [c[3], c[4], c[5], 1.0])
|
||||
};
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.gradient = Some(crate::script::Gradient {
|
||||
vertical: orientation.eq_ignore_ascii_case("VERTICAL"),
|
||||
start,
|
||||
end,
|
||||
});
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
|
||||
m.set(
|
||||
"SetTexture",
|
||||
// The trailing three are `Value`, not `Option<f32>`, and that is a fidelity fix rather than
|
||||
// laxity. The path form reads ONE argument (`0x770200`); only the colour form
|
||||
// (`0x770360`) reads up to four. A C function takes what it wants off the Lua stack and
|
||||
// ignores the rest, so `SetTexture(path, true)` is fine on the real client — and typing
|
||||
// these as `Option<f32>` made us raise on it, `bad argument #3: error converting Lua
|
||||
// boolean to f32`, in a call the client accepts silently.
|
||||
//
|
||||
// Found by `_LazyPig/LazyPigMenu.lua:182`
|
||||
// (`texture_title:SetTexture("Interface\DialogFrame\UI-DialogBox-Header", true)`), which
|
||||
// reached us only once the survey started seating the addon registry — the whole point of
|
||||
// that instrument fix. The stray `true` is meaningless in 1.12 and the addon author
|
||||
// presumably meant a later client's second parameter; either way the client shrugs.
|
||||
lua.create_function(
|
||||
|lua, (this, arg, g, b, a): (Table, Value, Value, Value, Value)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
// A plain SetTexture makes the region ordinary again — drop any portrait circular mask
|
||||
// and any live-unit-portrait binding.
|
||||
data.circular = false;
|
||||
data.portrait_unit = None;
|
||||
// Both forms write the SAME `+0xcc` texture slot — the path form loads a file
|
||||
// there (`0x770200`), the colour form generates an 8×8 solid into it
|
||||
// (`0x770360`) — so each clears the other. NEITHER touches the vertex colour at
|
||||
// `+0xb8`: a tint outlives the art it was tinting.
|
||||
match &arg {
|
||||
// SetTexture("") clears, same as SetTexture(nil) — the ref lua blanks state
|
||||
// art with the empty string (QuestLogFrame.lua:165-166).
|
||||
Value::String(s) if s.to_str()?.is_empty() => {
|
||||
data.texture = None;
|
||||
data.fill = None;
|
||||
}
|
||||
Value::String(s) => {
|
||||
data.texture = Some(s.to_str()?.to_string());
|
||||
data.fill = None;
|
||||
}
|
||||
// The colour form, and the ONLY branch that looks at the trailing three. A
|
||||
// non-numeric there takes the same default a missing one does, which is what
|
||||
// reading off a C stack does: `lua_tonumber` on a non-number yields 0.
|
||||
Value::Number(_) | Value::Integer(_) => {
|
||||
let chan = |v: &Value, dflt: f32| match v {
|
||||
Value::Number(_) | Value::Integer(_) => as_f32(v),
|
||||
Value::String(s) => s
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<f32>().ok())
|
||||
.unwrap_or(dflt),
|
||||
_ => dflt,
|
||||
};
|
||||
data.fill =
|
||||
Some([as_f32(&arg), chan(&g, 0.0), chan(&b, 0.0), chan(&a, 1.0)]);
|
||||
data.texture = None;
|
||||
}
|
||||
// SetTexture(nil) clears (the live API's blank-the-region form); a cleared
|
||||
// texture region draws nothing.
|
||||
Value::Nil => {
|
||||
data.texture = None;
|
||||
data.fill = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// SetDesaturated(flag) -> shaderSupported — Texture only (`0x79c1e0`, wow-re ledger; the
|
||||
// reference's own `ItemButtonTemplate.lua:69` is `local shaderSupported =
|
||||
// icon:SetDesaturated(desaturated)`).
|
||||
//
|
||||
// **The RETURN is the whole design, and it is the half a plausible implementation drops.**
|
||||
// 1.12 ran on cards that could not do the shader, so the verb reports whether it took effect
|
||||
// and FrameXML falls back by hand:
|
||||
//
|
||||
// if ( not desaturated ) then r,g,b = 1,1,1
|
||||
// elseif ( not r or not shaderSupported ) then r,g,b = 0.5,0.5,0.5 end
|
||||
// icon:SetVertexColor(r, g, b)
|
||||
//
|
||||
// We have no desaturation in the renderer, so we answer **nil — unsupported**, which is a real
|
||||
// 1.12 machine's answer and not a lie. Claiming support would suppress that grey fallback and
|
||||
// leave disabled icons drawn at full colour: strictly worse-looking than saying no. When a
|
||||
// desaturating shader lands, flip the return and this comment with it.
|
||||
//
|
||||
// Why it matters far past one verb: **98 of the 109 addons that draw and then raise on being
|
||||
// used, raise here** — `FuBar_Panel.lua:43`'s right-click reaches Dewdrop's `AddLine`
|
||||
// (`Dewdrop-2.0.lua:2172`), which calls `button.arrow:SetDesaturated(true)` unguarded. A
|
||||
// static scan costed this at 61 addons and it was declined; the use-probe costed it at 98 the
|
||||
// moment anyone right-clicks.
|
||||
m.set(
|
||||
"SetDesaturated",
|
||||
lua.create_function(|lua, (this, flag): (Table, Value)| {
|
||||
let on = !matches!(flag, Value::Nil | Value::Boolean(false));
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.region_data.entry(rh).or_default().desaturated = on;
|
||||
// nil, not false: the reference's `shaderSupported` is a 1|nil C answer and callers
|
||||
// write `not shaderSupported`.
|
||||
Ok(Value::Nil)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetTexture() — Texture only (`0x79ba70`), verified in wow-re's widget-method batch
|
||||
// (`system/ui/scratch/widget-api-batch-benilla.md`). Three contract details are each the kind a
|
||||
// plausible implementation gets silently wrong, so each is spelled out:
|
||||
//
|
||||
// · **Exactly ONE return value**, never a multi-return.
|
||||
// · **The colour form returns the literal string `"Solid Texture"`** (`0x835708`), NOT nil.
|
||||
// `SetTexture(r,g,b)` synthesizes an 8x8 solid, and the getter reports that name — so an
|
||||
// addon's `if not tex then` passes straight through on a colour-filled region. Returning nil
|
||||
// here would look tidier and would be wrong in the one direction callers test for.
|
||||
// · **The path is stripped at the LAST `.`** (`0x79baf0`): the loader appends `.blp`/`.tga` to
|
||||
// what was set and the getter strips an extension back off. Taken verbatim rather than
|
||||
// "strip only a real extension" — a directory containing a dot is mangled by the real client
|
||||
// too, and this surface is a transcription, not an improvement.
|
||||
//
|
||||
// Four corpus addons: `AtlasQuest.lua:228` (`AQATLASMAP = AtlasMap:GetTexture()`) and
|
||||
// `FuBarPlugin-2.0.lua:343` (`return self.iconFrame:GetTexture()`), each reached by two addons.
|
||||
m.set(
|
||||
"GetTexture",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_mut::<Model>().expect("model");
|
||||
let Some(data) = model.region_data.get(&rh) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if data.fill.is_some() {
|
||||
return Ok(Some("Solid Texture".to_string()));
|
||||
}
|
||||
Ok(data.texture.as_ref().map(|t| match t.rfind('.') {
|
||||
Some(i) => t[..i].to_string(),
|
||||
None => t.clone(),
|
||||
}))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetPortraitToTexture(path) — set the texture AND mark the region a portrait (drawn masked to
|
||||
// its inscribed circle). The client's portraits are circular (model or icon fallback); the frame
|
||||
// ring is a thin band whose transparent corners would otherwise show the square texture's edges.
|
||||
// The live API's `SetPortraitToTexture(texture, path)` is a global crop helper; ours is the
|
||||
// region-method face of the same intent. This is the *icon/path* portrait — distinct from the
|
||||
// `SetPortraitTexture(region, unit)` live model bake, so it drops any live-unit binding.
|
||||
// `SetTexture` on the same region clears the flag (a plain texture again).
|
||||
m.set(
|
||||
"SetPortraitToTexture",
|
||||
lua.create_function(|lua, (this, path): (Table, String)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.texture = Some(path);
|
||||
data.circular = true;
|
||||
data.portrait_unit = None;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetAlphaGradient(start, length) — the per-character write-on reveal (CSimpleFontString;
|
||||
// the quest-description machinery, ref QuestFrame.lua:548/558). Returns whether `start` is
|
||||
// still inside the text (chars) — the ref's OnUpdate loop advances until this goes false.
|
||||
m.set(
|
||||
"SetAlphaGradient",
|
||||
lua.create_function(|lua, (this, start, length): (Table, f32, f32)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.alpha_gradient = Some((start, length));
|
||||
let chars = data.text.as_deref().map_or(0, |t| t.chars().count());
|
||||
Ok(start < chars as f32)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetBlendMode("BLEND"|"ADD"|…) — the shared alphaMode enum (0x811aa8); only ADD changes
|
||||
// draw behavior in v1 (DISABLE/ALPHAKEY/MOD are accepted as straight alpha, a stated gap).
|
||||
m.set(
|
||||
"SetBlendMode",
|
||||
lua.create_function(|lua, (this, mode): (Table, String)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.region_data.entry(rh).or_default().additive = mode.eq_ignore_ascii_case("ADD");
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"SetDrawLayer",
|
||||
lua.create_function(|lua, (this, layer, sub): (Table, String, Option<i64>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let dl = draw_layer_from_str(&layer)
|
||||
.ok_or_else(|| mlua::Error::runtime(format!("unknown draw layer '{layer}'")))?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
if let Some(region) = model.arena.region_mut(rh) {
|
||||
region.draw_layer = dl;
|
||||
if let Some(s) = sub {
|
||||
region.sub_level = s.clamp(i64::from(i8::MIN), i64::from(i8::MAX)) as i8;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetRotation(radians) — spin the texture about its center, counterclockwise-positive (the
|
||||
// later-era Texture API, shipped early: the world-map player arrow's stand-in rotation —
|
||||
// see `QuadContent::Texture::rotation`). No-arg/nil resets to 0.
|
||||
m.set(
|
||||
"SetRotation",
|
||||
lua.create_function(|lua, (this, radians): (Table, Option<f32>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.region_data
|
||||
.entry(rh)
|
||||
.or_default()
|
||||
.rotation = radians.unwrap_or(0.0);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetTexCoord(left, right, top, bottom) — the 4-edge form (XML `<TexCoords>`): a UV sub-rect in
|
||||
// 0..1 texture space (top-left origin) the Texture region samples, slicing quadrant/atlas art
|
||||
// (decision 0084). SetTexCoord(ULx,ULy, LLx,LLy, URx,URy, LRx,LRy) — the 8-arg affine form: an
|
||||
// arbitrary UV quad (rotation/shear — the reference's `DrawRouteLine` route lines), stored per
|
||||
// corner in the renderer's screen winding.
|
||||
m.set(
|
||||
"SetTexCoord",
|
||||
lua.create_function(|lua, (this, rest): (Table, mlua::Variadic<f32>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let coords = match rest.len() {
|
||||
4 => Some(TexCoords::Rect([rest[0], rest[1], rest[2], rest[3]])),
|
||||
// The live arg order is UL, LL, UR, LR (corner pairs); [`TexCoords::Corners`]
|
||||
// stores screen order [TL, TR, BR, BL].
|
||||
8 => Some(TexCoords::Corners([
|
||||
[rest[0], rest[1]], // UL → TL
|
||||
[rest[4], rest[5]], // UR → TR
|
||||
[rest[6], rest[7]], // LR → BR
|
||||
[rest[2], rest[3]], // LL → BL
|
||||
])),
|
||||
// No args resets to the full texture (the live API's clear form).
|
||||
0 => None,
|
||||
n => {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"SetTexCoord: expected 4 (edges) or 8 (corner pairs) args, got {n}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.region_data
|
||||
.entry(rh)
|
||||
.or_default()
|
||||
.tex_coords = coords;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetTexCoord() → left, right, top, bottom (the 4-edge form; full texture if never set; an
|
||||
// affine mapping reports its bounding edges).
|
||||
m.set(
|
||||
"GetTexCoord",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let [l, r, t, b] = model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.tex_coords)
|
||||
.map(|tc| tc.edges())
|
||||
.unwrap_or([0.0, 1.0, 0.0, 1.0]);
|
||||
Ok((l, r, t, b))
|
||||
})?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
//! Region method-table cluster: **text** — a FontString's string, its faces, colour, shadow
|
||||
//! and justification. Split out of `region.rs` at the 0716 file-size budget.
|
||||
//!
|
||||
//! **These duplicate names the `<Font>` object table also carries, deliberately for now**: a region
|
||||
//! may override its inherited font object per-property, and the severance mask (`FontExplicit`) is
|
||||
//! what keeps the two apart. Collapsing them onto one shared block is the follow-on 1231 named.
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use crate::script::{JustifyH, JustifyV, Model, Outline};
|
||||
|
||||
/// Resolve `self` (a region wrapper) to its live [`RegionHandle`].
|
||||
use super::{measured_wh, region_handle_of};
|
||||
|
||||
/// Populate `m`'s text methods (see the module doc).
|
||||
pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
m.set(
|
||||
"SetText",
|
||||
lua.create_function(|lua, (this, text): (Table, Option<String>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.text = text;
|
||||
// Fresh text draws whole — an armed write-on gradient belongs to the old string.
|
||||
data.alpha_gradient = None;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetFormattedText(fmt, ...) = SetText(format(fmt, ...)) — routed through the stdlib's
|
||||
// positional-aware `format` so `%N$s` specs behave (a consensus call across the 0068 targets).
|
||||
m.set(
|
||||
"SetFormattedText",
|
||||
lua.create_function(|lua, (this, args): (Table, mlua::MultiValue)| {
|
||||
let format: mlua::Function = lua
|
||||
.globals()
|
||||
.get::<Table>("string")?
|
||||
.get::<mlua::Function>("format")?;
|
||||
let text: String = format.call(args)?;
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let data = model.region_data.entry(rh).or_default();
|
||||
data.text = Some(text);
|
||||
data.alpha_gradient = None;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"GetText",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let text = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
model.region_data.get(&rh).and_then(|d| d.text.clone())
|
||||
};
|
||||
match text {
|
||||
Some(t) => Ok(Value::String(lua.create_string(&t)?)),
|
||||
None => Ok(Value::Nil),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetStringWidth/GetStringHeight (FontString): the host-measured text extent from the measure
|
||||
// round-trip ([`super::UiScript::set_measured_text`]) — the client asks its font engine for the
|
||||
// laid-out string's metrics exactly here (`fontstring.md`), and the tooltip's auto-size sums
|
||||
// these to fit its lines. `0` until the string has been measured (a frame's latency; converges).
|
||||
// The stored measure only counts while its key matches the CURRENT text/font/wrap
|
||||
// ([`RegionData::measure_key`]): after a SetText the old string's width is not this string's
|
||||
// metric — serving it is how the whisper header's `GetWidth()` latched the edit-box insets on
|
||||
// the previous header's width. A poll-until-nonzero caller (the chat header machine) now
|
||||
// converges on the RIGHT measure instead of settling on a stale one.
|
||||
// `GetWidth`/`GetHeight` prefer the measured extent, falling back to an explicit `SetSize` — the
|
||||
// real client's `SmallTextTooltipText:GetWidth()` idiom (ref-GameTooltip.xml l.63).
|
||||
// GetStringWidth is the **natural, unwrapped** extent — never the declared box, and never the
|
||||
// wrapped one (wow-re `fontstring-overflow.md`, "The measurement echo": the reference's getter
|
||||
// re-measures the raw text with NO wrap constraint). Unlike `GetWidth` below it deliberately
|
||||
// does NOT fall back to an explicit `SetSize`: the declared width is the very thing a caller
|
||||
// asks this to be independent of. A kit that sizes a box from this number and then sets a width
|
||||
// on the string — which is what the reference's own `PanelTemplates_TabResize` does — would
|
||||
// otherwise read its own output back as its next input and never settle (decision 0997, the
|
||||
// macro window's character tab changing width every frame). `0` until measured, as ever.
|
||||
fn natural_w(lua: &Lua, this: &Table) -> mlua::Result<f32> {
|
||||
let rh = region_handle_of(lua, this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let Some(d) = model.region_data.get(&rh) else {
|
||||
return Ok(0.0);
|
||||
};
|
||||
let scale = model
|
||||
.arena
|
||||
.region(rh)
|
||||
.and_then(|r| model.arena.frame(r.owner))
|
||||
.map(|f| f.effective_scale)
|
||||
.unwrap_or(1.0);
|
||||
Ok(d.measured
|
||||
.filter(|m| m.key == d.measure_key(scale))
|
||||
.map(|m| m.natural_w)
|
||||
.unwrap_or(0.0))
|
||||
}
|
||||
m.set(
|
||||
"GetStringWidth",
|
||||
lua.create_function(|lua, this: Table| natural_w(lua, &this))?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"GetStringHeight",
|
||||
lua.create_function(|lua, this: Table| Ok(measured_wh(lua, &this)?.1))?,
|
||||
)?;
|
||||
|
||||
// SetJustifyH("LEFT"|"CENTER"|"RIGHT") — a FontString's horizontal justification (XML `justifyH`).
|
||||
m.set(
|
||||
"SetJustifyH",
|
||||
lua.create_function(|lua, (this, j): (Table, String)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let jh = match j.to_ascii_uppercase().as_str() {
|
||||
"LEFT" => JustifyH::Left,
|
||||
"RIGHT" => JustifyH::Right,
|
||||
_ => JustifyH::Center,
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.justify_h = jh;
|
||||
d.font_explicit.justify_h = true;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetJustifyV("TOP"|"MIDDLE"|"BOTTOM") — a FontString's vertical justification (XML `justifyV`).
|
||||
m.set(
|
||||
"SetJustifyV",
|
||||
lua.create_function(|lua, (this, j): (Table, String)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let jv = match j.to_ascii_uppercase().as_str() {
|
||||
"TOP" => JustifyV::Top,
|
||||
"BOTTOM" => JustifyV::Bottom,
|
||||
_ => JustifyV::Middle,
|
||||
};
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.justify_v = jv;
|
||||
d.font_explicit.justify_v = true;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetFontObject(GameFontNormal) — re-point this FontString at a Font object: its resolved paint
|
||||
// (face/height/color/outline/shadow) becomes the region's, and the link is kept live, so a later
|
||||
// `GameFontNormal:SetFont(…)` re-paints this region too ([`super::font`]'s module doc).
|
||||
//
|
||||
// All three argument forms the reference's own usage string names (`.rdata 0x87c5cc`:
|
||||
// `SetFontObject(font or "font" or nil)`) — the **object**, which is what 3,180 of the corpus's
|
||||
// 3,186 call sites pass (`Gratuity-2.0.lua:57`, every FuBar/Ace label); a **name string**, for
|
||||
// our own shipped XML and the 6 sites that use it; and **nil**, which severs the link. A frame,
|
||||
// a number, or an unknown name is an error — never a silent no-op (1203/1205/1211's class).
|
||||
m.set(
|
||||
"SetFontObject",
|
||||
lua.create_function(|lua, (this, font): (Table, Value)| {
|
||||
let name = crate::script::font::resolve("SetFontObject", &font)?;
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
// The nil form: unlink, and leave the paint standing (the reference stores a null
|
||||
// parent; nothing re-reads and nothing is cleared).
|
||||
let Some(name) = name else {
|
||||
model.region_data.entry(rh).or_default().font_object = None;
|
||||
return Ok(());
|
||||
};
|
||||
let Some(fo) = model.font_objects.get(&name).cloned() else {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"SetFontObject: no font object named '{name}' is registered"
|
||||
)));
|
||||
};
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.font_object = Some(name);
|
||||
// The severance mask is deliberately NOT reset here. §5-verified: the real "stop
|
||||
// inheriting this property" signal is a CLEARED bit in the inheritMask at
|
||||
// `FONTINSTANCE+0x2c` (FontString `+0xd4`, per-axis justify at `+0x124`), cleared by
|
||||
// each local setter and never restored — "a FontString that set its own colour stays
|
||||
// severed even across a later SetFontObject" (wow-re
|
||||
// `system/ui/scratch/font-object-lua-surface.md`). This corrects our first cut, which
|
||||
// reset it.
|
||||
crate::script::font::repaint(d, &fo);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetFontObject() → the font OBJECT this FontString last resolved (or nil).
|
||||
//
|
||||
// The object, not its name: `Dewdrop-2.0.lua:2181` is
|
||||
// `button.text:SetTextColor(button.text:GetFontObject():GetTextColor())` — 65 sites across 62
|
||||
// corpus addons that index the result immediately. A name string there raises.
|
||||
m.set(
|
||||
"GetFontObject",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let name = {
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.font_object.clone())
|
||||
.filter(|n| model.font_objects.contains_key(n))
|
||||
};
|
||||
match name {
|
||||
Some(n) => Ok(Value::Table(crate::script::font::wrapper(lua, &n)?)),
|
||||
None => Ok(Value::Nil),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetNonSpaceWrap(enable) / CanNonSpaceWrap() — FontString only (`0x79e9f0` / `0x79ead0`).
|
||||
//
|
||||
// Two contract details from wow-re's batch, both easy to get wrong:
|
||||
// · the getter is **`CanNonSpaceWrap`**, not `GetNonSpaceWrap`, and it answers **`1` or nil**,
|
||||
// not a boolean — 1.12 predates that convention and an addon may compare against 1.
|
||||
// · a **no-argument call ENABLES** it (the default is on), rather than being a query.
|
||||
//
|
||||
// `oRA2/Leader/Item.lua:561` is `f.textname:SetNonSpaceWrap(false)`, reached by two addons.
|
||||
m.set(
|
||||
"SetNonSpaceWrap",
|
||||
lua.create_function(|lua, (this, enable): (Table, Value)| {
|
||||
let on = match &enable {
|
||||
Value::Nil => true,
|
||||
Value::Boolean(b) => *b,
|
||||
_ => true,
|
||||
};
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
model.region_data.entry(rh).or_default().non_space_wrap = Some(on);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"CanNonSpaceWrap",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_mut::<Model>().expect("model");
|
||||
let on = model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.non_space_wrap)
|
||||
.unwrap_or(true);
|
||||
Ok(if on { Some(1i64) } else { None })
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// ── shadow, on the REGION ────────────────────────────────────────────────────────────────
|
||||
// These already existed on the font-object table; a FontString made by `CreateFontString` had
|
||||
// none of them, and that is where the corpus calls them:
|
||||
// `FuBar_NavigatorFu/NavigatorFu.lua:31` does
|
||||
// `coordText:SetShadowColor(GameFontNormal:GetShadowColor())` — the GETTER on a font object,
|
||||
// the SETTER on a fresh region — and `KLHThreatMeter/.../KTM_Gui.lua:404` is
|
||||
// `fontstring:SetShadowColor(0,0,0,0.3)`.
|
||||
//
|
||||
// **`GetShadowColor` returns FOUR values, not three** (`0x79dd2f`, `mov eax,0x4` — wow-re's
|
||||
// widget-method batch). Three is the plausible wrong answer and it silently drops the alpha
|
||||
// that NavigatorFu is round-tripping. `GetShadowOffset` returns two, in UI units.
|
||||
//
|
||||
// Either half may be set before the other, so each starts from whatever is there — the same
|
||||
// rule the font-object versions already follow.
|
||||
m.set(
|
||||
"SetShadowColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, f32, f32, f32, Option<f32>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
let offset = d.font_shadow.map_or([0.0, 0.0], |s| s.offset);
|
||||
d.font_shadow = Some(crate::script::FontShadow {
|
||||
offset,
|
||||
color: [r, g, b, a.unwrap_or(1.0)],
|
||||
});
|
||||
d.font_explicit.shadow = true;
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"GetShadowColor",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_mut::<Model>().expect("model");
|
||||
let c = model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.font_shadow)
|
||||
.map_or([0.0, 0.0, 0.0, 1.0], |s| s.color);
|
||||
Ok((c[0], c[1], c[2], c[3]))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"SetShadowOffset",
|
||||
lua.create_function(|lua, (this, x, y): (Table, f32, f32)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
let color = d.font_shadow.map_or([0.0, 0.0, 0.0, 1.0], |s| s.color);
|
||||
d.font_shadow = Some(crate::script::FontShadow {
|
||||
offset: [x, y],
|
||||
color,
|
||||
});
|
||||
d.font_explicit.shadow = true;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
m.set(
|
||||
"GetShadowOffset",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_mut::<Model>().expect("model");
|
||||
let o = model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.font_shadow)
|
||||
.map_or([0.0, 0.0], |s| s.offset);
|
||||
Ok((o[0], o[1]))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetFont(path, height [, flags]) — the direct face/size/outline setter (the real region API and
|
||||
// the XML `font=`/`<FontHeight>`/`outline=` join). `flags` is an OUTLINETYPE-ish string
|
||||
// ("OUTLINE"/"THICKOUTLINE"/…"); anything else clears the outline. A nil/empty `path` keeps the
|
||||
// current face (so a FontString with only `<FontHeight>` retains its inherited object's font).
|
||||
// Returns true (the live API returns whether the font loaded; we always accept — face
|
||||
// availability is the renderer's concern).
|
||||
m.set(
|
||||
"SetFont",
|
||||
lua.create_function(
|
||||
|lua, (this, path, height, flags): (Table, Option<String>, Option<f32>, Option<String>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
// Each argument actually supplied is an EXPLICIT set: it must survive a later
|
||||
// mutation of the font object this region inherits (`FontExplicit`).
|
||||
if let Some(p) = path.filter(|p| !p.is_empty()) {
|
||||
d.font_path = Some(p);
|
||||
d.font_explicit.face = true;
|
||||
}
|
||||
if let Some(h) = height {
|
||||
d.font_height = Some(h);
|
||||
d.font_explicit.height = true;
|
||||
}
|
||||
if let Some(f) = flags {
|
||||
d.outline = Outline::flags(&f);
|
||||
d.font_explicit.outline = true;
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// SetTextHeight(height) — switch the FontString to the scaled-string regime (§5-verified,
|
||||
// wow-re `fontstring-overflow.md`: `0x771600` is the ONLY clearer of the one-to-one bit
|
||||
// `0x200`; the literal size then flows through UNCAPPED, magnified from the raster). Stored
|
||||
// as the distinct [`RegionData::text_height`] — the font object is untouched, so GetFont
|
||||
// keeps reporting the face's own height like the real API.
|
||||
m.set(
|
||||
"SetTextHeight",
|
||||
lua.create_function(|lua, (this, height): (Table, f32)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
lua.app_data_mut::<Model>()
|
||||
.expect("model")
|
||||
.region_data
|
||||
.entry(rh)
|
||||
.or_default()
|
||||
.text_height = Some(height);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// GetFont() → path, height, flags — the resolved face/size/outline (nil path if never set).
|
||||
m.set(
|
||||
"GetFont",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let d = model.region_data.get(&rh);
|
||||
let path = d.and_then(|d| d.font_path.clone());
|
||||
let height = d.and_then(|d| d.font_height);
|
||||
let flags = d.map(|d| d.outline).unwrap_or_default().as_str();
|
||||
let path = match path {
|
||||
Some(p) => Value::String(lua.create_string(&p)?),
|
||||
None => Value::Nil,
|
||||
};
|
||||
Ok((path, height, flags))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// SetTextColor(r, g, b [, a]) — a FontString's text color. A different binding name for the
|
||||
// same `+0xb8` vertex-colour slot `SetVertexColor` writes: a FontString has no texel of its own
|
||||
// to multiply against, so its vertex colour IS the colour it draws.
|
||||
m.set(
|
||||
"SetTextColor",
|
||||
lua.create_function(
|
||||
|lua, (this, r, g, b, a): (Table, f32, f32, f32, Option<f32>)| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let d = model.region_data.entry(rh).or_default();
|
||||
d.vertex_color = Some([r, g, b, a.unwrap_or(1.0)]);
|
||||
d.font_explicit.color = true;
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// GetTextColor() → r, g, b, a — `SetTextColor`'s missing pair, and a real binding in the same
|
||||
// FontInstance family. 11 corpus sites read it off a FontString directly (`CustomNameplates`
|
||||
// re-tints a level tag from the name's colour; `TipBuddy` snapshots every tooltip line), on top
|
||||
// of the 65 that reach it through `GetFontObject()`. Never set = the white every region draws
|
||||
// at, same convention as `GetVertexColor` (the same slot).
|
||||
m.set(
|
||||
"GetTextColor",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model");
|
||||
let c = model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.vertex_color)
|
||||
.unwrap_or([1.0, 1.0, 1.0, 1.0]);
|
||||
Ok((c[0], c[1], c[2], c[3]))
|
||||
})?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -348,3 +348,67 @@ fn the_xml_movable_and_resizable_attributes_reach_the_methods() {
|
||||
(true, true)
|
||||
);
|
||||
}
|
||||
|
||||
/// **`StartSizing(grip)` moves the gripped edges and plants the opposite ones.**
|
||||
///
|
||||
/// `0x776830`, verified in wow-re's ledger; the reference's own caller is
|
||||
/// `FloatingChatFrame.lua:600`. Four corpus addons reach it through ONE line —
|
||||
/// `FuBar_Panel.lua:980`, replicated into FuBar_CorkFu, FuBar_FuXPFu, FuBar_SpellStatusFu and oRA2
|
||||
/// (1207: one library, not four votes).
|
||||
///
|
||||
/// What the ledger does NOT record is which edges a grip moves — it has the verb as pure
|
||||
/// orchestration with no inline math. Taken here as the plain meaning of an anchor point, which is
|
||||
/// how the reference's caller uses it. This test is where that reading is pinned, so an RE pass
|
||||
/// that contradicts it fails here first.
|
||||
#[test]
|
||||
fn start_sizing_moves_the_gripped_edge_and_plants_the_other() {
|
||||
let mut s = crate::script::UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(
|
||||
r#"
|
||||
f = CreateFrame("Frame", "Sizer", UIParent)
|
||||
f:SetResizable(true)
|
||||
f:SetWidth(200) f:SetHeight(100)
|
||||
f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 100, 50)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
|
||||
// Grip the RIGHT edge and drag 40 right: width grows, the left edge stays planted.
|
||||
s.mouse_move(300.0, 100.0);
|
||||
s.run("Sizer:StartSizing(\"RIGHT\")").unwrap();
|
||||
s.mouse_move(340.0, 100.0);
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f32>("return Sizer:GetWidth()").unwrap(), 240.0);
|
||||
assert_eq!(s.eval::<f32>("return Sizer:GetLeft()").unwrap(), 100.0);
|
||||
s.run("Sizer:StopMovingOrSizing()").unwrap();
|
||||
|
||||
// Grip the LEFT edge and drag 30 right: width SHRINKS and the left edge follows the cursor,
|
||||
// so the right edge is the one that stays put.
|
||||
let right_before = s.eval::<f32>("return Sizer:GetRight()").unwrap();
|
||||
s.run("Sizer:StartSizing(\"LEFT\")").unwrap();
|
||||
s.mouse_move(370.0, 100.0);
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f32>("return Sizer:GetWidth()").unwrap(), 210.0);
|
||||
assert_eq!(s.eval::<f32>("return Sizer:GetLeft()").unwrap(), 130.0);
|
||||
assert_eq!(
|
||||
s.eval::<f32>("return Sizer:GetRight()").unwrap(),
|
||||
right_before,
|
||||
"the ungripped edge must not move"
|
||||
);
|
||||
|
||||
// StopMovingOrSizing ends it: further motion changes nothing.
|
||||
s.run("Sizer:StopMovingOrSizing()").unwrap();
|
||||
s.mouse_move(500.0, 100.0);
|
||||
s.resolve();
|
||||
assert_eq!(s.eval::<f32>("return Sizer:GetWidth()").unwrap(), 210.0);
|
||||
|
||||
// A frame that is not resizable refuses, like the movable family's siblings.
|
||||
s.run("g = CreateFrame(\"Frame\", \"NotSizer\", UIParent)")
|
||||
.unwrap();
|
||||
assert!(
|
||||
s.run("NotSizer:StartSizing(\"RIGHT\")").is_err(),
|
||||
"StartSizing must refuse a frame that is not resizable"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user