diff --git a/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua b/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua index 90dae3d1..2e11ec2d 100644 --- a/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua +++ b/EllesmereUIUnitFrames/EUI_UnitFrames_Options.lua @@ -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 @@ -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) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua index c2cf6417..417c109e 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua @@ -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 diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua index 29032305..94b48a0d 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua @@ -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. @@ -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() @@ -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() @@ -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 @@ -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 @@ -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