Loads the original interface from its own manifest, in the order it states, before any addon — because addons are written against a world where FrameXML has already defined its frames and its several thousand functions. Expect it to fail. It is an experiment, it wants the missing-API fallback on beside it to get anywhere, and a half-loaded FrameXML sitting on top of the client's own interface is not a state to be in by accident, so it is off unless asked for through WOWEE_LOAD_FRAMEXML. The docs also give the missing-API fallback a findable home, which it did not have: it went in under a commit about click thresholds because I ran git add -A after saying I would land it separately, and nothing in that message mentions seventy-six lines of Lua engine. The history cannot be corrected without rewriting what is already pushed, so this at least makes the feature discoverable by someone who did not watch it happen. Written down with it: why XML becomes Lua rather than widgets directly, that anchors are constraints rather than positions, what the two switches cost, and the gaps — fonts sized but not loaded, four frame types that lay out but do not behave, left button only, and a texture cache that never evicts.
4.2 KiB
The widget system
The addon API used to answer without doing anything. CreateFrame returned a
table, events dispatched to it, and CreateTexture handed back an object whose
every method was a no-op — so an addon could be written, loaded and run without
putting a pixel on the screen.
There is now a real retained widget tree behind it. The same tree is what FrameXML targets, because FrameXML is only Lua and XML over a widget system, so building it once serves both goals: addons that draw, and a route to running the original interface rather than imitating it.
Shape
| Piece | Where | Notes |
|---|---|---|
| Widget tree, anchors, draw order, hit testing | src/ui/widget_tree.cpp |
No Vulkan or ImGui, so the layout rules are testable without a device |
| Drawing, texture cache, backdrops, status bars | src/ui/widget_renderer.cpp |
Reads Interface\ art through the existing asset path |
| XML reader | src/ui/xml_parser.cpp |
Enough for FrameXML: CDATA, comments, both quote styles |
| XML to Lua | src/ui/framexml_emitter.cpp |
Emits the calls a script would make |
| Lua bindings | src/addons/lua_engine.cpp |
Frames and regions are Lua tables carrying a __wid handle |
Coordinates follow WoW throughout — origin bottom-left, y upward — and flip once at the point of drawing, so every anchor rule reads the way Blizzard documents it rather than mirrored.
Anchors are constraints, not positions. An anchor says "this fraction of my rect
sits at that point", so one anchor plus a size places a frame and two opposing
anchors give the size as well. That is what SetAllPoints relies on, and how
most of FrameXML sizes its backgrounds without ever stating a size.
Why XML becomes Lua
The alternative was to build widgets from C++ while walking the XML, which would
have meant a second implementation of everything CreateFrame already does —
parenting, naming, templates, script binding — kept in step with the first by
hand. Emitting Lua means XML frames and hand-written frames travel one path, a
template declared in XML is usable from a script without translation, and the
emitter's output is a string a test can read without a Lua state.
Environment switches
Both are off by default. Both exist because the work they enable is not finished.
WOWEE_LUA_API_FALLBACK=1
Unknown globals answer with a no-op instead of erroring, and every name asked for is logged once and listed at shutdown.
This is how a large body of Lua gets brought up: rather than guessing which of the missing functions matter, run it and collect the ones it actually reaches.
It has a real cost. Code that checks whether a function exists before using it —
which addons do constantly — sees everything as present and takes branches meant
for a different client. Names in SCREAMING_SNAKE_CASE are treated as constants
and still come back nil, because handing a function to something expecting a
number turns a missing value into a confusing type error further away.
WOWEE_LOAD_FRAMEXML=1
Loads the original interface from Interface/FrameXML/FrameXML.toc, in the
order that manifest states, before any addon.
Expect it to fail for now. It wants the fallback above on beside it to get anywhere, and what it needs is measured rather than guessed:
tools/framexml_api_gap.py <path to Interface/FrameXML>
At the time of writing that reports 1,218 missing functions across 2,537 call sites — out of 4,316 globals FrameXML calls, of which it defines 2,796 itself as it loads. The tail is very flat: the most-used missing name has 47 uses and the rest drop to about two each, so this is a long list of functions that mostly need to exist and return something sane, not a wall of hard work.
Known gaps
- Fonts are sized but not loaded.
FRIZQT__.TTFand its siblings are in the game data, but using them needs a font atlas rebuild, which cannot happen while a frame is being built. EditBox,Slider,ScrollFrameandCooldownare created as plain frames. They exist and lay out; they do not yet behave.- Only the left mouse button reaches frames.
- The texture cache never evicts.
Interface\art is small and reused, but a long session with many addons would grow it without bound.