Files
NoM0Re 1836ba84d8 (fix/WeakAuras): Resolve long-standing frame level overflow
Switches frame level assignment to depth-based ordering to avoid overflow
and ensure deterministic ordering. WeakAurasFrame now correctly lives on
FrameStrata MEDIUM without interfering with Blizzard UI elements.

(cherry picked from commit e92edf5700ad70587a71c3a403e5cc672dbc9e8e)
2026-02-16 11:38:32 -07:00

1624 lines
53 KiB
Lua

if not WeakAuras.IsLibsOK() then return end
local AddonName = ...
local Private = select(2, ...)
local WeakAuras = WeakAuras
local L = WeakAuras.L
local SharedMedia = LibStub("LibSharedMedia-3.0")
local default = {
controlledChildren = {},
border = false,
borderColor = {0, 0, 0, 1},
backdropColor = {1, 1, 1, 0.5},
borderEdge = "Square Full White",
borderOffset = 4,
borderInset = 1,
borderSize = 2,
borderBackdrop = "Blizzard Tooltip",
grow = "DOWN",
selfPoint = "TOP",
align = "CENTER",
space = 2,
stagger = 0,
sort = "none",
animate = false,
anchorPoint = "CENTER",
anchorFrameType = "SCREEN",
xOffset = 0,
yOffset = 0,
radius = 200,
rotation = 0,
stepAngle = 15,
fullCircle = true,
arcLength = 360,
constantFactor = "RADIUS",
frameStrata = 1,
scale = 1,
useLimit = false,
limit = 5,
gridType = "RD",
centerType = "LR",
gridWidth = 5,
rowSpace = 1,
columnSpace = 1,
sharedFrameLevel = true, -- true to ensure identical behavior on newer clients
}
Private.regionPrototype.AddAlphaToDefault(default);
local controlPointFunctions = {
["SetAnchorPoint"] = function(self, point, relativeFrame, relativePoint, offsetX, offsetY)
self:ClearAllPoints();
self.point, self.relativeFrame, self.relativePoint, self.offsetX, self.offsetY
= point, relativeFrame, relativePoint, offsetX, offsetY
self.totalOffsetX = (self.animOffsetX or 0) + (self.offsetX or 0)
self.totalOffsetY = (self.animOffsetY or 0) + (self.offsetY or 0)
if self.relativeFrame and self.relativePoint then
self:SetPoint(self.point, self.relativeFrame, self.relativePoint, self.totalOffsetX, self.totalOffsetY)
else
self:SetPoint(self.point, self.totalOffsetX, self.totalOffsetY)
end
end,
["ClearAnchorPoint"] = function(self)
self.point, self.relativeFrame, self.relativePoint, self.offsetX, self.offsetY = nil, nil, nil, nil, nil
end,
["ReAnchor"] = function(self, frame)
self:ClearAllPoints()
self.relativeFrame = frame
if self.relativeFrame and self.relativePoint then
self:SetPoint(self.point, self.relativeFrame, self.relativePoint, self.totalOffsetX, self.totalOffsetY)
else
self:SetPoint(self.point, self.totalOffsetX, self.totalOffsetY)
end
end,
["SetOffsetAnim"] = function(self, x, y)
self.animOffsetX, self.animOffsetY = x, y
self.totalOffsetX = (self.animOffsetX or 0) + (self.offsetX or 0)
self.totalOffsetY = (self.animOffsetY or 0) + (self.offsetY or 0)
if not self.point then
-- Nothing to do
elseif self.relativeFrame and self.relativePoint then
self:SetPoint(self.point, self.relativeFrame, self.relativePoint, self.totalOffsetX, self.totalOffsetY)
else
self:SetPoint(self.point, self.totalOffsetX, self.totalOffsetY)
end
end
}
local function createControlPoint(self)
local controlPoint = CreateFrame("Frame", nil, self.parent)
WeakAuras.Mixin(controlPoint, controlPointFunctions)
controlPoint:SetWidth(16)
controlPoint:SetHeight(16)
controlPoint:Show()
controlPoint:SetAnchorPoint(self.parent.selfPoint)
return controlPoint
end
local function releaseControlPoint(self, controlPoint)
controlPoint:Hide()
controlPoint:SetAnchorPoint(self.parent.selfPoint)
local regionData = controlPoint.regionData
if regionData then
if self.parent.anchorPerUnit == "UNITFRAME" then
Private.dyngroup_unitframe_monitor[regionData] = nil
end
controlPoint.regionData = nil
regionData.controlPoint = nil
end
end
local function create(parent)
local region = CreateFrame("Frame", nil, parent)
region.regionType = "dynamicgroup"
region:SetSize(16, 16)
region:SetMovable(true)
region.sortedChildren = {}
region.controlledChildren = {}
region.updatedChildren = {}
region.sortStates = {}
region.growStates = {}
local background = CreateFrame("Frame", nil, region)
region.background = background
region.selfPoint = "TOPLEFT"
region.controlPoints = CreateObjectPool(createControlPoint, releaseControlPoint)
region.controlPoints.parent = region
Private.regionPrototype.create(region)
region.suspended = 0
local oldSetFrameLevel = region.SetFrameLevel
region.SetFrameLevel = function(self, level)
oldSetFrameLevel(self, level)
self.background:SetFrameLevel(level)
end
return region
end
function WeakAuras.GetPolarCoordinates(x, y, originX, originY)
local dX, dY = x - originX, y - originY;
local r = math.sqrt(dX * dX + dY * dY);
local theta = atan2(dY, dX);
return r, theta;
end
function WeakAuras.InvertSort(sortFunc)
-- takes a comparator and returns the "inverse"
-- i.e. when sortFunc returns true/false, inverseSortFunc returns false/true
-- nils are preserved to ensure that inverseSortFunc composes well
if type(sortFunc) ~= "function" then
error("InvertSort requires a function to invert.")
else
return function(...)
local result = sortFunc(...)
if result == nil then return nil end
return not result
end
end
end
function WeakAuras.SortNilLast(a, b)
-- sorts nil values to the end
-- only returns nil if both values are non-nil
-- Useful as a high priority sorter in a composition,
-- to ensure that children with missing data
-- don't ever sit in the middle of a row
-- and interrupt the sorting algorithm
if a == nil and b == nil then
-- guarantee stability in the nil region
return false
elseif a == nil then
return false
elseif b == nil then
return true
else
return nil
end
end
local sortNilFirst = WeakAuras.InvertSort(WeakAuras.SortNilLast)
function WeakAuras.SortNilFirst(a, b)
if a == nil and b == nil then
-- we want SortNil to always prevent nils from propagating
-- as well as to sort nils onto one side
-- to maintain stability, we need SortNil(nil, nil) to always be false
-- hence this special case
return false
else
return sortNilFirst(a,b)
end
end
function WeakAuras.SortGreaterLast(a, b)
-- sorts values in ascending order
-- values of disparate types are sorted according to the value of type(value)
-- which is a bit weird but at least guarantees a stable sort
-- can only sort comparable values (i.e. numbers and strings)
-- no support currently for tables with __lt metamethods
if a == b then
return nil
end
if type(a) ~= type(b) then
return type(a) > type(b)
end
if type(a) == "number" then
if abs(b - a) < 0.001 then
return nil
else
return a < b
end
elseif type(a) == "string" then
return a < b
else
return nil
end
end
WeakAuras.SortGreaterFirst = WeakAuras.InvertSort(WeakAuras.SortGreaterLast)
function WeakAuras.SortRegionData(path, sortFunc)
-- takes an array-like table, and a function that takes 2 values and returns true/false/nil
-- creates function that accesses the value indicated by path, and compares using sortFunc
if type(path) ~= "table" then
path = {}
end
if type(sortFunc) ~= "function" then
-- if sortFunc not provided, compare by default as "<"
sortFunc = WeakAuras.SortGreaterLast
end
return function(a, b)
local aValue, bValue = a, b
for _, key in ipairs(path) do
if type(aValue) ~= "table" then return nil end
if type(bValue) ~= "table" then return nil end
aValue, bValue = aValue[key], bValue[key]
end
return sortFunc(aValue, bValue)
end
end
function WeakAuras.SortAscending(path)
return WeakAuras.SortRegionData(path, WeakAuras.ComposeSorts(WeakAuras.SortNilFirst, WeakAuras.SortGreaterLast))
end
function WeakAuras.SortDescending(path)
return WeakAuras.InvertSort(WeakAuras.SortAscending(path))
end
function WeakAuras.ComposeSorts(...)
-- accepts vararg of sort funcs
-- returns new sort func that combines the functions passed in
-- order of functions passed in determines their priority in new sort
-- returns nil if all functions return nil,
-- so that it can be composed or inverted without trouble
local sorts = {}
for i = 1, select("#", ...) do
local sortFunc = select(i, ...)
if type(sortFunc) == "function" then
tinsert(sorts, sortFunc)
end
end
return function(a, b)
for _, sortFunc in ipairs(sorts) do
local result = sortFunc(a, b)
if result ~= nil then
return result
end
end
return nil
end
end
local function noop() end
local sorters = {
none = function(data)
return WeakAuras.ComposeSorts(
WeakAuras.SortAscending({"dataIndex"}),
WeakAuras.SortAscending({"region", "state", "index"})
), { index = true }
end,
hybrid = function(data)
local sortHybridTable = data.sortHybridTable or {}
local hybridSortAscending = data.hybridSortMode == "ascending"
local hybridFirst = data.hybridPosition == "hybridFirst"
local function sortHybridStatus(a, b)
if not b then return true end
if not a then return false end
local aIsHybrid = sortHybridTable[a.id]
local bIsHybrid = sortHybridTable[b.id]
if aIsHybrid and not bIsHybrid then
return hybridFirst
elseif bIsHybrid and not aIsHybrid then
return not hybridFirst
else
return nil
end
end
local sortExpirationTime
if hybridSortAscending then
sortExpirationTime = WeakAuras.SortAscending({"region", "state", "expirationTime"})
else
sortExpirationTime = WeakAuras.SortDescending({"region", "state", "expirationTime"})
end
return WeakAuras.ComposeSorts(
sortHybridStatus,
sortExpirationTime,
WeakAuras.SortAscending({"dataIndex"})
), {expirationTime = true}
end,
ascending = function(data)
return WeakAuras.ComposeSorts(
WeakAuras.SortAscending({"region", "state", "expirationTime"}),
WeakAuras.SortAscending({"dataIndex"})
), {expirationTime = true}
end,
descending = function(data)
return WeakAuras.ComposeSorts(
WeakAuras.SortDescending({"region", "state", "expirationTime"}),
WeakAuras.SortAscending({"dataIndex"})
), {expirationTime = true}
end,
custom = function(data)
local sortStr = data.customSort or ""
local sortFunc = WeakAuras.LoadFunction("return " .. sortStr, data.id) or noop
local sortOn = nil
local events = WeakAuras.split(data.sortOn or "")
if #events > 0 then
sortOn = {}
for _, event in ipairs(events) do
sortOn[event] = true
end
end
return function(a, b)
Private.ActivateAuraEnvironment(data.id)
local ok, result = pcall(sortFunc, a, b)
Private.ActivateAuraEnvironment()
if ok then
return result
else
Private.GetErrorHandlerId(data.id, L["Custom Sort"])
end
end, sortOn
end
}
WeakAuras.SortFunctions = sorters
local function createSortFunc(data)
local sorter = sorters[data.sort] or sorters.none
return sorter(data)
end
local function polarToRect(r, theta)
return r * math.cos(theta), r * math.sin(theta)
end
local function staggerCoefficient(alignment, stagger)
if alignment == "LEFT" then
if stagger < 0 then
return 1
else
return 0
end
elseif alignment == "RIGHT" then
if stagger > 0 then
return 1
else
return 0
end
else
return 0.5
end
end
local anchorers = {
["NAMEPLATE"] = function(data)
return function(frames, activeRegions)
for _, regionData in ipairs(activeRegions) do
local unit = regionData.region.state and regionData.region.state.unit
local found
if unit then
local frame = WeakAuras.GetNamePlateForUnit(unit)
if frame then
frames[frame] = frames[frame] or {}
tinsert(frames[frame], regionData)
found = true
end
end
if not found and WeakAuras.IsOptionsOpen() and regionData.region.state then
Private.ensurePRDFrame()
Private.personalRessourceDisplayFrame:anchorFrame(regionData.region.state.id, "NAMEPLATE")
frames[Private.personalRessourceDisplayFrame] = frames[Private.personalRessourceDisplayFrame] or {}
tinsert(frames[Private.personalRessourceDisplayFrame], regionData)
end
end
end, {unit = true }
end,
["UNITFRAME"] = function(data)
return function(frames, activeRegions)
for _, regionData in ipairs(activeRegions) do
local unit = regionData.region.state and regionData.region.state.unit
if unit then
local frame = WeakAuras.GetUnitFrame(unit) or WeakAuras.HiddenFrames
if frame then
frames[frame] = frames[frame] or {}
tinsert(frames[frame], regionData)
end
end
end
end, {unit = true }
end,
["CUSTOM"] = function(data)
local anchorStr = data.customAnchorPerUnit or ""
local anchorFunc = WeakAuras.LoadFunction("return " .. anchorStr, data.id) or noop
local anchorOn = nil
local events = WeakAuras.split(data.anchorOn or "")
if #events > 0 then
anchorOn = {}
for _, event in ipairs(events) do
anchorOn[event] = true
end
end
return function(frames, activeRegions)
Private.ActivateAuraEnvironment(data.id)
local ok = pcall(anchorFunc, frames, activeRegions)
if not ok then
Private.GetErrorHandlerUid(data.uid, L["Custom Anchor"])
end
Private.ActivateAuraEnvironment()
end, anchorOn
end
}
-- Names are based on the Left->Right layout,
local centeredIndexerStart = {
-- Left to right, e.g: 1 2 3 4
["LR"] = function(maxIndex)
return maxIndex > 0 and 1 or nil
end,
["RL"] = function(maxIndex)
return maxIndex > 0 and maxIndex or nil
end,
-- Center -> Left -> Right, e.g: 4 2 1 3
["CLR"] = function(maxIndex)
if maxIndex >= 3 then
return maxIndex - maxIndex % 2
else
return maxIndex > 0 and maxIndex or nil
end
end,
-- Center -> Right -> Left, e.g: 3 1 2 4
["CRL"] = function(maxIndex)
if maxIndex % 2 == 1 then
return maxIndex
else
return maxIndex > 0 and maxIndex - 1 or nil
end
end
}
local centeredIndexerNext = {
["LR"] = function(index, maxIndex)
index = index + 1
return index <= maxIndex and index or nil
end,
["RL"] = function(index, maxIndex)
index = index - 1
return index > 0 and index or nil
end,
["CLR"] = function(index, maxIndex)
-- Center -> Left -> Right
-- So even -> odd
if index % 2 == 0 then
index = index - 2
if index == 0 then
index = 1
end
else
index = index + 2
end
if index > maxIndex then
return nil
end
return index
end,
["CRL"] = function(index, maxIndex)
-- Center -> Right -> Left
-- So odd -> even
if index % 2 == 1 then
index = index - 2
if index == -1 then
index = 2
end
else
index = index + 2
end
if index > maxIndex then
return nil
end
return index
end,
}
local function createAnchorPerUnitFunc(data)
local anchorer = anchorers[data.anchorPerUnit] or anchorers.NAMEPLATE
return anchorer(data)
end
local function getDimension(regionData, dim)
return regionData.dimensions[dim]
end
local growers = {
LEFT = function(data)
local stagger = -(data.stagger or 0)
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local startX, startY = 0, 0
local coeff = staggerCoefficient(data.align, data.stagger)
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local x, y = startX, startY + (numVisible - 1) * stagger * coeff
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { x, y, true }
x = x - regionData.dimensions.width - space
y = y - stagger
end
end
end
end, anchorOn
end,
RIGHT = function(data)
local stagger = data.stagger or 0
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local startX, startY = 0, 0
local coeff = 1 - staggerCoefficient(data.align, stagger)
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local x, y = startX, startY - (numVisible - 1) * stagger * coeff
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { x, y, true }
x = x + (regionData.dimensions.width) + space
y = y + stagger
end
end
end
end, anchorOn
end,
UP = function(data)
local stagger = data.stagger or 0
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local startX, startY = 0, 0
local coeff = 1 - staggerCoefficient(data.align, stagger)
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local x, y = startX - (numVisible - 1) * stagger * coeff, startY
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { x, y, true }
x = x + stagger
y = y + (regionData.dimensions.height) + space
end
end
end
end, anchorOn
end,
DOWN = function(data)
local stagger = data.stagger or 0
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local startX, startY = 0, 0
local coeff = staggerCoefficient(data.align, stagger)
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local x, y = startX - (numVisible - 1) * stagger * coeff, startY
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = { x, y, true }
x = x + stagger
y = y - (regionData.dimensions.height) - space
end
end
end
end, anchorOn
end,
HORIZONTAL = function(data)
local stagger = data.stagger or 0
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local midX, midY = 0, 0
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
local FirstIndex = centeredIndexerStart[data.centerType]
local NextIndex = centeredIndexerNext[data.centerType]
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local totalWidth = (numVisible - 1) * space
for i = 1, numVisible do
local regionData = regionDatas[i]
totalWidth = totalWidth + (regionData.dimensions.width)
end
local x, y = midX - totalWidth/2, midY - (stagger * (numVisible - 1)/2)
newPositions[frame] = {}
local i = FirstIndex(numVisible)
while i do
local regionData = regionDatas[i]
x = x + (regionData.dimensions.width) / 2
newPositions[frame][regionData] = { x, y, true }
x = x + (regionData.dimensions.width) / 2 + space
y = y + stagger
i = NextIndex(i, numVisible)
end
end
end, anchorOn
end,
VERTICAL = function(data)
local stagger = -(data.stagger or 0)
local space = data.space or 0
local limit = data.useLimit and data.limit or math.huge
local midX, midY = 0, 0
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
local FirstIndex = centeredIndexerStart[data.centerType]
local NextIndex = centeredIndexerNext[data.centerType]
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local totalHeight = (numVisible - 1) * space
for i = 1, numVisible do
local regionData = regionDatas[i]
totalHeight = totalHeight + (regionData.dimensions.height)
end
local x, y = midX - (stagger * (numVisible - 1)/2), midY - totalHeight/2
newPositions[frame] = {}
local i = FirstIndex(numVisible)
while i do
local regionData = regionDatas[i]
y = y + (regionData.dimensions.height) / 2
newPositions[frame][regionData] = { x, y, true }
x = x + stagger
y = y + (regionData.dimensions.height) / 2 + space
i = NextIndex(i, numVisible)
end
end
end, anchorOn
end,
CIRCLE = function(data)
local oX, oY = 0, 0
local constantFactor = data.constantFactor
local space = data.space or 0
local radius = data.radius or 0
local stepAngle = (data.stepAngle or 0) * math.pi / 180
local limit = data.useLimit and data.limit or math.huge
local sAngle = (data.rotation or 0) * math.pi / 180
local arc = (data.fullCircle and 360 or data.arcLength or 0) * math.pi / 180
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local r
if constantFactor == "RADIUS" or constantFactor == "ANGLE" then
r = radius
else
if numVisible <= 1 then
r = 0
else
r = (numVisible * space) / (2 * math.pi)
end
end
local theta = sAngle
local dAngle
if numVisible == 1 then
dAngle = 0
elseif constantFactor == "ANGLE" then
dAngle = stepAngle
elseif not data.fullCircle then
dAngle = arc / (numVisible - 1)
else
dAngle = arc / numVisible
end
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
local x, y = polarToRect(r, theta)
newPositions[frame][regionData] = { x, y, true }
theta = theta + dAngle
end
end
end
end, anchorOn
end,
COUNTERCIRCLE = function(data)
local oX, oY = 0, 0
local constantFactor = data.constantFactor
local space = data.space or 0
local radius = data.radius or 0
local stepAngle = (data.stepAngle or 0) * math.pi / 180
local limit = data.useLimit and data.limit or math.huge
local sAngle = (data.rotation or 0) * math.pi / 180
local arc = (data.fullCircle and 360 or data.arcLength or 0) * math.pi / 180
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
local r
if constantFactor == "RADIUS" or constantFactor == "ANGLE" then
r = radius
else
if numVisible <= 1 then
r = 0
else
r = (numVisible * space) / (2 * math.pi)
end
end
local theta = sAngle
local dAngle
if numVisible == 1 then
dAngle = 0
elseif constantFactor == "ANGLE" then
dAngle = -stepAngle
elseif not data.fullCircle then
dAngle = arc / (1 - numVisible)
else
dAngle = arc / -numVisible
end
newPositions[frame] = {}
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
local x, y = polarToRect(r, theta)
newPositions[frame][regionData] = { x, y, true }
theta = theta + dAngle
end
end
end
end, anchorOn
end,
GRID = function(data)
local gridType = data.gridType
local gridWidth = data.gridWidth
local rowSpace = data.rowSpace
local colSpace = data.columnSpace
local rowFirst = (gridType:find("^[RLH]")) ~= nil
local limit = data.useLimit and data.limit or math.huge
local rowMul, colMul, primary_horizontal, secondary_horizontal, primary_vertical, secondary_vertical
if gridType:find("D") then
rowMul = -1
else
rowMul = 1
end
if gridType:find("L") then
colMul = -1
else
colMul = 1
end
if gridType:sub(1, 1) == "H" then
primary_horizontal = true
elseif gridType:sub(2, 2) == "H" then
secondary_horizontal = true
end
if gridType:sub(1, 1) == "V" then
primary_vertical = true
elseif gridType:sub(2, 2) == "V" then
secondary_vertical = true
end
local primary = {
-- x direction
dim = "width",
coord = 1,
mul = colMul,
space = colSpace,
current = 0
}
local secondary = {
-- y direction
dim = "height",
coord = 2,
mul = rowMul,
space = rowSpace,
current = 0
}
if not rowFirst then
primary, secondary = secondary, primary
end
local anchorPerUnitFunc, anchorOn
if data.useAnchorPerUnit then
anchorPerUnitFunc, anchorOn = createAnchorPerUnitFunc(data)
end
return function(newPositions, activeRegions)
local frames = {}
if anchorPerUnitFunc then
anchorPerUnitFunc(frames, activeRegions)
else
frames[""] = activeRegions
end
for frame, regionDatas in pairs(frames) do
local numVisible = min(limit, #regionDatas)
primary.current = 0
secondary.current = 0
secondary.max = 0
newPositions[frame] = {}
local minX, maxX, minY, maxY, totalMinX, totalMaxX, totalMinY, totalMaxY
local start
for i, regionData in ipairs(regionDatas) do
if i <= numVisible then
newPositions[frame][regionData] = {
[primary.coord] = primary.current,
[secondary.coord] = secondary.current,
[3] = true
}
local x, y = newPositions[frame][regionData][1], newPositions[frame][regionData][2]
if minX == nil then
minX, maxX, minY, maxY = x, x, y, y
start = i
else
minX, maxX = math.min(minX, x), math.max(maxX, x)
minY, maxY = math.min(minY, y), math.max(maxY, y)
end
if totalMinX == nil then
totalMinX, totalMaxX, totalMinY, totalMaxY = x, x, y, y
else
totalMinX, totalMaxX = math.min(totalMinX, x), math.max(totalMaxX, x)
totalMinY, totalMaxY = math.min(totalMinY, y), math.max(totalMaxY, y)
end
secondary.max = max(secondary.max, getDimension(regionData, secondary.dim))
if i % gridWidth == 0 then
if primary_horizontal then
local offsetX = (maxX - minX) / 2
for j = start, i do
newPositions[frame][regionDatas[j]][1] = newPositions[frame][regionDatas[j]][1] - offsetX
end
end
if primary_vertical then
local offsetY = (maxY - minY) / 2
for j = start, i do
newPositions[frame][regionDatas[j]][2] = newPositions[frame][regionDatas[j]][2] - offsetY
end
end
primary.current = 0
secondary.current = secondary.current + (secondary.space + secondary.max) * secondary.mul
secondary.max = 0
minX, maxX = nil, nil
minY, maxY = nil, nil
else
primary.current = primary.current + (primary.space + getDimension(regionData, primary.dim)) * primary.mul
end
end
end
if (primary_horizontal or primary_vertical) and minX then
local offsetX = (maxX - minX) / 2
local offsetY = (maxY - minY) / 2
for j = start, #regionDatas do
if j <= numVisible then
if primary_horizontal then
newPositions[frame][regionDatas[j]][1] = newPositions[frame][regionDatas[j]][1] - offsetX
end
if primary_vertical then
newPositions[frame][regionDatas[j]][2] = newPositions[frame][regionDatas[j]][2] - offsetY
end
end
end
end
if (secondary_horizontal or secondary_vertical) and totalMinX then
local offsetX = (totalMaxX - totalMinX) / 2
local offsetY = (totalMaxY - totalMinY) / 2
for j = 1, #regionDatas do
if j <= numVisible then
if secondary_horizontal then
newPositions[frame][regionDatas[j]][1] = newPositions[frame][regionDatas[j]][1] - offsetX
end
if secondary_vertical then
newPositions[frame][regionDatas[j]][2] = newPositions[frame][regionDatas[j]][2] - offsetY
end
end
end
end
end
end, anchorOn
end,
CUSTOM = function(data)
local growStr = data.customGrow or ""
local growFunc = WeakAuras.LoadFunction("return " .. growStr, data.id) or noop
local growOn = nil
local events = WeakAuras.split(data.growOn or "")
if #events > 0 then
growOn = {}
for _, event in ipairs(events) do
growOn[event] = true
end
end
return function(newPositions, activeRegions)
Private.ActivateAuraEnvironment(data.id)
local ok = pcall(growFunc, newPositions, activeRegions)
Private.ActivateAuraEnvironment()
if not ok then
Private.GetErrorHandlerId(data.id, L["Custom Grow"])
wipe(newPositions)
end
end, growOn
end
}
WeakAuras.GrowFunctions = growers
local function createGrowFunc(data)
local grower = growers[data.grow] or growers.DOWN
return grower(data)
end
local function SafeGetPos(region, func)
local ok, value1, value2 = pcall(func, region)
if ok then
return value1, value2
end
end
local function isDifferent(regionData, cache, events)
local id = regionData.id
local cloneId = regionData.cloneId or ""
local state = regionData.region.state
if not events then
return false
elseif events.changed then
return true -- escape hatch, not super recommended
else
local isDifferent = false
if not cache[id] then
isDifferent = true
local cachedState = {}
cache[id] = {[cloneId] = cachedState}
for event in pairs(events) do
cachedState[event] = state[event]
end
elseif not cache[id][cloneId] then
isDifferent = true
local cachedState = {}
cache[id][cloneId] = cachedState
for event in pairs(events) do
cachedState[event] = state[event]
end
else
local cachedState = cache[id][cloneId]
for event in pairs(events) do
if regionData.region.state[event] ~= cachedState[event] then
cachedState[event] = state[event]
isDifferent = true
end
end
end
return isDifferent
end
end
local function clearCache(cache, id, cloneId)
cloneId = cloneId or ""
if cache[id] then
cache[id][cloneId] = nil
end
end
-- Resize queue
local RunNextFrame = CreateFrame("Frame")
local q = {}
RunNextFrame:Hide()
local function QueueResize(g)
if not g then return end
q[#q+1] = g
RunNextFrame:Show()
end
RunNextFrame:SetScript("OnUpdate", function(self)
self:Hide()
for i = 1, #q do
local g = q[i]
q[i] = nil
if g and g.Resize then
g:Resize()
end
end
end)
local function modify(parent, region, data)
Private.FixGroupChildrenOrderForGroup(data)
region:SetScale(data.scale and data.scale > 0 and data.scale <= 10 and data.scale or 1)
Private.regionPrototype.modify(parent, region, data)
if data.border and not data.useAnchorPerUnit then
local background = region.background
background:SetBackdrop({
edgeFile = data.borderEdge ~= "None" and SharedMedia:Fetch("border", data.borderEdge) or "",
edgeSize = data.borderSize,
bgFile = data.borderBackdrop ~= "None" and SharedMedia:Fetch("background", data.borderBackdrop) or "",
insets = {
left = data.borderInset,
right = data.borderInset,
top = data.borderInset,
bottom = data.borderInset,
},
});
background:SetBackdropBorderColor(data.borderColor[1], data.borderColor[2],
data.borderColor[3], data.borderColor[4]);
background:SetBackdropColor(data.backdropColor[1], data.backdropColor[2],
data.backdropColor[3], data.backdropColor[4]);
background:ClearAllPoints();
background:SetPoint("bottomleft", region, "bottomleft", -1 * data.borderOffset, -1 * data.borderOffset)
background:SetPoint("topright", region, "topright", data.borderOffset, data.borderOffset)
background:Show();
else
region.background:Hide();
end
function region:IsSuspended()
return not WeakAuras.IsLoginFinished() or self.suspended > 0
end
function region:Suspend()
-- Stops group from repositioning and re-indexing children
-- Calls to Activate, Deactivate, and Re-index will cache the relevant children
-- Similarly, Sort, Position, and Resize will be stopped
-- to be called on the next Resume
-- for when the group is resumed
self.suspended = self.suspended + 1
end
function region:Resume()
-- Allows group to re-index and reposition.
-- TriggersSortUpdatedChildren and PositionChildren to happen
if self.suspended > 0 then
self.suspended = self.suspended - 1
end
region:RunDelayedActions()
end
function region:RunDelayedActions()
if not self:IsSuspended() then
if self.needToReload then
self:ReloadControlledChildren()
end
if self.needToSort then
self:SortUpdatedChildren()
end
if self.needToPosition then
self:PositionChildren()
end
if self.needToResize then
self:Resize()
end
end
end
local function createRegionData(childData, childRegion, childID, cloneID, dataIndex)
cloneID = cloneID or ""
local controlPoint = region.controlPoints:Acquire()
controlPoint:SetWidth(childRegion:GetWidth())
controlPoint:SetHeight(childRegion:GetHeight())
local regionData = {
data = childData,
region = childRegion,
id = childID,
cloneId = cloneID,
dataIndex = dataIndex,
controlPoint = controlPoint,
parent = region
}
if childData.regionType == "text" then
regionData.dimensions = childRegion
else
regionData.dimensions = childData
end
controlPoint.regionData = regionData
childRegion:SetParent(controlPoint)
region.controlledChildren[childID] = region.controlledChildren[childID] or {}
region.controlledChildren[childID][cloneID] = controlPoint
childRegion:SetAnchor(data.selfPoint, controlPoint, data.selfPoint)
return regionData
end
local function getRegionData(childID, cloneID)
cloneID = cloneID or ""
local controlPoint
controlPoint = region.controlledChildren[childID] and region.controlledChildren[childID][cloneID]
if not controlPoint then return end
return controlPoint.regionData
end
local function releaseRegionData(regionData)
if region.controlledChildren[regionData.id] then
region.controlledChildren[regionData.id][regionData.cloneId] = nil
end
region.controlPoints:Release(regionData.controlPoint)
end
function region:ReloadControlledChildren()
-- 'forgets' about regions it controls and starts from scratch. Mostly useful when Add()ing the group
if not self:IsSuspended() then
Private.StartProfileSystem("dynamicgroup")
Private.StartProfileAura(data.id)
self.needToReload = false
self.sortedChildren = {}
self.sortStates = {}
self.growStates = {}
self.controlledChildren = {}
self.updatedChildren = {}
self.controlPoints:ReleaseAll()
for dataIndex, childID in ipairs(data.controlledChildren) do
local childRegion, childData = WeakAuras.GetRegion(childID), WeakAuras.GetData(childID)
if childRegion and childData then
local regionData = createRegionData(childData, childRegion, childID, nil, dataIndex)
if childRegion.toShow then
tinsert(self.sortedChildren, regionData)
self.updatedChildren[regionData] = true
end
end
if childData and Private.clones[childID] then
for cloneID, cloneRegion in pairs(Private.clones[childID]) do
local regionData = createRegionData(childData, cloneRegion, childID, cloneID, dataIndex)
if cloneRegion.toShow then
tinsert(self.sortedChildren, regionData)
self.updatedChildren[regionData] = true
end
end
end
end
Private.StopProfileSystem("dynamicgroup")
Private.StopProfileAura(data.id)
self:SortUpdatedChildren()
else
self.needToReload = true
end
end
function region:AddChild(childID, cloneID)
-- adds regionData to the store.
-- this is useful mostly for when clones are created which we didn't know about last time Reload was called
cloneID = cloneID or ""
if self.controlledChildren[childID] and self.controlledChildren[childID][cloneID] then
return
end
local dataIndex = tIndexOf(data.controlledChildren, childID)
if not dataIndex then return end
local childData = WeakAuras.GetData(childID)
local childRegion = WeakAuras.GetRegion(childID, cloneID)
if not childData or not childRegion then return end
local regionData = createRegionData(childData, childRegion, childID, cloneID, dataIndex)
if childRegion.toShow then
tinsert(self.sortedChildren, regionData)
self.updatedChildren[regionData] = true
end
self:SortUpdatedChildren()
end
function region:ActivateChild(childID, cloneID)
-- Causes the group to start controlling its order and position
-- Called in the child's Expand() method
local regionData = getRegionData(childID, cloneID)
if not regionData then
return self:AddChild(childID, cloneID)
end
if not regionData.region.toShow then return end
-- it's possible that while paused, we might get Activate, Deactivate, Activate on the same child
-- so we need to check if this child has been updated since the last Sort
-- if it has been, then don't insert it again
if not regionData.active and self.updatedChildren[regionData] == nil then
tinsert(self.sortedChildren, regionData)
self.updatedChildren[regionData] = true
self:SortUpdatedChildren()
elseif isDifferent(regionData, self.sortStates, self.sortOn) then
self.updatedChildren[regionData] = true
self:SortUpdatedChildren()
elseif isDifferent(regionData, self.growStates, self.growOn) then
self:PositionChildren()
end
end
function region:RemoveChild(childID, cloneID)
-- removes something from the store. Mostly useful when a clone gets released
-- so that we don't step on our own feet.
local regionData = getRegionData(childID, cloneID)
if not regionData then return end
releaseRegionData(regionData)
self.updatedChildren[regionData] = false
clearCache(self.sortStates, childID, cloneID)
clearCache(self.growStates, childID, cloneID)
self:SortUpdatedChildren()
end
function region:DeactivateChild(childID, cloneID)
-- Causes the group to stop controlling its order and position
-- Called in the child's Collapse() method
local regionData = getRegionData(childID, cloneID)
if regionData and not regionData.region.toShow then
self.updatedChildren[regionData] = false
end
clearCache(self.sortStates, childID, cloneID)
clearCache(self.growStates, childID, cloneID)
self:SortUpdatedChildren()
end
region.sortFunc, region.sortOn = createSortFunc(data)
function region:SortUpdatedChildren()
-- iterates through cache to insert all updated children in the right spot
-- Called when the Group is Resume()d
-- uses sort data to determine the correct spot
if not self:IsSuspended() then
Private.StartProfileSystem("dynamicgroup")
Private.StartProfileAura(data.id)
self.needToSort = false
local i = 1
while self.sortedChildren[i] do
local regionData = self.sortedChildren[i]
local active = self.updatedChildren[regionData]
if active ~= nil then
regionData.active = active
end
if active == false then
-- i now refers to what was i + 1, so don't increment
tremove(self.sortedChildren, i)
else
local j = i
while j > 1 do
local otherRegionData = self.sortedChildren[j - 1]
if not (active or self.updatedChildren[otherRegionData])
or not self.sortFunc(regionData, otherRegionData) then
break
else
self.sortedChildren[j] = otherRegionData
j = j - 1
self.sortedChildren[j] = regionData
end
end
i = i + 1
end
end
self.updatedChildren = {}
Private.StopProfileSystem("dynamicgroup")
Private.StopProfileAura(data.id)
self:PositionChildren()
else
self.needToSort = true
end
end
region.growFunc, region.growOn = createGrowFunc(data)
region.anchorPerUnit = data.useAnchorPerUnit and data.anchorPerUnit
local animate = data.animate
function region:PositionChildren()
-- Repositions active children according to their index
-- Positioning is based on grow information from the data
if not self:IsSuspended() then
self.needToPosition = false
if #self.sortedChildren > 0 then
if animate then
Private.RegisterGroupForPositioning(data.uid, self)
else
self:DoPositionChildren()
end
else
self:Resize()
end
else
self.needToPosition = true
end
end
function region:DoPositionChildrenPerFrame(frame, positions, handledRegionData)
for regionData, pos in pairs(positions) do
if type(regionData) ~= "table" then
break;
end
handledRegionData[regionData] = true
local x, y, show = type(pos[1]) == "number" and pos[1] or 0,
type(pos[2]) == "number" and pos[2] or 0,
type(pos[3]) ~= "boolean" and true or pos[3]
local controlPoint = regionData.controlPoint
controlPoint:ClearAnchorPoint()
if frame == "" then
controlPoint:SetAnchorPoint(
data.selfPoint,
self,
data.selfPoint,
x, y
)
else
controlPoint:SetAnchorPoint(
data.selfPoint,
frame,
data.anchorPoint,
x + data.xOffset, y + data.yOffset
)
end
if show and frame ~= WeakAuras.HiddenFrames then
controlPoint:Show()
else
controlPoint:Hide()
end
controlPoint:SetWidth(regionData.dimensions.width)
controlPoint:SetHeight(regionData.dimensions.height)
if (data.anchorFrameParent or data.anchorFrameParent == nil)
and (
data.useAnchorPerUnit
or (
not data.useAnchorPerUnit
and not (data.anchorFrameType == "SCREEN" or data.anchorFrameType == "UIPARENT" or data.anchorFrameType == "MOUSE")
)
)
then
local parent
if frame == "" then
parent = self.relativeTo
else
if type(frame) == "string" then
parent = _G[frame]
else
parent = frame
end
end
if parent and parent.IsObjectType and parent:IsObjectType("Frame") then
controlPoint:SetParent(parent)
controlPoint:SetScale(data.scale and data.scale > 0 and data.scale <= 10 and data.scale or 1)
end
else
controlPoint:SetParent(self)
controlPoint:SetScale(1)
end
local childData = controlPoint.regionData.data
local childRegion = controlPoint.regionData.region
if(childData.frameStrata == 1) then
local frameStrata = region:GetFrameStrata()
childRegion:SetFrameStrata(frameStrata ~= "UNKNOWN" and frameStrata or "BACKGROUND");
else
childRegion:SetFrameStrata(Private.frame_strata_types[childData.frameStrata]);
end
Private.ApplyFrameLevel(childRegion)
if self.anchorPerUnit == "UNITFRAME" then
Private.dyngroup_unitframe_monitor[regionData] = frame
end
if animate then
Private.CancelAnimation(regionData.controlPoint, true)
local xPrev = regionData.xOffset or x
local yPrev = regionData.yOffset or y
local xDelta = xPrev - x
local yDelta = yPrev - y
if show and (abs(xDelta) > 0.01 or abs(yDelta) > 0.01) then
local anim
if data.grow == "CIRCLE" or data.grow == "COUNTERCIRCLE" then
local originX, originY = 0,0
local radius1, previousAngle = WeakAuras.GetPolarCoordinates(xPrev, yPrev, originX, originY)
local radius2, newAngle = WeakAuras.GetPolarCoordinates(x, y, originX, originY)
local dAngle = newAngle - previousAngle
dAngle = ((dAngle > 180 and dAngle - 360) or (dAngle < -180 and dAngle + 360) or dAngle)
if(math.abs(radius1 - radius2) > 0.1) then
local translateFunc = [[
function(progress, _, _, previousAngle, dAngle)
local previousRadius, dRadius = %f, %f;
local targetX, targetY = %f, %f
local radius = previousRadius + (1 - progress) * dRadius;
local angle = previousAngle + (1 - progress) * dAngle;
return cos(angle) * radius - targetX, sin(angle) * radius - targetY;
end
]]
anim = {
type = "custom",
duration = 0.2,
use_translate = true,
translateType = "custom",
translateFunc = translateFunc:format(radius1, radius2 - radius1, x, y),
x = previousAngle,
y = dAngle,
selfPoint = data.selfPoint,
anchor = self,
anchorPoint = data.selfPoint,
}
else
local translateFunc = [[
function(progress, _, _, previousAngle, dAngle)
local radius = %f;
local targetX, targetY = %f, %f
local angle = previousAngle + (1 - progress) * dAngle;
return cos(angle) * radius - targetX, sin(angle) * radius - targetY;
end
]]
anim = {
type = "custom",
duration = 0.2,
use_translate = true,
translateType = "custom",
translateFunc = translateFunc:format(radius1, x, y),
x = previousAngle,
y = dAngle,
selfPoint = data.selfPoint,
anchor = self,
anchorPoint = data.selfPoint,
}
end
end
if not(anim) then
anim = {
type = "custom",
duration = 0.2,
use_translate = true,
x = xDelta,
y = yDelta,
selfPoint = data.selfPoint,
anchor = self,
anchorPoint = data.selfPoint,
}
end
-- update animated expand & collapse for this child
Private.Animate("controlPoint", data.uid, "controlPoint", anim, regionData.controlPoint, true)
end
end
regionData.xOffset = x
regionData.yOffset = y
regionData.shown = show
end
end
function region:DoPositionChildren()
Private.StartProfileSystem("dynamicgroup")
Private.StartProfileAura(data.id)
local handledRegionData = {}
local newPositions = {}
self.growFunc(newPositions, self.sortedChildren)
if #newPositions > 0 then
for index = 1, #newPositions do
if type(newPositions[index]) == "table" then
local data = self.sortedChildren[index]
if data then
newPositions[data] = newPositions[index]
else
geterrorhandler()(("Error in '%s', Grow function return position for an invalid region"):format(region.id))
end
newPositions[index] = nil
end
end
region:DoPositionChildrenPerFrame("", newPositions, handledRegionData)
else
for frame, positions in pairs(newPositions) do
region:DoPositionChildrenPerFrame(frame, positions, handledRegionData)
end
end
for index, child in ipairs(self.sortedChildren) do
if not handledRegionData[child] then
child.controlPoint:Hide()
end
end
Private.StopProfileSystem("dynamicgroup")
Private.StopProfileAura(data.id)
QueueResize(self)
end
function region:Resize()
-- Resizes the dynamic group, for background and border purposes
if not self:IsSuspended() then
self.needToResize = false
-- if self.dynamicAnchor then self:UpdateBorder(); return end
Private.StartProfileSystem("dynamicgroup")
Private.StartProfileAura(data.id)
local numVisible, minX, maxX, maxY, minY = 0, nil, nil, nil, nil
for active, regionData in ipairs(self.sortedChildren) do
if regionData.shown then
numVisible = numVisible + 1
local childRegion = regionData.region
local regionLeft, regionRight, regionTop, regionBottom
= SafeGetPos(childRegion, childRegion.GetLeft), SafeGetPos(childRegion, childRegion.GetRight),
SafeGetPos(childRegion, childRegion.GetTop), SafeGetPos(childRegion, childRegion.GetBottom)
if(regionLeft and regionRight and regionTop and regionBottom) then
minX = minX and min(regionLeft, minX) or regionLeft
maxX = maxX and max(regionRight, maxX) or regionRight
minY = minY and min(regionBottom, minY) or regionBottom
maxY = maxY and max(regionTop, maxY) or regionTop
end
end
end
if numVisible > 0 then
self:Show()
minX, maxX, minY, maxY = (minX or 0), (maxX or 0), (minY or 0), (maxY or 0)
local width, height = maxX - minX, maxY - minY
width = width > 0 and width or 16
height = height > 0 and height or 16
self:SetWidth(width)
self:SetHeight(height)
self.currentWidth = width
self.currentHeight = height
if data.border and not data.useAnchorPerUnit then
local regionLeft = SafeGetPos(region, region.GetLeft) or minX
local regionBottom = SafeGetPos(region, region.GetBottom) or minY
if regionLeft and regionBottom then
self.background:ClearAllPoints()
self.background:SetPoint("BOTTOMLEFT", region, "BOTTOMLEFT", minX + -1 * data.borderOffset - regionLeft, minY + -1 * data.borderOffset - regionBottom)
self.background:SetPoint("TOPRIGHT", region, "BOTTOMLEFT", maxX + data.borderOffset - regionLeft, maxY + data.borderOffset - regionBottom)
end
end
else
self:Hide()
end
if WeakAuras.IsOptionsOpen() then
Private.OptionsFrame().moversizer:ReAnchor()
end
Private.StopProfileSystem("dynamicgroup")
Private.StopProfileAura(data.id)
else
self.needToResize = true
end
end
region:ReloadControlledChildren()
Private.regionPrototype.modifyFinish(parent, region, data)
end
Private.RegisterRegionType("dynamicgroup", create, modify, default)