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
73 changes: 64 additions & 9 deletions Modules/BuffBars.lua
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,44 @@ local function hookChildFrame(child, module)
child.__ecmHooked = true
end

--- Restyles visible children without changing their layout.
---@param module BuffBars
---@param why string|nil
local function restyleActiveChildren(module, why)
local viewer = BuffBarCooldownViewer
if not viewer then return end

local cfg = module:GetModuleConfig()
local globalConfig = module:GetGlobalConfig()
if not cfg or not globalConfig then
return
end

local children = getChildrenOrdered(viewer, why)
if not children then
return
end

local spellColors = getSpellColors()
module._layoutRunning = true
module._editLocked = false
local ok, err = pcall(function()
for _, entry in ipairs(children) do
local child = entry.frame
hookChildFrame(child, module)
spellColors:DiscoverBar(child)
if child:IsShown() then
StyleChildBar(module, child, cfg, globalConfig, spellColors)
end
end
end)
module._layoutRunning = nil
if not module._editLocked then
module._warned = false
end
ns.DebugAssert(ok, "Error restyling buff bars after UNIT_AURA: " .. tostring(err))
end

local function getViewerPosition(module)
local cfg = module:GetModuleConfig()
local mode = cfg and cfg.anchorMode or ns.Constants.ANCHORMODE_CHAIN
Expand Down Expand Up @@ -393,6 +431,28 @@ function BuffBars:OnZoneChanged()
ns.Runtime.RequestLayout("BuffBars:OnZoneChanged")
end

--- Coalesces player aura changes into one deferred restyle after Blizzard's handler.
---@param unit string
function BuffBars:OnUnitAura(unit)
if unit ~= "player" then
return
end
if self._auraRestylePending then
return
end
self._auraRestylePending = true
local generation = (self._auraRestyleGeneration or 0) + 1
self._auraRestyleGeneration = generation

C_Timer.After(0, function()
if generation ~= self._auraRestyleGeneration then return end
self._auraRestylePending = nil
if self:IsEnabled() then
restyleActiveChildren(self, "UNIT_AURA")
end
end)
end

--- Gets a boolean indicating if editing is allowed.
--- @return boolean isEditLocked Whether editing is locked due to combat or secrets
--- @return string reason Reason editing is locked ("combat", "secrets", or nil)
Expand All @@ -416,16 +476,9 @@ function BuffBars:OnEnable()
self:RegisterEvent("ZONE_CHANGED_INDOORS", function(_, ...) self:OnZoneChanged() end)
self:RegisterEvent("PLAYER_ENTERING_WORLD", function(_, ...) self:OnZoneChanged() end)
-- Blizzard updates each child's auraInstanceID synchronously inside its own
-- UNIT_AURA handler but does not always re-fire SetPoint/OnShow/OnHide on
-- bars whose layout order is unchanged (e.g. a configured slot whose aura
-- toggles on/off). Defer a layout pass so StyleChildBar re-evaluates the
-- UNIT_AURA handler; OnUnitAura defers so StyleChildBar re-evaluates the
-- active-aura background after Blizzard's update completes.
self:RegisterEvent("UNIT_AURA", function(_, _, unit)
if unit ~= "player" then
return
end
ns.Runtime.RequestLayout("BuffBars:UNIT_AURA")
end)
self:RegisterEvent("UNIT_AURA", function(_, _, unit) self:OnUnitAura(unit) end)

C_Timer.After(0.1, function()
self:HookViewer()
Expand All @@ -436,4 +489,6 @@ end
function BuffBars:OnDisable()
self:UnregisterAllEvents()
ns.Runtime.UnregisterFrame(self)
self._auraRestylePending = nil
self._auraRestyleGeneration = (self._auraRestyleGeneration or 0) + 1
end
35 changes: 34 additions & 1 deletion Modules/ExternalBars.lua
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,30 @@ function ExternalBars:CreateFrame()
return frame
end

local function queueAuraUpdate(externalBars, reason)
externalBars._pendingAuraUpdateReason = reason
if externalBars._auraUpdatePending then return end

externalBars._auraUpdatePending = true
local generation = (externalBars._auraUpdateGeneration or 0) + 1
externalBars._auraUpdateGeneration = generation
C_Timer.After(0, function()
if generation ~= externalBars._auraUpdateGeneration then return end
externalBars._auraUpdatePending = nil
local updateReason = externalBars._pendingAuraUpdateReason
externalBars._pendingAuraUpdateReason = nil
if externalBars:IsEnabled() then
externalBars:OnExternalAurasUpdated(updateReason)
end
end)
end

local function cancelQueuedAuraUpdate(externalBars)
externalBars._auraUpdateGeneration = (externalBars._auraUpdateGeneration or 0) + 1
externalBars._auraUpdatePending = nil
externalBars._pendingAuraUpdateReason = nil
end

function ExternalBars:HookViewer()
local viewer = getViewer()
if not viewer then
Expand All @@ -769,14 +793,15 @@ function ExternalBars:HookViewer()

hooksecurefunc(viewer, "UpdateAuras", function()
if self:IsEnabled() then
self:OnExternalAurasUpdated("viewer:UpdateAuras")
queueAuraUpdate(self, "viewer:UpdateAuras")
end
end)

viewer:HookScript("OnShow", function()
if not self:IsEnabled() then
return
end
cancelQueuedAuraUpdate(self)
self:_RefreshOriginalIconsState()
self:OnExternalAurasUpdated("viewer:OnShow")
end)
Expand All @@ -785,6 +810,7 @@ function ExternalBars:HookViewer()
if not self:IsEnabled() then
return
end
cancelQueuedAuraUpdate(self)
self._activeAuraCount = 0
self:_hideExcessBars(0)
self:_StopDurationTicker()
Expand All @@ -805,6 +831,7 @@ function ExternalBars:OnExternalAurasUpdated(reason)
local auraInfo = viewer and viewer.auraInfo or nil
local auraStates = self._auraStates or {}
self._auraStates = auraStates
local previousAuraCount = self._activeAuraCount or 0
local debugEnabled = ns.IsDebugEnabled()
local auraDiagnostics = debugEnabled and {} or nil

Expand Down Expand Up @@ -866,6 +893,11 @@ function ExternalBars:OnExternalAurasUpdated(reason)
})
end

if activeAuraCount == previousAuraCount and self.InnerFrame then
self:UpdateLayout(reason)
return
end

ns.Runtime.RequestLayout("ExternalBars:" .. (reason or "UpdateAuras"), {
diagnostics = function()
return self:_GetLayoutRequestDiagnostics(reason, viewer, auraInfo, activeAuraCount)
Expand Down Expand Up @@ -1032,6 +1064,7 @@ end

function ExternalBars:OnDisable()
self:UnregisterAllEvents()
cancelQueuedAuraUpdate(self)
self:_StopDurationTicker()
self:_SetOriginalIconsHidden(false)
self._activeAuraCount = 0
Expand Down
37 changes: 34 additions & 3 deletions Modules/ExtraIcons.lua
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,36 @@ end
-- Events and hooks
--------------------------------------------------------------------------------

local function getMainFootprint(extraIcons)
local state = extraIcons._viewers and extraIcons._viewers.main
local container = state and state.container
local shown = container and container:IsShown() or false
return shown, shown and container:GetWidth() or 0, shown and container:GetScale() or 1
end

local function reconcileBagCooldown(extraIcons)
if not extraIcons:IsEnabled() or not extraIcons.InnerFrame or not extraIcons._viewers then return end

local wasShown, oldWidth, oldScale = getMainFootprint(extraIcons)
extraIcons:UpdateLayout("OnBagUpdateCooldown")
local isShown, newWidth, newScale = getMainFootprint(extraIcons)
if wasShown ~= isShown or not ns.NumericEquals(oldWidth, newWidth) or not ns.NumericEquals(oldScale, newScale) then
ns.Runtime.RequestLayout("ExtraIcons:OnBagUpdateCooldown:FootprintChanged")
end
end

--- Coalesces bag cooldown bursts and reconciles after Blizzard updates item data.
function ExtraIcons:OnBagUpdateCooldown()
self:ThrottledRefresh("OnBagUpdateCooldown")
ns.Runtime.RequestLayout("ExtraIcons:OnBagUpdateCooldown")
if self._bagCooldownPending then return end
self._bagCooldownPending = true
local generation = (self._bagCooldownGeneration or 0) + 1
self._bagCooldownGeneration = generation

C_Timer.After(0, function()
if generation ~= self._bagCooldownGeneration then return end
self._bagCooldownPending = nil
reconcileBagCooldown(self)
end)
end

function ExtraIcons:OnBagUpdateDelayed()
Expand Down Expand Up @@ -618,8 +645,12 @@ function ExtraIcons:OnDisable()
ns.Runtime.UnregisterFrame(self)

if self._viewers then
for _, vs in pairs(self._viewers) do vs.originalPoint = nil end
for _, vs in pairs(self._viewers) do
vs.originalPoint = nil
end
end
self._isEditModeActive = nil
self._trackedEquipSlots = nil
self._bagCooldownPending = nil
self._bagCooldownGeneration = (self._bagCooldownGeneration or 0) + 1
end
51 changes: 49 additions & 2 deletions Tests/Modules/BuffBars_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -845,11 +845,33 @@ describe("BuffBars real source", function()
assert.is_true(BuffBars._viewerHooked)
end)

it("registers UNIT_AURA on enable and requests layout only for player auras", function()
it("coalesces player UNIT_AURA into one owner-local restyle", function()
stubChildLayoutEnvironment()
local child = makeStyledChild("Changed Aura", true, 1)
function BuffBarCooldownViewer:GetChildren()
return child
end
local discovered = {}
spellColorStore.DiscoverBar = function(_, bar)
discovered[#discovered + 1] = bar
end
local captured = {}
function BuffBars:RegisterEvent(event, cb)
captured[event] = cb
end
function BuffBars:IsEnabled()
return true
end
function BuffBars:GetModuleConfig()
return { showIcon = false, showSpellName = true, showDuration = true }
end
function BuffBars:GetGlobalConfig()
return {
texture = "Solid",
barHeight = 20,
barBgColor = { r = 0, g = 0, b = 0, a = 0.8 },
}
end
local reasons = {}
ns.Runtime.RequestLayout = function(reason)
reasons[#reasons + 1] = reason
Expand All @@ -862,8 +884,33 @@ describe("BuffBars real source", function()
-- LibEvent dispatches cb(target, event, ...wowArgs)
cb(BuffBars, "UNIT_AURA", "target")
cb(BuffBars, "UNIT_AURA", "player")
cb(BuffBars, "UNIT_AURA", "player")
BuffBars._editLocked = true
BuffBars._warned = true

assert.are.equal(2, #timerCallbacks)
timerCallbacks[2]()

assert.same({}, reasons)
assert.is_nil(BuffBars._auraRestylePending)
assert.is_false(BuffBars._editLocked)
assert.is_false(BuffBars._warned)
assert.is_true(child.__ecmHooked)
assert.same({ child }, discovered)
end)

it("ignores an early player aura update while the viewer is unavailable", function()
BuffBarCooldownViewer = nil
_G.BuffBarCooldownViewer = nil
function BuffBars:IsEnabled()
return true
end

BuffBars:OnUnitAura("player")
timerCallbacks[1]()

assert.same({ "BuffBars:UNIT_AURA" }, reasons)
assert.same({}, errorLogs)
assert.is_nil(BuffBars._auraRestylePending)
end)

it("unregisters on disable", function()
Expand Down
55 changes: 55 additions & 0 deletions Tests/Modules/ExternalBars_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,61 @@ describe("ExternalBars real source", function()
assert.are.equal(18, diagnostics.globalBarHeight)
end)

it("updates same-count aura content locally without requesting global layout", function()
setViewerAuras({
{
auraInstanceID = 11,
texture = 5011,
duration = 12,
expirationTime = 112,
auraData = { name = "Ironbark", spellId = 102342 },
},
})
assert.is_true(syncAndLayout("initial"))
requestLayoutReasons = {}

setViewerAuras({
{
auraInstanceID = 22,
texture = 5022,
duration = 10,
expirationTime = 110,
auraData = { name = "Pain Suppression", spellId = 33206 },
},
})
ExternalBars:OnExternalAurasUpdated("viewer:UpdateAuras")

assert.same({}, requestLayoutReasons)
assert.are.equal("Pain Suppression", ExternalBars._barPool[1].Bar.Name:GetText())
assert.are.equal(5022, ExternalBars._barPool[1]._iconTexture:GetTexture())
end)

it("coalesces viewer aura update bursts before synchronizing", function()
setViewerAuras({
{
auraInstanceID = 11,
texture = 5011,
duration = 12,
expirationTime = 112,
auraData = { name = "Ironbark", spellId = 102342 },
},
})
ensureModuleFrame()
function ExternalBars:IsEnabled()
return true
end
ExternalBars:HookViewer()

viewer:UpdateAuras()
viewer:UpdateAuras()

assert.are.equal(1, #afterCallbacks)
assert.same({}, requestLayoutReasons)
afterCallbacks[1]()

assert.same({ "ExternalBars:viewer:UpdateAuras" }, requestLayoutReasons)
end)

it("configures duration bar progress when duration text is hidden", function()
profile.externalBars.showDuration = false

Expand Down
Loading