A Scalebane Royal Guard
Join Date: Mar 2015
Posts: 431
|
Table-based frame markup
I guess this idea isn't completely new (thinking about AceConfig for example), but I created a crossover between XML and Lua that is akin to JSON. By using tables and a special parser, it essentially does the job of XML but without any XML. I'm using this to create frames and templates for my controller UI with slightly different syntax. Figured I'd post it here to see if there's any interest in releasing this as a library. Feedback is welcome.
What are the upsides?- Full control of the API syntax, no need to adhere to XML rules.
- Fully recursive; you can create an entire UI with just one big nested table.
- Unlike XML, regions don't need to be sorted by layer.
- Supports anchoring to relative regions.
- Runs constructors at the end of building the frame.
- Templates and frames can be local.
- Automatically adds and hooks scripts on widgets by using an extended mixin function.
- Metatable functions can be called directly within the table that creates the widget.
- Any arbitrary data (not a metatable function, API call or script) found within a table is moved over to the widget itself.
- Indentation is optional and is entirely up to the author.
- Generally cleaner code and easier to debug.
- Tables are collapsible in most code editors that support Lua.
What are the downsides?- Extra overhead and garbage generation.
- Tends to lead to more lines in the code, depending on indentation.
- Depends on a lot of packing, unpacking and repacking of tables to unify the syntax.
- By default, the numerical index of 1 denotes a table of children.
- By default, children will always be named and therefore globally accessible.
- Children cannot be anonymous.
- A child's name and its parent frame key is always the same.
To show you how this works, I'll start by comparing a regular XML template (ActionButtonTemplate) and a rewritten version that uses the table parser.
XML version:
XML Code:
<CheckButton name="ActionButtonTemplate" virtual="true"> <Size> <AbsDimension x="36" y="36"/> </Size> <Layers> <Layer level="BACKGROUND"> <Texture name="$parentIcon" parentKey="icon"/> </Layer> <Layer level="ARTWORK" textureSubLevel="1"> <Texture name="$parentFlash" parentKey="Flash" file="Interface\Buttons\UI-QuickslotRed" hidden="true"/> <Texture name="$parentFlyoutBorder" inherits="ActionBarFlyoutButton-IconFrame" parentKey="FlyoutBorder" hidden="true"> <Anchors> <Anchor point="CENTER"/> </Anchors> </Texture> <Texture name="$parentFlyoutBorderShadow" inherits="ActionBarFlyoutButton-IconShadow" parentKey="FlyoutBorderShadow" hidden="true"> <Anchors> <Anchor point="CENTER"/> </Anchors> </Texture> </Layer> <Layer level="ARTWORK" textureSubLevel="2"> <Texture name="$parentFlyoutArrow" inherits="ActionBarFlyoutButton-ArrowUp" parentKey="FlyoutArrow" hidden="true"/> <FontString name="$parentHotKey" inherits="NumberFontNormalSmallGray" parentKey="HotKey" justifyH="RIGHT"> <Size x="36" y="10"/> <Anchors> <Anchor point="TOPLEFT" x="1" y="-3"/> </Anchors> </FontString> <FontString name="$parentCount" inherits="NumberFontNormal" parentKey="Count" justifyH="RIGHT"> <Anchors> <Anchor point="BOTTOMRIGHT" x="-2" y="2"/> </Anchors> </FontString> </Layer> <Layer level="OVERLAY"> <FontString name="$parentName" parentKey="Name" inherits="GameFontHighlightSmallOutline"> <Size x="36" y="10"/> <Anchors> <Anchor point="BOTTOM" x="0" y="2"/> </Anchors> </FontString> <Texture name="$parentBorder" file="Interface\Buttons\UI-ActionButton-Border" parentKey="Border" hidden="true" alphaMode="ADD"> <Size x="62" y="62"/> <Anchors> <Anchor point="CENTER"/> </Anchors> </Texture> </Layer> <Layer level="OVERLAY" textureSubLevel="1"> <Texture parentKey="NewActionTexture" atlas="bags-newitem" useAtlasSize="false" alphaMode="ADD" hidden="true"> <Size x="44" y="44"/> <Anchors> <Anchor point="CENTER"/> </Anchors> </Texture> <Texture parentKey="SpellHighlightTexture" atlas="bags-newitem" useAtlasSize="false" alphaMode="ADD" hidden="true"> <Size x="44" y="44"/> <Anchors> <Anchor point="CENTER"/> </Anchors> </Texture> <Texture parentKey="AutoCastable" file="Interface\Buttons\UI-AutoCastableOverlay" hidden="true"> <Size x="58" y="58"/> <Anchors> <Anchor point="CENTER" x="0" y="0"/> </Anchors> </Texture> </Layer> </Layers> <Animations> <AnimationGroup parentKey="SpellHighlightAnim" looping="REPEAT"> <Alpha childKey="SpellHighlightTexture" smoothing="OUT" duration=".35" order="1" fromAlpha="0" toAlpha="1"/> <Alpha childKey="SpellHighlightTexture" smoothing="IN" duration=".35" order="2" fromAlpha="1" toAlpha="0"/> </AnimationGroup> </Animations> <Frames> <Frame name="$parentShine" parentKey="AutoCastShine" inherits="AutoCastShineTemplate"> <Anchors> <Anchor point="CENTER" x="0" y="0"/> </Anchors> <Size x="28" y="28"/> </Frame> <Cooldown name="$parentCooldown" inherits="CooldownFrameTemplate" parentKey="cooldown"> <Size x="36" y="36"/> <Anchors> <Anchor point="CENTER" x="0" y="-1"/> </Anchors> <SwipeTexture> <Color r="1" g="1" b="1" a="0.8"/> </SwipeTexture> </Cooldown> </Frames> <NormalTexture name="$parentNormalTexture" parentKey="NormalTexture" file="Interface\Buttons\UI-Quickslot2"> <Anchors> <Anchor point="TOPLEFT" x="-15" y="15"/> <Anchor point="BOTTOMRIGHT" x="15" y="-15"/> </Anchors> </NormalTexture> <PushedTexture file="Interface\Buttons\UI-Quickslot-Depress"/> <HighlightTexture alphaMode="ADD" file="Interface\Buttons\ButtonHilight-Square"/> <CheckedTexture alphaMode="ADD" file="Interface\Buttons\CheckButtonHilight"/> </CheckButton>
Lua version:
Lua Code:
local ActionButtonTemplate = { _size = {36, 36}, SetPushedTexture = [[Interface\Buttons\UI-Quickslot-Depress]]; SetHighlightTexture = {[[Interface\Buttons\ButtonHilight-Square]], 'ADD'}; SetCheckedTexture = {[[Interface\Buttons\CheckButtonHilight]], 'ADD'}; OnLoad = function(self) self:SetNormalTexture(self.NormalTexture); end, { icon = { _type = 'Texture'; _setup = {'BACKGROUND'}, }, Flash = { _type = 'Texture'; _setup = {'ARTWORK', nil, 1}; _texture = [[Interface\Buttons\UI-QuickslotRed]]; _hide = true; _point = {'CENTER', 0, 0}; }, FlyoutBorder = { _type = 'Texture'; _setup = {'ARTWORK', 'ActionBarFlyoutButton-IconFrame', 1}; _hide = true; _point = {'CENTER', 0, 0}; }, FlyoutBorderShadow = { _type = 'Texture'; _setup = {'ARTWORK', 'ActionBarFlyoutButton-IconShadow', 1}; _hide = true; _point = {'CENTER', 0, 0}; }, FlyoutArrow = { _type = 'Texture'; _setup = {'ARTWORK', 'ActionBarFlyoutButton-ArrowUp', 2}; _hide = true; }, HotKey = { _type = 'FontString'; _setup = {'ARTWORK', 'NumberFontNormalSmallGray', 2}; _justifyH = 'RIGHT'; _size = {36, 10}; _point = {'TOPLEFT', 1, -3}; }, Count = { _type = 'FontString'; _setup = {'ARTWORK', 'NumberFontNormal', 2}; _point = {'BOTTOMRIGHT', -2, 2}; }, Name = { _type = 'FontString'; _setup = {'OVERLAY', 'GameFontHighlightSmallOutline'}; _size = {36, 10}; _point = {'BOTTOM', 0, 2}; }, Border = { _type = 'Texture'; _setup = {'OVERLAY'}; _blend = 'ADD'; _texture = [[Interface\Buttons\UI-ActionButton-Border]]; _hide = true; _size = {62, 62}; _point = {'CENTER', 0, 0}; }, NewActionTexture = { _type = 'Texture'; _setup = {'OVERLAY', nil, 1}; _atlas = 'bags-newitem'; _hide = true; _size = {44, 44}; _point = {'CENTER', 0, 0}; }, SpellHighlightTexture = { _type = 'Texture'; _setup = {'OVERLAY', nil, 1}; _atlas = 'bags-newitem'; _blend = 'ADD'; _hide = true; _size = {44, 44}; _point = {'CENTER', 0, 0}; }, AutoCastable = { _type = 'Texture'; _setup = {'OVERLAY', nil, 1}; _texture = [[Interface\Buttons\UI-AutoCastableOverlay]]; _size = {58, 58}; _hide = true; _point = {'CENTER', 0, 0}; }, SpellHighlightAnim = { _type = 'AnimationGroup'; SetLooping = 'REPEAT'; { inAnim = { _type = 'Animation'; _setup = 'Alpha'; SetChildKey = 'SpellHighlightTexture'; SetSmoothing = 'OUT'; SetDuration = .35; SetOrder = 1; SetFromAlpha = 0; SetToAlpha = 1; }, outAnim = { _type = 'Animation'; _setup = 'Alpha'; SetChildKey = 'SpellHighlightTexture'; SetSmoothing = 'IN'; SetDuration = .35; SetOrder = 2; SetFromAlpha = 1; SetToAlpha = 0; }, }, }, AutoCastShine = { _type = 'Frame'; _setup = {'AutoCastShineTemplate'}; _point = {'CENTER', 0, 0}; _size = {28, 28}; }, cooldown = { _type = 'Cooldown'; _setup = {'CooldownFrameTemplate'}; _size = {36, 36}; _point = {'CENTER', 0, -1}; SetSwipeColor = {1, 1, 1, .8}; }, NormalTexture = { _type = 'Texture'; _texture = [[Interface\Buttons\UI-Quickslot2]]; _points = { {'TOPLEFT', -15, 15}; {'BOTTOMRIGHT', 15, -15}; }, }, }, }
Personally, I find the second version a lot easier to read and modify, especially since the only hierarchical indentation is how a child relates to its parent. Note that there's no split between frames, regions or animation widgets, so they can be organised however you want. I'm currently using this for quite a few different frames and the performance dent is not as bad as I expected when I started tinkering with it. Having that said, this is most likely slower than XML and definitely slower than manually creating all your frames directly in Lua.
Here's the source code:
Lua Code:
---------------------------------------------------------------- local assert, pairs, ipairs, type, unpack, wipe, tconcat = assert, pairs, ipairs, type, unpack, wipe, table.concat ---------------------------------------------------------------- local CreateFrame = CreateFrame ---------------------------------------------------------------- local anchor, load, call, mixin, callMethodsOnRegion, -- func calls pop, popMulti, addSubTable, -- table operations err, extractBuildInfo, getRelative -- misc ---------------------------------------------------------------- local ARGS, ERROR, ERROR_CODES, REGION, API, BUILDINFO local ANCHORS, CONSTRUCTORS = {}, {} ---------------------------------------------------------------- --- Create a new frame. -- @param object : Type of object to be created. Frame or inherited therefrom. -- @param name : Name of the object to be created. -- @param parent : Parent of the object. -- @param xml : Inherited xml. -- @param blueprint : Table consisting of additional regions to be created. -- @return frame : Returns the created object. function CreateTableFrame(object, name, parent, xml, blueprint, recursive) ---------------------------------- local frame = CreateFrame(object, name, parent, xml) ---------------------------------- if blueprint then if recursive then BuildFromTable(frame, blueprint, true) else local children = pop(blueprint, 1) callMethodsOnRegion(frame, blueprint) BuildFromTable(frame, children, true) end end ---------------------------------- if not recursive then anchor() load() end return frame end --- Build frame from blueprint. -- @param frame : Parent of the blueprint. -- @param blueprint : Blueprint to be constructed. -- @param recursive : Whether this is a recursive call. -- @return frame : Returns the altered frame. function BuildFromTable(frame, blueprint, recursive) assert(type(blueprint) == 'table', err('Blueprint', frame:GetName(), ERROR_CODES.BLUEPRINT)) for key, config in pairs(blueprint) do assert(type(config) == 'table', err(key, frame:GetName(), ERROR_CODES.CONFIGTABLE)) ---------------------------------- local object, objectType, buildInfo, isLoop = extractBuildInfo(config) ---------------------------------- for i = 1, ( isLoop or 1 ) do local key = ( isLoop and key..i ) or key local region = frame[key] if not region then ---------------------------------- if buildInfo then -- Assert type exists if setup table exists. assert(object, err(key, name, ERROR_CODES.REGION)) end ---------------------------------- if object then -- Region type has special constructor. if REGION[object] then region = REGION[object](frame, key, buildInfo) -- Region already exists. elseif objectType == 'table' then region = REGION.Existing(frame, key, object) -- Region should be a type of frame. elseif objectType == 'string' then region = CreateTableFrame(object, '$parent'..key, frame, buildInfo and tconcat(buildInfo, ', '), pop(config, 1), true) end else -- Assume this is a data table. region = config end ---------------------------------- frame[key] = region end if isLoop and region.SetID then region:SetID(i) end callMethodsOnRegion(region, config) end end -- parse if explicitly called without wrapping (building on top of existing frame) if not recursive then anchor() load() end return frame end ---------------------------------- function call(region, method, data) if data == 'nil' then data = nil end local func = API[method] or region[method] if type(func) == 'function' then -- if sequential array, just unpack it. if ( type(data) == 'table' and #data ~= 0 ) then return func(region, unpack(data)) else return func(region, data) end elseif type(data) == 'function' and region.HasScript and region:HasScript(method) then if region:GetScript(method) then region:HookScript(method, data) else region:SetScript(method, data) end else region[method] = data end end function callMethodsOnRegion(region, methods) -- mixin before running the rest of the region method stack, -- since mixed in functions may be called from the blueprint local mixin = pop(methods, '_mixin') if mixin then call(region, '_mixin', mixin) -- if the mixin has an onload script, add it to the constructor stack. -- remove the onload function from the object itself. if region.OnLoad and not methods.OnLoad then -- use :GetScript in case more than one load script was hooked. methods.OnLoad = region:GetScript('OnLoad') region:SetScript('OnLoad', nil) region.OnLoad = nil end end for method, data in pairs(methods) do call(region, method, data) end end ---------------------------------- function getRelative(region, relative) if type(relative) == 'table' then return relative elseif type(relative) == 'string' then local searchResult for key in relative:gmatch('%a+') do if key == 'parent' then searchResult = searchResult and searchResult:GetParent() or region:GetParent() elseif searchResult then searchResult = searchResult[key] else searchResult = region[key] end end return searchResult else err('Relative region', region:GetName(), ERROR.CODES.RELATIVEREGION) end end function anchor() for _, setup in ipairs(ANCHORS) do local numArgs = #setup if numArgs == 2 then local region, point = unpack(setup) region:SetPoint(point) elseif numArgs == 4 then local region, point, xOffset, yOffset = unpack(setup) region:SetPoint(point, xOffset, yOffset) elseif numArgs == 6 then local region, point, relativeRegion, relativePoint, xOffset, yOffset = unpack(setup) region:SetPoint(point, getRelative(region, relativeRegion), relativePoint, xOffset, yOffset) end end wipe(ANCHORS) end function load() for _, setup in ipairs(CONSTRUCTORS) do local region, constructor = unpack(setup) assert(type(constructor) == 'function', err('Constructor', region:GetName(), ERROR_CODES.CONSTRUCTOR)) constructor(region) end wipe(CONSTRUCTORS) end local function gMixin(object, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) for k, v in pairs(mixin) do object[k] = v end end return object end function mixin(t, ...) t = gMixin(t, ...) if t.HasScript then for k, v in pairs(t) do if t:HasScript(k) then if t:GetScript(k) then t:HookScript(k, v) else t:SetScript(k, v) end end end end end function extractBuildInfo(bp) local fType, buildInfo, isLoop = popMulti(bp, unpack(BUILDINFO)) return fType, type(fType), buildInfo, isLoop end ---------------------------------- function popMulti(tbl, ...) local popped = {...} for i, key in pairs(popped) do popped[i] = tbl[key] or false tbl[key] = nil end return unpack(popped) end function pop(tbl, key) local val = tbl[key] tbl[key] = nil return val end function addSubTable(tbl, ...) tbl[#tbl + 1] = {...} end ---------------------------------- ARGS = { ---------------------------------- MULTI_RUN = 'function name on object as key, table of values to be unpacked into key function. Nesting allowed.', REGION = 'Region key should be of type string and refer to a valid widget type.', BLUEPRINT = '(frame, blueprint); blueprint = { child1 = {}, child2 = {}, ..., childN = {} }', RELATIVEREGION = '(frame or string). Example of string: $parent.Sibling', }--------------------------------- ---------------------------------- ERROR = '%s in %s %s.' ---------------------------------- ERROR_CODES = { ---------------------------------- MULTI_FUNC = 'does not exist. Loop table should contain: '..ARGS.MULTI_RUN, MULTI_TABLE = 'has an invalid loop table. Loop table should contain: '..ARGS.MULTI_RUN, REGION = 'missing region type. '..ARGS.REGION, RELATIVEREGION = 'is invalid. Type should be a parsable string or existing frame. Arguments: '..ARGS.RELATIVEREGION, CONSTRUCTOR = 'is invalid. Constructor must be a function.', BLUEPRINT = 'is invalid. Blueprint should be a nested table. Arguments: '..ARGS.BLUEPRINT, CONFIGTABLE = 'is not a valid config table or existing region.', }--------------------------------- function err(key, name, code) return ERROR:format(key, name or 'unnamed region', code) end -- Special constructors ---------------------------------- REGION = { ---------------------------------- AnimationGroup = function(parent, key, setup) return parent:CreateAnimationGroup('$parent'..key, setup and unpack(setup)) end, Animation = function(parent, key, setup) return parent:CreateAnimation(setup, '$parent'..key) end, FontString = function(parent, key, setup) return parent:CreateFontString('$parent'..key, setup and unpack(setup)) end, Texture = function(parent, key, setup) return parent:CreateTexture('$parent'..key, setup and unpack(setup)) end, --- Existing = function(parent, key, region) _G[parent:GetName()..key] = region region:SetParent(parent) return region end }--------------------------------- ---------------------------------- BUILDINFO = { ---------------------------------- '_type', -- type of object to be created. '_setup', -- denotes the optional args to be passed to constructor. '_repeat', -- denotes whether multiple regions should be created. } -- API listing ---------------------------------- API = { ---------------------------------- [1] = function(frame, blueprint) BuildFromTable(frame, blueprint, true) end, _id = function(region, ...) region:SetID(...) end, --- Texture _atlas = function(texture, ...) texture:SetAtlas(...) end, _blend = function(texture, ...) texture:SetBlendMode(...) end, _coords = function(texture, ...) texture:SetTexCoord(...) end, _gradient = function(texture, ...) texture:SetGradientAlpha(...) end, _texture = function(texture, ...) texture:SetTexture(...) end, --- FontString _justifyH = function(fontString, ...) fontString:SetJustifyH(...) end, _justifyV = function(fontString, ...) fontString:SetJustifyV(...) end, _color = function(fontString, ...) fontString:SetTextColor(...) end, _font = function(fontString, ...) fontString:SetFont(...) end, _fontH = function(fontString, ...) fontString:SetHeight(...) end, _text = function(fontString, ...) fontString:SetText(...) end, --- LayeredRegion _layer = function(region, ...) region:SetDrawLayer(...) end, _Vertex = function(region, ...) region:SetVertexColor(...) end, --- Frame _attrib = function(frame, attributes) for k, v in pairs(attributes) do frame:SetAttribute(k, v) end end, _backdrop = function(frame, backdrop) frame:SetBackdrop(backdrop) end, _events = function(frame, ...) for _, v in ipairs({...}) do frame:RegisterEvent(v) end end, _hooks = function(frame, scripts) for k, v in pairs(scripts) do frame:HookScript(k, v) end end, _level = function(frame, ...) frame:SetFrameLevel(...) end, _strata = function(frame, ...) frame:SetFrameStrata(...) end, _scripts = function(frame, scripts) for k, v in pairs(scripts) do frame:SetScript(k, v) end end, --- Region _alpha = function(region, ...) region:SetAlpha(...) end, _clear = function(region) region:ClearAllPoints() end, _fill = function(region, target) region:SetAllPoints(target ~= true and getRelative(region, target)) end, _height = function(region, ...) region:SetHeight(...) end, _hide = function(region, ...) region:Hide() end, _show = function(region, ...) region:Show() end, _size = function(region, ...) region:SetSize(...) end, _scale = function(region, ...) region:SetScale(...) end, _width = function(region, ...) region:SetWidth(...) end, --- Button _click = function(button, ...) button:SetAttribute('type', 'click') button:SetAttribute('clickbutton', ...) end, _macro = function(button, ...) button:SetAttribute('type', 'macro') button:SetAttribute('macrotext', ...) end, _action = function(button, ...) button:SetAttribute('type', 'action') button:SetAttribute('action', ...) end, _spell = function(button, ...) button:SetAttribute('type', 'spell') button:SetAttribute('spell', ...) end, _unit = function(button, ...) button:SetAttribute('type', 'target') button:SetAttribute('unit', ...) end, _item = function(button, ...) button:SetAttribute('type', 'item') button:SetAttribute('item', ...) end, --- Constructor (faux event to replicate XML) OnLoad = function(region, ...) addSubTable(CONSTRUCTORS, region, ...) end, --- Points _point = function(region, ...) addSubTable(ANCHORS, region, ...) end, _points = function(region, ...) for _, point in ipairs({...}) do addSubTable(ANCHORS, region, unpack(point)) end end, -- Mixin _mixin = function(region, ...) mixin(region, ...) end, --- Multiple runs _multiple = function(region, multiTable) for k, v in pairs(multiTable) do assert(region[k] or API[k], err(k, region:GetName(), ERROR_CODES.MULTI_FUNC)) assert(type(v) == 'table', err(k, region:GetName(), ERROR_CODES.MULTI_TABLE)) for _, args in pairs(v) do call(region, k, args) end end end, }---------------------------------
__________________
Last edited by MunkDev : 08-31-17 at 03:30 PM.
|