Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15737,6 +15737,7 @@ initFrame:SetScript("OnEvent", function(self)
db.profile.playerAuras[key] = v
if ns.RefreshPlayerAuras then ns.RefreshPlayerAuras() end
if ns.ApplyPlayerAuraScale then ns.ApplyPlayerAuraScale() end
if ns.ApplyPlayerAuraSpacing then ns.ApplyPlayerAuraSpacing() end
end

_, h = W:Spacer(parent, y, 20); y = y - h
Expand Down Expand Up @@ -15766,19 +15767,36 @@ initFrame:SetScript("OnEvent", function(self)
setValue = function(v) PASet("iconSize", v) end }
); y = y - h

-- Inline cog: Icon Zoom (next to "Icon Size"). Buffs and debuffs
-- crop independently.
-- Inline cog: Icon Zoom and grid spacing (next to "Icon Size").
-- Buffs and debuffs crop and space independently. 5 is Blizzard's own
-- gap, so the spacing defaults change nothing; negative values overlap
-- the icons deliberately.
if not EllesmereUI._prebuilding then
local rgn = paRow1._rightRegion
local _, cogShow = EllesmereUI.BuildCogPopup({
title = "Icon Zoom",
rows = {
-- Rows are grouped buff-first, debuff-second, so the
-- Spacing X/Y pair under each Zoom slider reads as that
-- group's spacing. Every label reuses an existing locale key.
{ type = "slider", label = "Buff Zoom", min = 0, max = 0.20, step = 0.01,
get = function() return PAGet("buffIconZoom") or 0.055 end,
set = function(v) PASet("buffIconZoom", v) end },
{ type = "slider", label = "Spacing X", min = -30, max = 30, step = 1,
get = function() return PAGet("buffSpacingX") or 5 end,
set = function(v) PASet("buffSpacingX", v) end },
{ type = "slider", label = "Spacing Y", min = -30, max = 30, step = 1,
get = function() return PAGet("buffSpacingY") or 5 end,
set = function(v) PASet("buffSpacingY", v) end },
{ type = "slider", label = "Debuff Zoom", min = 0, max = 0.20, step = 0.01,
get = function() return PAGet("debuffIconZoom") or 0.055 end,
set = function(v) PASet("debuffIconZoom", v) end },
{ type = "slider", label = "Spacing X", min = -30, max = 30, step = 1,
get = function() return PAGet("debuffSpacingX") or 5 end,
set = function(v) PASet("debuffSpacingX", v) end },
{ type = "slider", label = "Spacing Y", min = -30, max = 30, step = 1,
get = function() return PAGet("debuffSpacingY") or 5 end,
set = function(v) PASet("debuffSpacingY", v) end },
},
})
local cogBtn = CreateFrame("Button", nil, rgn)
Expand Down
3 changes: 3 additions & 0 deletions EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11571,6 +11571,9 @@ local function ReloadFrames()
-- path that lands here (fonts, profiles, options) forces their one
-- explicit refresh instead.
if ns.RefreshPlayerAuras then ns.RefreshPlayerAuras() end
-- Same deal for the grid spacing: a profile switch re-anchors immediately
-- instead of waiting for the next aura event.
if ns.ApplyPlayerAuraSpacing then ns.ApplyPlayerAuraSpacing() end
end

-- Toggle a frame's oUF Castbar element without rewriting Blizzard's cast bar
Expand Down
149 changes: 147 additions & 2 deletions EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,130 @@ local function RefreshAll()
end
ns.RefreshPlayerAuras = RefreshAll

-------------------------------------------------------------------------------
-- Icon spacing (chain re-anchor of the Blizzard grid)
--
-- Blizzard lays the grid out with a fixed 5px gap. The spacing sliders
-- re-anchor the buttons in a chain -- each icon relative to its neighbour,
-- the first one left on Blizzard's own anchor -- which reproduces the same
-- grid at any gap. Row and column direction come from the container's own
-- iconStride / addIconsToRight / addIconsToTop, so the layout follows
-- whatever the user set in Edit Mode. No aura data is read at any point
-- (secret on Midnight), and at the default of 5 -- Blizzard's own gap --
-- the layout pass short-circuits and the grid is untouched.
-------------------------------------------------------------------------------
local BLIZZ_GAP = 5

local function SpacingFor(isDebuff)
local cfg = PA()
if not cfg then return BLIZZ_GAP, BLIZZ_GAP end
if isDebuff then
return cfg.debuffSpacingX or BLIZZ_GAP, cfg.debuffSpacingY or BLIZZ_GAP
end
return cfg.buffSpacingX or BLIZZ_GAP, cfg.buffSpacingY or BLIZZ_GAP
end

-- Shown aura buttons only: hidden pool entries and the container's anchor
-- dummy would corrupt the chain.
local function VisibleAuras(auras)
local out = {}
if type(auras) ~= "table" then return out end
for _, button in ipairs(auras) do
if button and button.Icon and not button.isAuraAnchor
and button.IsShown and button:IsShown() then
out[#out + 1] = button
end
end
return out
end

-- Re-entry guard: our SetPoint calls can fire layout hooks again.
local spacingBusy = false

-- Whether a non-default layout is currently applied, per grid. Needed so a
-- return to the default (a profile switch to one with no spacing saved, or
-- the sliders reset) restores Blizzard's grid immediately: chaining once at
-- the stock gap reproduces it exactly, and we cannot ask Blizzard to relayout
-- (driving its aura machinery from addon context taints it). While this is
-- false the default still short-circuits, so an untouched grid costs nothing.
local spacingActive = { [false] = false, [true] = false }

local function LayoutSpacing(container, list, isDebuff)
if #list < 2 then return end
local padX, padY = SpacingFor(isDebuff)
local isDefault = (padX == BLIZZ_GAP and padY == BLIZZ_GAP)
if isDefault and not spacingActive[isDebuff] then return end
spacingActive[isDebuff] = not isDefault
local stride = container.iconStride or 2
local addRight = container.addIconsToRight and true or false
local addTop = container.addIconsToTop and true or false
local cy = addTop and "BOTTOM" or "TOP"
local cx = addRight and "LEFT" or "RIGHT"
local prevX = addRight and "RIGHT" or "LEFT"
local xoff = addRight and padX or -padX
local oppY = addTop and "TOP" or "BOTTOM"
local yoff = addTop and padY or -padY
for i = 2, #list do
local button = list[i]
local col = (i - 1) % stride
button:ClearAllPoints()
if col == 0 then
-- First icon of a row: hang it off the row above.
button:SetPoint(cy .. cx, list[i - stride], oppY .. cx, 0, yoff)
else
button:SetPoint(cy .. cx, list[i - 1], cy .. prevX, xoff, 0)
end
end
end

-- Forward: ApplySpacing schedules it when a layout pass carries no aura list
-- yet (the buttons land a moment later); without the retry that grid would
-- sit at the stock gap until the next aura event.
local spacingPending = false
local spacingRetrying = false -- the retry pass must not re-schedule itself
local DeferSpacing

local function ApplySpacing(frame, auras)
if spacingBusy then return end
local cfg = PA()
if not (cfg and cfg.enabled) then return end
local container = frame and frame.AuraContainer
if not container then return end
-- Prefer the auras Blizzard just laid out: frame.auraFrames does not yet
-- contain a button created this pass, and spacing from it left a new aura
-- at the stock gap until the next update.
local list = VisibleAuras(auras)
if #list == 0 then list = VisibleAuras(container.auras) end
if #list == 0 then
list = VisibleAuras(frame.auraFrames)
if #list == 0 then
if DeferSpacing and not spacingRetrying then DeferSpacing() end
return
end
end
if #list < 2 then return end
spacingBusy = true
LayoutSpacing(container, list, frame == DebuffFrame)
spacingBusy = false
end

local function ApplyAllSpacing()
ApplySpacing(BuffFrame)
ApplySpacing(DebuffFrame)
end
ns.ApplyPlayerAuraSpacing = ApplyAllSpacing

DeferSpacing = function()
if spacingPending or not (C_Timer and C_Timer.After) then return end
spacingPending = true
C_Timer.After(0, function()
spacingPending = false
spacingRetrying = true
ApplyAllSpacing()
spacingRetrying = false
end)
end

-- UpdateGridLayout can fire several times within one frame; each fire used to
-- queue its own full refresh. Coalesced to one pass per tick. Named rather than
-- an inline closure so scheduling allocates nothing.
Expand All @@ -292,6 +416,10 @@ local function DoPendingRefresh()
if _paAuraDirty then
RefreshAll()
end
-- Unconditional: a grid pass whose hook saw no aura list yet would
-- otherwise sit at the stock gap until the next aura event. At the
-- default this is two number compares per grid.
ApplyAllSpacing()
end

local function RequestRefresh()
Expand Down Expand Up @@ -755,6 +883,14 @@ initFrame:SetScript("OnEvent", function(self, event, arg1)
if event == "PLAYER_LOGIN" then
self:UnregisterEvent("PLAYER_LOGIN")

-- Early spacing passes: the main install below waits a full second
-- for the UF db, and a saved gap showing at the stock spacing for
-- that second reads as a visible late snap on every reload.
-- ApplyAllSpacing no-ops until the db (and a second aura) exists, so
-- firing early is safe; whichever attempt first finds the db wins.
C_Timer.After(0.15, ApplyAllSpacing)
C_Timer.After(0.4, ApplyAllSpacing)

-- Delay to let UF db initialize
C_Timer.After(1, function()
local cfg = PA()
Expand All @@ -765,6 +901,10 @@ initFrame:SetScript("OnEvent", function(self, event, arg1)

-- Initial skin pass
RefreshAll()
-- Initial spacing pass: the login grid was laid out before these
-- hooks existed, so without this a saved gap only applies on the
-- first aura change after logging in.
ApplyAllSpacing()

-- Aura-set dirty signal: aura buttons are only born from
-- aura-list changes, so the grid hooks below sweep only after a
Expand All @@ -785,7 +925,11 @@ initFrame:SetScript("OnEvent", function(self, event, arg1)

-- Hook aura updates to catch new/changed buttons
if BuffFrame and BuffFrame.AuraContainer then
hooksecurefunc(BuffFrame.AuraContainer, "UpdateGridLayout", function()
hooksecurefunc(BuffFrame.AuraContainer, "UpdateGridLayout", function(_, auras)
-- Space with the aura list Blizzard just laid out, so an
-- aura applied this pass lands at the configured gap on
-- the same frame.
ApplySpacing(BuffFrame, auras)
RequestRefresh()
end)
if BuffFrame.RefreshConsolidationFrameVisibility then
Expand All @@ -805,7 +949,8 @@ initFrame:SetScript("OnEvent", function(self, event, arg1)
end
end
if DebuffFrame and DebuffFrame.AuraContainer then
hooksecurefunc(DebuffFrame.AuraContainer, "UpdateGridLayout", function()
hooksecurefunc(DebuffFrame.AuraContainer, "UpdateGridLayout", function(_, auras)
ApplySpacing(DebuffFrame, auras)
RequestRefresh()
end)
end
Expand Down