Files
benilla-octo/crates/benilla-app/assets/ui/CompatTemplates.xml
T
gromandClaude Opus 5 af823dd708 compat: the secure-handler family and the animation surface
Both modules land together because `classic_api/mod.rs` and `widget_surface.rs` carry rows for
each: split, the first commit named a module the second added, and would not have compiled.

## The secure-handler family, because 1.12 has nothing to be secure from

`frame.rs` declined it with a stated reason — "modern restricted execution, which 1.12 has no
equivalent of" — and that is right about the *sandbox* and is the whole of why it is now built.
A secure handler exists so an untainted snippet may call **protected** functions; 1.12 has
neither taint nor protected functions, so the restriction has no counterpart here, while the
plumbing underneath (a named frame store, a snippet on a child's handler) is ordinary
behaviour the snippets need. The body runs as plain Lua with the names the secure environment
would bind.

**The per-header shared table is the part worth getting right.** `MenuWheel.lua:484` writes a
bare `held = false` inside a snippet, and in the real secure environment that lands in a table
per header, not `_G`. `setfenv` onto one table per frame, `__index` to `_G` so the rest of the
API resolves; the test asserts `held` is **not** a global afterwards. My first test asserted
through `_G` and failed — the implementation was right and the test measured the wrong thing.

**`SecureActionButtonTemplate` is not empty**: reading your own attributes on click is plain
dispatch. `type="click"` clicks the frame in `clickbutton` (how the wheels reach the game's
micro buttons), and the lookup follows upstream's order (`type-<button>`, `*type*`, `type`).

**That exposed a real bug of ours**: `SetAttribute` silently dropped a frame.
`attribute_value` kept bool/int/number/string and sent everything else to `ScriptValue::Nil`,
so `SetAttribute("clickbutton", SomeFrame)` stored nothing — and upstream's `clickbutton` is
routinely a frame, which `Cinematic/MenuWheel.lua` relies on. A widget table is now kept as
`ScriptValue::Frame(id)`, the variant `NAME_PLATE_CREATED`'s arg1 already uses, which
round-trips through `object::frame_wrapper`. A non-widget table is still nil, documented.

**Not built, named rather than left to be discovered**: the binding half (`ClearBindings`,
`SetBindingClick`, …) are no-ops — no override-binding layer here to write into — so a wheel
opened with the mouse works and hold-a-key-and-release does not; and the `_onshow`/`_onhide`
attribute snippets are not run, since `SetAttribute` is a plain store with nothing to drive
them.

Templates in `assets/ui/CompatTemplates.xml`, listed on that wall with the reason: no stock
file to migrate from, and XML rather than `compat/` only because `inherits=` is loader-resolved.
A trap caught before shipping: `BackdropTemplateMixin.GetBackdropColor` forwarding to
`self:GetBackdropColor()` calls **itself** once `Mixin()` copies it onto the frame — the stock
getters answer, so there are no getter rows, and a test mixes in and reads back.

## `CreateAnimationGroup`: the surface, with nothing animated

1.12 has no animation system and this UI pass has no interpolation to hang one on. The cost is
the **absent constructor**: `Cinematic/Options.lua:7106` builds a rotating spinner and the nil
method aborted the whole options panel. Measured first — that is the addon's only animation in
55,661 lines, so this is a surface for a decoration.

**A non-looping group finishes the moment it plays**, firing `OnFinished`, because nothing
moves and that is the reading that keeps an addon's flow right; one that chains its next step
off `OnFinished` would otherwise wait forever. A looping group plays until stopped. Stated in
the header, since the name implies motion: a spinner does not spin, a fade appears at its
destination, a sequence lands on its last frame. Setters and getters round-trip.

Corpus: all 100 of grom's addons surveyed before and after — 85 ok / 1 fail, rosters
identical. benilla-ui 2081 passed. Full chain green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-11-01 17:12:13 -07:00

176 lines
9.2 KiB
XML

<Ui>
<!--
The ClassicAPI-era FrameXML templates addons on this install inherit by name. Not windows:
there is nothing to migrate, because 1.12 does not have these templates at all — they are
the same kind of surface as `script/compat/`'s verbs, declared here because `inherits=` is
resolved by the XML loader and cannot be answered from Lua.
Both are used **unguarded inside Ace3 itself**, so this is not one addon's problem:
`AceGUIContainer-Frame.lua:179` and `:206` and `AceGUIContainer-DropDownGroup.lua:128`
name `BackdropTemplate`, and `AceConfigDialog-3.0.lua:573` names
`DialogBorderOpaqueTemplate`. A missing template makes `CreateFrame` raise
("Couldn't find inherited node"), which is where OpenMapClassic stopped — on its first
file, in a library, before a line of its own code ran.
-->
<!--
9.0 moved the backdrop off the C side and gave addons `BackdropTemplateMixin` to put it
back; 1.12 has `SetBackdrop`/`GetBackdrop`/`SetBackdropColor`/`SetBackdropBorderColor` as
real frame methods, so the template is correctly **empty** — a frame inheriting it already
answers all four. The global the mixin idiom tests for
(`BackdropTemplateMixin and "BackdropTemplate" or nil`) is set beside it, its methods
forwarding to those stock ones so `Mixin(f, BackdropTemplateMixin)` is a no-op rather than
a shadow. `OnBackdropLoaded`/`OnBackdropSizeChanged` are the hooks 9.0's own template
fires; ours have nothing to recompute, and an addon that calls one gets a function rather
than a nil.
-->
<Frame name="BackdropTemplate" virtual="true"/>
<!--
A dialog border frame, which is all Ace3 wants of it: `local border = CreateFrame("Frame",
nil, frame, "DialogBorderOpaqueTemplate")` then `border:SetAllPoints(frame)`. Era's own is
nine textures out of `SharedUIPanelTemplates.xml`; the same shape in 1.12's vocabulary is
one `<Backdrop>` over this client's dialog art, which is what every stock 1.12 dialog
draws and what our `GameMenuFrame.xml` already uses. The opaque variant differs only in
its background alpha, hence the two rows.
`enableMouse` stays off: it is a decoration over a frame that handles its own input, and a
border that ate clicks would make an Ace options window unusable.
-->
<Frame name="DialogBorderTemplate" virtual="true">
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
<Color r="1" g="1" b="1" a="0.85"/>
</Backdrop>
</Frame>
<Frame name="DialogBorderOpaqueTemplate" virtual="true">
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
<Color r="1" g="1" b="1" a="1"/>
</Backdrop>
</Frame>
<!--
The secure-handler family. On a modern client these exist so an *untainted* snippet may
call protected functions; 1.12 has neither taint nor protected functions, so what the
template encodes is a restriction that does not exist here and the frame itself carries no
behaviour. Empty, therefore — the header methods the snippets need (`SetFrameRef`,
`WrapScript`, `Execute`) are composed in `script/compat/classic_api/secure.rs`, beside the
rest of the compat surface.
Cinematic's two wheels (`MenuWheel.lua:312`, `MarkerWheel.lua:205`) name the first two.
-->
<Frame name="SecureHandlerBaseTemplate" virtual="true"/>
<Frame name="SecureHandlerShowHideTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
<Frame name="SecureHandlerAttributeTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
<Frame name="SecureHandlerClickTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
<Frame name="SecureHandlerStateTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
<Frame name="SecureHandlerEnterLeaveTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
<Frame name="SecureHandlerDragTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
<Frame name="SecureHandlerMouseUpDownTemplate" virtual="true" inherits="SecureHandlerBaseTemplate"/>
<!--
`SecureActionButtonTemplate`, and this one is **not** empty: its whole job is to read its
own attributes on click and act on them, and that dispatch is plain behaviour rather than
anything protected. `type="click"` clicks the frame named by `clickbutton`, which is how
Cinematic's wheels reach the game's own menu buttons (`MenuWheel.lua:357`), and the three
cast-ish types forward to the stock verbs. The taint machinery is what 1.12 has no
counterpart for, and none of this needs it.
`RegisterForClicks` is the caller's to set, as upstream: the wheels ask for `"AnyUp"`.
-->
<Button name="SecureActionButtonTemplate" virtual="true">
<Scripts>
<OnClick>BenillaSecureAction_OnClick(this, arg1);</OnClick>
</Scripts>
</Button>
<Script><![CDATA[
-- `SecureActionButtonTemplate`'s dispatch. `self:GetAttribute(k)` first tries the
-- button-specific key the real template prefers (`<type>-<button>`, then `*<key>*`), then
-- the plain one, which is the lookup order upstream documents.
local function attr(self, key, button)
local v
if button then
v = self:GetAttribute(key .. "-" .. button)
if v ~= nil then return v end
v = self:GetAttribute("*" .. key .. "-" .. button)
if v ~= nil then return v end
end
v = self:GetAttribute("*" .. key .. "*")
if v ~= nil then return v end
return self:GetAttribute(key)
end
function BenillaSecureAction_OnClick(self, button)
local kind = attr(self, "type", button)
if not kind or kind == "none" then return end
if kind == "click" then
local target = attr(self, "clickbutton", button)
if type(target) == "string" then target = getglobal(target) end
if target and target.Click then target:Click(button) end
elseif kind == "spell" then
local spell = attr(self, "spell", button)
if spell then
local unit = attr(self, "unit", button)
if unit then
CastSpellByName(spell, unit)
else
CastSpellByName(spell)
end
end
elseif kind == "macro" then
local text = attr(self, "macrotext", button)
if text then
RunScript(text)
else
local name = attr(self, "macro", button)
if name then RunMacro(name) end
end
elseif kind == "item" then
local item = attr(self, "item", button)
if item then UseItemByName(item, attr(self, "unit", button)) end
end
end
-- The mixin 9.0 shipped so addons could call the backdrop methods it had removed. Here
-- the stock methods are the real ones, so each row forwards; the table exists for the
-- `BackdropTemplateMixin and "BackdropTemplate"` test and for `Mixin()`.
BackdropTemplateMixin = {}
function BackdropTemplateMixin.OnBackdropLoaded(self) end
function BackdropTemplateMixin.OnBackdropSizeChanged(self) end
function BackdropTemplateMixin.SetupTextureCoordinates(self) end
function BackdropTemplateMixin.ApplyBackdrop(self)
-- 9.0's own ApplyBackdrop re-reads `self.backdropInfo`; ours hands it to the stock
-- setter, which is the same journey with the C side still present.
if self.backdropInfo then
self:SetBackdrop(self.backdropInfo)
if self.backdropColor then
local c = self.backdropColor
self:SetBackdropColor(c.r, c.g, c.b, self.backdropColorAlpha or 1)
end
if self.backdropBorderColor then
local c = self.backdropBorderColor
self:SetBackdropBorderColor(c.r, c.g, c.b, self.backdropBorderColorAlpha or 1)
end
end
end
function BackdropTemplateMixin.ClearBackdrop(self)
self:SetBackdrop(nil)
end
-- No getter rows: 1.12's own `GetBackdrop`/`GetBackdropColor`/`GetBackdropBorderColor`
-- already answer, and a forwarding copy would call *itself* once `Mixin()` had written it
-- onto the frame.
]]></Script>
</Ui>