--[[-------------------------------------------------------------------------- The modern UI core - mixins, closures and frame pools - on 3.3.5a. Ascension's own nameplate add-on (Ascension_NamePlates) is written against the modern UI framework they backported into their client - mixins, closure generation, and frame pools. None of it exists on 3.3.5a, and all of it is plain Lua 5.1 with no client support required, so it ports across unchanged apart from the notes below. Sources, read out of the donor client at G:\AscensionWoW\resources\ascension-live and resolved to the WINNING archive copy rather than the first one found: Interface\SharedXML\Util\Mixin.lua Interface\SharedXML\Util\FunctionUtil.lua Interface\SharedXML\Pools.lua Interface\SharedXML\SharedConstants.lua (nop, CallErrorHandler, OnEventToMethod) Interface\SharedXML\Util\Util.lua (UseParentLevel) Ascension's Pools.lua carries an upstream reference of its own - it is Blizzard's file from the wow-ui-source mirror - which is why WeakAuras ships a byte-similar copy. WHY THIS IS IN THE COMPAT ADD-ON AND NOT IN A NAMEPLATE ADD-ON The usual reason, and this time it is load-bearing rather than theoretical. Four add-ons in the bundle already define parts of this surface: WeakAuras ObjectPoolMixin, FramePoolMixin, CreateObjectPool, CreateFramePool TidyPlates_ThreatPlates CreateFramePool (guarded) HidingBar CreateFromMixins (guarded) PallyPower Mixin WeakAuras' Pools.lua opens with `if ObjectPoolMixin then return end`, so it is the whole FILE that steps aside, not one function - claim ObjectPoolMixin here and WeakAuras keeps a consistent set rather than a half-replaced one. Its implementation is the same Blizzard source, so either way the API matches; what must not happen is one file's ObjectPoolMixin paired with another's CreateFramePool. Everything here is behind a completeness guard, never an unconditional override, since the add-ons above are entitled to have got there first on a configuration where this add-on is somehow absent. ----------------------------------------------------------------------------]] local note = ManiaxCompat and ManiaxCompat.note if not note then -- Compat.lua did not load. Recording is a nicety; the shims are not, so -- carry on with a sink rather than failing the whole file on a dropped -- toc line. note = function() end end --------------------------------------------------------------------------- -- nop / CallErrorHandler -- -- nop is used as a value, not merely called: Ascension's nameplate code does -- `NamePlateUnitMixin.UpdateDebuffs = nop` to disable a method. A missing nop -- therefore nils out the method rather than making it harmless. --------------------------------------------------------------------------- if not nop then function nop() end noop = nop note("applied", "nop") end if not CallErrorHandler then function CallErrorHandler(...) return geterrorhandler()(...) end note("applied", "CallErrorHandler") end --------------------------------------------------------------------------- -- Mixin (Legion+) -- -- Verbatim from Ascension's Mixin.lua. Pure Lua; nothing here needs a client -- feature 3.3.5a lacks. -- -- The ...Scripts variants matter for the XML path: a template's OnLoad calls -- MixinAndLoadScripts(self, "SomeMixin"), which copies the mixin's methods -- onto the frame AND wires any of them whose names are real script handlers -- (OnEvent, OnUpdate, OnSizeChanged...) via SetScript. Without that second -- half the methods exist but nothing ever calls them, which presents as a -- frame that is created correctly and then simply never updates. --------------------------------------------------------------------------- if not Mixin then -- Takes exactly one value. Callers must capture select(i, ...) into a local -- first: a bare select() in the last argument position expands to every -- remaining value, which would spill the rest of the mixin list into this -- function's arguments. local function resolve(m) local given = m if type(m) == "string" then m = _G[m] end assert(type(m) == "table", "Mixins must be a table or a string pointing to a global table. Got Name: " .. tostring(given) .. " Type: " .. type(m)) return m end function Mixin(target, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) mixin = resolve(mixin) for k, v in pairs(mixin) do target[k] = v end end return target end function MixinSafe(target, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) mixin = resolve(mixin) for k, v in pairs(mixin) do if not target[k] then target[k] = v end end end return target end function MixinAndLoad(target, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) mixin = resolve(mixin) for k, v in pairs(mixin) do target[k] = v end if mixin.OnLoad then mixin.OnLoad(target) end end return target end function MixinAndLoadSafe(target, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) mixin = resolve(mixin) for k, v in pairs(mixin) do if not target[k] then target[k] = v end end if mixin.OnLoad then mixin.OnLoad(target) end end return target end function MixinScripts(target, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) mixin = resolve(mixin) local hasScript = target.HasScript ~= nil for k, v in pairs(mixin) do target[k] = v if hasScript and target:HasScript(k) then target:SetScript(k, v) end end end return target end function MixinScriptsSafe(target, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) mixin = resolve(mixin) local hasScript = target.HasScript ~= nil for k, v in pairs(mixin) do if not target[k] then target[k] = v end if hasScript and target:HasScript(k) and not target:GetScript(k) then target:SetScript(k, v) end end end return target end function MixinAndLoadScripts(target, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) mixin = resolve(mixin) local hasScript = target.HasScript ~= nil for k, v in pairs(mixin) do target[k] = v if hasScript and target:HasScript(k) then target:SetScript(k, v) end end if mixin.OnLoad then mixin.OnLoad(target) end end return target end function MixinAndLoadScriptsSafe(target, ...) for i = 1, select("#", ...) do local mixin = select(i, ...) mixin = resolve(mixin) local hasScript = target.HasScript ~= nil for k, v in pairs(mixin) do if not target[k] then target[k] = v end if hasScript and target:HasScript(k) and not target:GetScript(k) then target:SetScript(k, v) end end if mixin.OnLoad then mixin.OnLoad(target) end end return target end note("applied", "Mixin", "8 variants") else note("skipped", "Mixin", "already defined") end if not CreateFromMixins then function CreateFromMixins(...) return Mixin({}, ...) end function CreateFromMixinsAndLoad(...) return MixinAndLoad({}, ...) end function CreateAndInitFromMixin(mixin, ...) local object = Mixin({}, mixin) if object.Init then object:Init(...) end return object end note("applied", "CreateFromMixins") end --------------------------------------------------------------------------- -- GenerateClosure (BfA+) -- -- Binds leading arguments to a function. Ascension's nameplate driver relies -- on it to register methods as callbacks with self bound, so a missing one -- means the callback fires with the wrong first argument rather than not at -- all - the silent variety of wrong. -- -- Table-of-generators rather than a select()-based forwarder because that is -- what the original does, and because closing over the values directly avoids -- allocating a table per call on a hot path. --------------------------------------------------------------------------- if not GenerateClosure then local gen = { function(f) return function(...) return f(...) end end, function(f, a) return function(...) return f(a, ...) end end, function(f, a, b) return function(...) return f(a, b, ...) end end, function(f, a, b, c) return function(...) return f(a, b, c, ...) end end, function(f, a, b, c, d) return function(...) return f(a, b, c, d, ...) end end, function(f, a, b, c, d, e) return function(...) return f(a, b, c, d, e, ...) end end, function(fn, a, b, c, d, e, f) return function(...) return fn(a, b, c, d, e, f, ...) end end, function(fn, a, b, c, d, e, f, g) return function(...) return fn(a, b, c, d, e, f, g, ...) end end, function(fn, a, b, c, d, e, f, g, h) return function(...) return fn(a, b, c, d, e, f, g, h, ...) end end, function(fn, a, b, c, d, e, f, g, h, i) return function(...) return fn(a, b, c, d, e, f, g, h, i, ...) end end, } function GenerateClosure(f, ...) local generator = gen[select("#", ...) + 1] if generator then return generator(f, ...) end error("Closure generation does not support more than " .. (#gen - 1) .. " parameters") end function GenerateClosureSafe(f, ...) local generator = gen[select("#", ...) + 1] or gen[#gen] if generator then return generator(f, ...) end end note("applied", "GenerateClosure") end if not ExecuteFrameScript then function ExecuteFrameScript(frame, scriptName, ...) local script = frame:GetScript(scriptName) if script then xpcall(script, CallErrorHandler, frame, ...) end end note("applied", "ExecuteFrameScript") end if not CallMethodOnNearestAncestor then function CallMethodOnNearestAncestor(self, methodName, ...) local ancestor = self:GetParent() while ancestor and not ancestor[methodName] do ancestor = ancestor:GetParent() end if ancestor then return true, ancestor[methodName](ancestor, ...) end return false end note("applied", "CallMethodOnNearestAncestor") end if not GetValueOrCallFunction then function GetValueOrCallFunction(tbl, key, ...) if not tbl then return end local value = tbl[key] if type(value) == "function" then return value(...) end return value end note("applied", "GetValueOrCallFunction") end --------------------------------------------------------------------------- -- Pools (Legion+) -- -- Guarded on ObjectPoolMixin specifically, because that is the symbol -- WeakAuras' whole Pools.lua tests before deciding to step aside. Claiming -- any other name here while leaving that one unset would let both files -- define halves of the same system. -- -- One deliberate difference from Ascension's copy: their FramePoolFactory -- falls back to string.format("FramePoolFrame_%s_%s", n, time()) for frames -- whose parent is unnamed, which collides whenever two frames are created -- inside the same second. A counter cannot collide, and nothing reads these -- names for meaning - they exist only because CreateFrame wants something -- unique when a template is involved. --------------------------------------------------------------------------- if not ObjectPoolMixin then ObjectPoolMixin = {} function ObjectPoolMixin:OnLoad(creationFunc, resetterFunc) self.creationFunc = creationFunc self.resetterFunc = resetterFunc self.activeObjects = {} self.inactiveObjects = {} self.deferredRelease = {} self.numActiveObjects = 0 end function ObjectPoolMixin:Acquire() local numInactive = #self.inactiveObjects if numInactive > 0 then local obj = self.inactiveObjects[numInactive] self.activeObjects[obj] = true self.numActiveObjects = self.numActiveObjects + 1 self.inactiveObjects[numInactive] = nil self.deferredRelease[obj] = nil return obj, false end local newObj = self.creationFunc(self) if self.resetterFunc and not self.disallowResetIfNew then self.resetterFunc(self, newObj) end self.activeObjects[newObj] = true self.numActiveObjects = self.numActiveObjects + 1 return newObj, true end function ObjectPoolMixin:Release(obj) if self:IsActive(obj) then self.inactiveObjects[#self.inactiveObjects + 1] = obj self.activeObjects[obj] = nil self.numActiveObjects = self.numActiveObjects - 1 self.deferredRelease[obj] = nil if self.resetterFunc then self.resetterFunc(self, obj) end return true end return false end function ObjectPoolMixin:ReleaseAll() for obj in pairs(self.activeObjects) do self:Release(obj) end end function ObjectPoolMixin:ReleaseAllWithParent(parent) for obj in pairs(self.activeObjects) do if obj.GetParent and obj:GetParent() == parent then self:Release(obj) end end end function ObjectPoolMixin:DeferredRelease(obj) if self:IsActive(obj) then self.deferredRelease[obj] = true end end function ObjectPoolMixin:DeferredReleaseAll() for obj in pairs(self.activeObjects) do self.deferredRelease[obj] = true end end function ObjectPoolMixin:DeferredReleaseAllWithParent(parent) for obj in pairs(self.activeObjects) do if obj.GetParent and obj:GetParent() == parent then self.deferredRelease[obj] = true end end end function ObjectPoolMixin:CompleteDeferredRelease() local pending = {} for obj in pairs(self.deferredRelease) do pending[#pending + 1] = obj end for _, obj in ipairs(pending) do self:Release(obj) end end function ObjectPoolMixin:SetResetDisallowedIfNew(disallowed) self.disallowResetIfNew = disallowed end function ObjectPoolMixin:EnumerateActive() return pairs(self.activeObjects) end function ObjectPoolMixin:EnumerateActiveWithParent(parent) local current = nil return function() current = self:GetNextActive(current) while current do if current.GetParent and current:GetParent() == parent then return current end current = self:GetNextActive(current) end end, nil end function ObjectPoolMixin:GetNextActive(current) return (next(self.activeObjects, current)) end function ObjectPoolMixin:GetNextInactive(current) return (next(self.inactiveObjects, current)) end function ObjectPoolMixin:IsActive(object) return (self.activeObjects[object] ~= nil) end function ObjectPoolMixin:GetNumActive() return self.numActiveObjects end function ObjectPoolMixin:EnumerateInactive() return ipairs(self.inactiveObjects) end function CreateObjectPool(creationFunc, resetterFunc) local pool = CreateFromMixins(ObjectPoolMixin) pool:OnLoad(creationFunc, resetterFunc) return pool end FramePoolMixin = CreateFromMixins(ObjectPoolMixin) local function FramePoolFactory(framePool) if type(framePool.parent) == "string" then framePool.parent = _G[framePool.parent] end framePool.frameCount = framePool.frameCount + 1 local parentName = framePool.parent and framePool.parent.GetName and framePool.parent:GetName() local frameName if parentName then frameName = string.format("%sPoolFrame%s%d", parentName, tostring(framePool.frameTemplate), framePool.frameCount) else frameName = string.format("ManiaxFramePool_%s_%d", tostring(framePool.frameTemplate), framePool.frameCount) end return CreateFrame(framePool.frameType, frameName, framePool.parent, framePool.frameTemplate) end function FramePoolMixin:OnLoad(frameType, parent, frameTemplate, resetterFunc) ObjectPoolMixin.OnLoad(self, FramePoolFactory, resetterFunc) self.frameType = frameType self.frameCount = 0 self.parent = parent self.frameTemplate = frameTemplate end function FramePoolMixin:GetTemplate() return self.frameTemplate end function FramePool_Hide(framePool, frame) frame:Hide() end function FramePool_HideAndClearAnchors(framePool, frame) frame:Hide() frame:ClearAllPoints() end function CreateFramePool(frameType, parent, frameTemplate, resetterFunc) local pool = CreateFromMixins(FramePoolMixin) pool:OnLoad(frameType, parent, frameTemplate, resetterFunc or FramePool_HideAndClearAnchors) return pool end TexturePoolMixin = CreateFromMixins(ObjectPoolMixin) local function TexturePoolFactory(texturePool) if type(texturePool.parent) == "string" then texturePool.parent = _G[texturePool.parent] end return texturePool.parent:CreateTexture(nil, texturePool.layer, texturePool.textureTemplate) end function TexturePoolMixin:OnLoad(parent, layer, textureTemplate, resetterFunc) ObjectPoolMixin.OnLoad(self, TexturePoolFactory, resetterFunc) self.parent = parent self.layer = layer self.textureTemplate = textureTemplate end function TexturePool_Hide(texturePool, texture) texture:Hide() end function TexturePool_HideAndClearAnchors(texturePool, texture) texture:Hide() texture:ClearAllPoints() end function CreateTexturePool(parent, layer, textureTemplate, resetterFunc) local pool = CreateFromMixins(TexturePoolMixin) pool:OnLoad(parent, layer, textureTemplate, resetterFunc or TexturePool_HideAndClearAnchors) return pool end note("applied", "Pools", "Object/Frame/Texture") else note("skipped", "Pools", "ObjectPoolMixin already defined") end --------------------------------------------------------------------------- -- Frame helpers used by Ascension's templates. --------------------------------------------------------------------------- if not UseParentLevel then function UseParentLevel(frame) local parent = frame:GetParent() if parent then frame:SetFrameLevel(parent:GetFrameLevel()) end end note("applied", "UseParentLevel") end -- OnEventToMethod: the XML dispatch idiom Ascension uses everywhere, as -- . The frame gets one method per event -- name and this routes to it. Absent, every such frame silently handles no -- events at all, since the XML attribute resolves to nil and the client -- installs no handler. if not OnEventToMethod then function OnEventToMethod(self, event, ...) if self[event] then self[event](self, ...) end end note("applied", "OnEventToMethod") end