From 0f69a1e3210fa0e6ee82d787117f2ce8aa964570 Mon Sep 17 00:00:00 2001 From: labrie75 <82786635+labrie75@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:05:39 +0900 Subject: [PATCH 1/4] Update EUI_UnitFrames_Options.lua --- .../EUI_UnitFrames_Options.lua | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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) From d28b8c27d07654d76d91c8ef0e18e3035a687b86 Mon Sep 17 00:00:00 2001 From: labrie75 <82786635+labrie75@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:05:54 +0900 Subject: [PATCH 2/4] Update EllesmereUIUnitFrames.lua --- EllesmereUIUnitFrames/EllesmereUIUnitFrames.lua | 3 +++ 1 file changed, 3 insertions(+) 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 From bf59a6f89a14b78898663533f9c9d77c9c203d04 Mon Sep 17 00:00:00 2001 From: labrie75 <82786635+labrie75@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:06:09 +0900 Subject: [PATCH 3/4] Update EllesmereUIUnitFrames_PlayerAuras.lua --- .../EllesmereUIUnitFrames_PlayerAuras.lua | 115 +++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua index 29032305..1952b5fe 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua @@ -282,6 +282,106 @@ 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 + +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) 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 + -- 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. @@ -291,6 +391,8 @@ local function DoPendingRefresh() refreshPending = false if _paAuraDirty then RefreshAll() + -- The refresh can reflow the grid; keep the spacing on the same pass. + ApplyAllSpacing() end end @@ -765,6 +867,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 +891,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 +915,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 From c4fa61b112a54966e6e5e88c1605919f766b38d1 Mon Sep 17 00:00:00 2001 From: labrie75 <82786635+labrie75@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:23:06 +0900 Subject: [PATCH 4/4] Update EllesmereUIUnitFrames_PlayerAuras.lua --- .../EllesmereUIUnitFrames_PlayerAuras.lua | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua index 1952b5fe..94b48a0d 100644 --- a/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua +++ b/EllesmereUIUnitFrames/EllesmereUIUnitFrames_PlayerAuras.lua @@ -358,6 +358,13 @@ local function LayoutSpacing(container, list, isDebuff) 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() @@ -369,7 +376,13 @@ local function ApplySpacing(frame, auras) -- 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) 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) @@ -382,6 +395,17 @@ local function ApplyAllSpacing() 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. @@ -391,9 +415,11 @@ local function DoPendingRefresh() refreshPending = false if _paAuraDirty then RefreshAll() - -- The refresh can reflow the grid; keep the spacing on the same pass. - ApplyAllSpacing() 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() @@ -857,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()