diff --git a/Modules/BuffBars.lua b/Modules/BuffBars.lua
index 4cef909..ec5974b 100644
--- a/Modules/BuffBars.lua
+++ b/Modules/BuffBars.lua
@@ -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
@@ -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)
@@ -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()
@@ -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
diff --git a/Modules/ExternalBars.lua b/Modules/ExternalBars.lua
index 193ff43..53e7d50 100644
--- a/Modules/ExternalBars.lua
+++ b/Modules/ExternalBars.lua
@@ -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
@@ -769,7 +793,7 @@ function ExternalBars:HookViewer()
hooksecurefunc(viewer, "UpdateAuras", function()
if self:IsEnabled() then
- self:OnExternalAurasUpdated("viewer:UpdateAuras")
+ queueAuraUpdate(self, "viewer:UpdateAuras")
end
end)
@@ -777,6 +801,7 @@ function ExternalBars:HookViewer()
if not self:IsEnabled() then
return
end
+ cancelQueuedAuraUpdate(self)
self:_RefreshOriginalIconsState()
self:OnExternalAurasUpdated("viewer:OnShow")
end)
@@ -785,6 +810,7 @@ function ExternalBars:HookViewer()
if not self:IsEnabled() then
return
end
+ cancelQueuedAuraUpdate(self)
self._activeAuraCount = 0
self:_hideExcessBars(0)
self:_StopDurationTicker()
@@ -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
@@ -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)
@@ -1032,6 +1064,7 @@ end
function ExternalBars:OnDisable()
self:UnregisterAllEvents()
+ cancelQueuedAuraUpdate(self)
self:_StopDurationTicker()
self:_SetOriginalIconsHidden(false)
self._activeAuraCount = 0
diff --git a/Modules/ExtraIcons.lua b/Modules/ExtraIcons.lua
index 98a3e44..7a50fea 100644
--- a/Modules/ExtraIcons.lua
+++ b/Modules/ExtraIcons.lua
@@ -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()
@@ -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
diff --git a/Tests/Modules/BuffBars_spec.lua b/Tests/Modules/BuffBars_spec.lua
index 335465d..3f20216 100644
--- a/Tests/Modules/BuffBars_spec.lua
+++ b/Tests/Modules/BuffBars_spec.lua
@@ -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
@@ -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()
diff --git a/Tests/Modules/ExternalBars_spec.lua b/Tests/Modules/ExternalBars_spec.lua
index a510043..b019f93 100644
--- a/Tests/Modules/ExternalBars_spec.lua
+++ b/Tests/Modules/ExternalBars_spec.lua
@@ -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
diff --git a/Tests/Modules/ExtraIcons_spec.lua b/Tests/Modules/ExtraIcons_spec.lua
index 911a79b..459d8da 100644
--- a/Tests/Modules/ExtraIcons_spec.lua
+++ b/Tests/Modules/ExtraIcons_spec.lua
@@ -630,26 +630,81 @@ describe("ExtraIcons real source", function()
assert.is_true(#vs.iconPool >= 1)
end)
- it("refreshes cooldowns via ThrottledRefresh", function()
- local reasons = {}
- function ExtraIcons:ThrottledRefresh(reason)
- reasons[#reasons + 1] = reason
+ it("coalesces bag cooldown bursts and updates a healthstone locally", function()
+ local layoutReasons = {}
+ ns.Runtime.RequestLayout = function(reason)
+ layoutReasons[#layoutReasons + 1] = reason
+ end
+ local utilityIconChild = TestHelpers.makeFrame({ shown = true, width = 18, height = 18 })
+ utilityIconChild.GetSpellID = function() return 1 end
+ UtilityCooldownViewer.childXPadding = 0
+ UtilityCooldownViewer.iconScale = 1
+ UtilityCooldownViewer._children = { utilityIconChild }
+ UtilityCooldownViewer:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
+ itemCounts[HEALTHSTONE_ID] = 1
+ itemIconsByID[HEALTHSTONE_ID] = "healthstone"
+ ExtraIcons.GetModuleConfig = function()
+ return makeViewersConfig({ { kind = "itemStack", itemStackId = "healthstones" } })
end
-
ExtraIcons.InnerFrame = ExtraIcons:CreateFrame()
+ assert.is_true(ExtraIcons:UpdateLayout("initial"))
+ itemCooldownByID[HEALTHSTONE_ID] = { 100, 60, true }
+
+ ExtraIcons:OnBagUpdateCooldown()
ExtraIcons:OnBagUpdateCooldown()
- assert.same({ "OnBagUpdateCooldown" }, reasons)
+
+ assert.are.equal(1, #timerCallbacks)
+ timerCallbacks[1]()
+
+ assert.same({ 100, 60 }, ExtraIcons._viewers.utility.iconPool[1].Cooldown.__cooldown)
+ assert.same({}, layoutReasons)
end)
- it("requests layout on bag cooldown change so charged item display is re-evaluated", function()
+ it("requests downstream layout when bag cooldown reconciliation changes the main footprint", function()
local layoutReasons = {}
ns.Runtime.RequestLayout = function(reason)
layoutReasons[#layoutReasons + 1] = reason
end
+ local mainIconChild = TestHelpers.makeFrame({ shown = true, width = 18, height = 18 })
+ mainIconChild.GetSpellID = function() return 1 end
+ EssentialCooldownViewer.childXPadding = 0
+ EssentialCooldownViewer.iconScale = 1
+ EssentialCooldownViewer._children = { mainIconChild }
+ EssentialCooldownViewer:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
+ itemCounts[HEALTHSTONE_ID] = 1
+ itemIconsByID[HEALTHSTONE_ID] = "healthstone"
+ ExtraIcons.GetModuleConfig = function()
+ return makeViewersConfig({}, { { kind = "itemStack", itemStackId = "healthstones" } })
+ end
+
ExtraIcons.InnerFrame = ExtraIcons:CreateFrame()
+ assert.is_true(ExtraIcons:UpdateLayout("initial"))
+ itemCounts[HEALTHSTONE_ID] = 0
+
ExtraIcons:OnBagUpdateCooldown()
- assert.same({ "ExtraIcons:OnBagUpdateCooldown" }, layoutReasons)
+ timerCallbacks[1]()
+
+ assert.same({ "ExtraIcons:OnBagUpdateCooldown:FootprintChanged" }, layoutReasons)
+ assert.is_false(ExtraIcons._viewers.main.container:IsShown())
+ end)
+
+ it("does not run a pending bag cooldown reconciliation after disable", function()
+ local layouts = 0
+ function ExtraIcons:UpdateLayout()
+ layouts = layouts + 1
+ end
+ function ExtraIcons:UnregisterAllEvents() end
+
+ ExtraIcons.InnerFrame = ExtraIcons:CreateFrame()
+ ExtraIcons:OnBagUpdateCooldown()
+ ExtraIcons:OnDisable()
+ assert.are.equal(1, layouts)
+
+ timerCallbacks[1]()
+
+ assert.are.equal(1, layouts)
+ assert.is_nil(ExtraIcons._bagCooldownPending)
end)
it("edit mode callbacks toggle state and defer layout", function()
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index ff9e130..cc3304d 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -135,7 +135,7 @@ flowchart TD
## Event Flow & Layout Pipeline
-WoW events funnel through `Runtime.lua` into a data-driven scheduler path. Red nodes and regions identify changed or newly documented architecture.
+WoW events funnel through `Runtime.lua` into a data-driven scheduler path. High-frequency module-owned updates are coalesced locally: ExtraIcons bag cooldowns, BuffBars player auras, and ExternalBars aura hooks update only their owner unless the change affects downstream geometry. Red nodes and regions identify changed or newly documented architecture.
```mermaid
flowchart TD
@@ -153,6 +153,7 @@ flowchart TD
PB_PWR["UNIT_POWER_UPDATE
→ PowerBar handler"]
RB_AURA["UNIT_AURA / UNIT_POWER_UPDATE
→ ResourceBar handler"]
BB_ZONE["ZONE_CHANGED_* / PLAYER_ENTERING_WORLD
→ BuffBars:OnZoneChanged"]
+ LOCAL["High-frequency owner updates
ExtraIcons / BuffBars / ExternalBars
→ coalesced local update"]
end
subgraph HOOKS["Frame Hooks (BuffBars / ExternalBars)"]
@@ -201,6 +202,7 @@ flowchart TD
WOW_MOUNT & WOW_COMBAT & WOW_ZONE & WOW_SPEC & WOW_TARGET & WOW_REST & WOW_CVAR --> DISPATCH
DISPATCH --> SCHEDULER
PB_PWR & RB_AURA & BB_ZONE --> REQ_LAY
+ LOCAL --> MOD_UPD
BB_HOOKS & EB_HOOKS --> REQ_LAY
FLUSH --> EXECUTE
UPD_FADE --> FADE_LOGIC
diff --git a/docs/BuffBars.md b/docs/BuffBars.md
index 2e59426..29451e4 100644
--- a/docs/BuffBars.md
+++ b/docs/BuffBars.md
@@ -8,7 +8,7 @@
| **Description** | Mirrors Blizzard's `BuffBarCooldownViewer` area into ECM-styled aura bars. ECM repositions and restyles Blizzard-owned child bars instead of creating its own aura rows. |
| **Source file** | [`Modules/BuffBars.lua`](../Modules/BuffBars.lua) |
| **Mixin** | `BarMixin.AddFrameMixin(self, "BuffBars")` using `BarMixin.FrameProto` methods such as `EnsureFrame()`, `GetModuleConfig()`, `ShouldShow()`, and `CalculateLayoutParams()`. |
-| **Events listened to** | - `ZONE_CHANGED_NEW_AREA` — refreshes zone-specific Blizzard aura bars and requests layout.
- `ZONE_CHANGED` — refreshes zone changes that can alter the viewer's child set.
- `ZONE_CHANGED_INDOORS` — refreshes indoor/outdoor aura transitions.
- `PLAYER_ENTERING_WORLD` — catches initial world entry and reload/login transitions.
- `UNIT_AURA` (player only) — requests a deferred layout so `StyleChildBar` re-evaluates the active-aura background after Blizzard updates each child's `auraInstanceID`. |
+| **Events listened to** | - `ZONE_CHANGED_NEW_AREA` — refreshes zone-specific Blizzard aura bars and requests layout.
- `ZONE_CHANGED` — refreshes zone changes that can alter the viewer's child set.
- `ZONE_CHANGED_INDOORS` — refreshes indoor/outdoor aura transitions.
- `PLAYER_ENTERING_WORLD` — catches initial world entry and reload/login transitions.
- `UNIT_AURA` (player only) — coalesces bursts into one deferred owner-local restyle after Blizzard updates each child's `auraInstanceID`; structural child hooks still request global layout. |
| **Hooks** | - `BuffBarCooldownViewer:OnShow` — requests a layout pass when Blizzard re-shows the viewer.
- `BuffBarCooldownViewer:OnSizeChanged` — requests a second-pass layout when Blizzard changes viewer width/size.
- `child:SetPoint` — restores ECM's cached anchors, restyles the child, and queues a second-pass layout.
- `child:OnShow` — reapplies ECM styling and queues a second-pass layout.
- `child:OnHide` — queues a second-pass layout so the remaining bars restack cleanly. |
| **Dependencies** | - `ns.BarMixin` / `BarMixin.FrameProto` — frame-module lifecycle, config access, anchor calculation.
- `ns.Runtime` — frame registration plus `RequestLayout()` / layout execution.
- `ns.BarStyle.StyleChildBar` — applies ECM visuals to Blizzard child bars.
- `ns.FrameUtil` — lazy anchors, width snapshots, icon texture lookup.
- `ns.SpellColors.Get("buffBars")` — scoped spell-color discovery, lookup, and cache clearing.
- `ns.Constants` / `ns.defaults` — scope name, anchor-mode semantics, default colors/config.
- Blizzard `BuffBarCooldownViewer` and its child aura-bar frames — source viewer and mirrored rows.
- `C_Timer.After(0.1)` — deferred hook install so the Blizzard viewer exists before BuffBars attaches hooks. |
| **Options file(s)** | [`UI/BuffBarsOptions.lua`](../UI/BuffBarsOptions.lua), plus BuffBars' section registration into [`UI/SpellColorsPage.lua`](../UI/SpellColorsPage.lua) |
@@ -54,6 +54,8 @@ sequenceDiagram
BuffBars->>Runtime: RequestLayout("BuffBars:OnZoneChanged")
Game->>BuffBars: PLAYER_ENTERING_WORLD
BuffBars->>Runtime: RequestLayout("BuffBars:OnZoneChanged")
+ Game->>BuffBars: UNIT_AURA("player")
+ BuffBars->>BuffBars: Coalesce and defer restyleActiveChildren()
end
rect rgb(46,30,46)
diff --git a/docs/ExternalBars.md b/docs/ExternalBars.md
index 70b2d91..6f20ed2 100644
--- a/docs/ExternalBars.md
+++ b/docs/ExternalBars.md
@@ -13,7 +13,7 @@ This module is intentionally absent from the `docs/ARCHITECTURE.md` Event Refere
| **Source file** | [`Modules/ExternalBars.lua`](../Modules/ExternalBars.lua) |
| **Mixin** | `BarMixin.AddFrameMixin`; inherits [`FrameProto`](../BarMixin.lua) |
| **Events listened to** | None for aura data. `ExternalBars` does not call `RegisterEvent()` in `Modules/ExternalBars.lua`; aura refresh is driven by hooks on `ExternalDefensivesFrame:UpdateAuras()` plus the frame's `OnShow` / `OnHide`. `OnDisable()` still calls `UnregisterAllEvents()` as defensive cleanup. |
-| **Hooks** | - Post-hook `ExternalDefensivesFrame:UpdateAuras()` → `OnExternalAurasUpdated()`
- `ExternalDefensivesFrame:HookScript("OnShow")` → refresh original-icon state, then resync aura state
- `ExternalDefensivesFrame:HookScript("OnHide")` → clear active rows, stop duration ticker, request layout
- `hideOriginalIcons` uses `ExternalDefensivesFrame:SetAlpha(0)` and `EnableMouse(false)` instead of `Hide()` so Blizzard keeps driving `UpdateAuras()` |
+| **Hooks** | - Post-hook `ExternalDefensivesFrame:UpdateAuras()` → coalesced deferred `OnExternalAurasUpdated()`
- Same-count updates run `ExternalBars:UpdateLayout()` locally; count changes request global layout because downstream anchors may move
- `ExternalDefensivesFrame:HookScript("OnShow")` → refresh original-icon state, then resync aura state
- `ExternalDefensivesFrame:HookScript("OnHide")` → clear active rows, stop duration ticker, request layout
- `hideOriginalIcons` uses `ExternalDefensivesFrame:SetAlpha(0)` and `EnableMouse(false)` instead of `Hide()` so Blizzard keeps driving `UpdateAuras()` |
| **Dependencies** | - `ns.SpellColors.Get("externalBars")` scoped color store
- `BarStyle.StyleChildBar(...)` shared BuffBars / ExternalBars row styling
- `C_UnitAuras.GetAuraDataByAuraInstanceID("player", auraInstanceID)` for accessible aura metadata
- `C_UnitAuras.GetAuraDuration("player", auraInstanceID)` duration objects for secret-safe bar timers and text
- `FrameUtil` lazy setters and icon helpers
- `ns.Runtime.RequestLayout(...)` / runtime layout passes |
| **Options file(s)** | [`UI/ExternalBarsOptions.lua`](../UI/ExternalBarsOptions.lua), shared section registration in [`UI/SpellColorsPage.lua`](../UI/SpellColorsPage.lua) |
| **Options dependencies** | - `ns.OptionUtil` for disabled predicates, default-value transforms, module toggle handling, and layout breadcrumbs
- `LibSettingsBuilder` for the declarative Settings rows consumed by the root options tree
- `ns.SpellColors` for the scoped color store edited by the shared page
- `ns.SpellColorsPage` for `RegisterSection(...)` and the shared spell-colors editor |
@@ -47,7 +47,7 @@ sequenceDiagram
EB->>Viewer: hooksecurefunc(UpdateAuras)
EB->>Viewer: HookScript(OnShow / OnHide)
EB->>EB: _RefreshOriginalIconsState()
- EB->>EB: OnExternalAurasUpdated()
+ EB->>EB: Coalesce into one deferred OnExternalAurasUpdated()
EB->>Runtime: RequestLayout("ExternalBars:OnEnable")
end
@@ -60,7 +60,11 @@ sequenceDiagram
AuraAPI-->>EB: accessible aura metadata
EB->>EB: copy into _auraStates[index]
end
- EB->>Runtime: RequestLayout("ExternalBars:UpdateAuras")
+ alt active aura count changed
+ EB->>Runtime: RequestLayout("ExternalBars:UpdateAuras")
+ else count unchanged
+ EB->>EB: UpdateLayout("viewer:UpdateAuras")
+ end
end
rect rgb(30,30,60)
diff --git a/docs/ExtraIcons.md b/docs/ExtraIcons.md
index bb25f01..ac95f17 100644
--- a/docs/ExtraIcons.md
+++ b/docs/ExtraIcons.md
@@ -8,7 +8,7 @@
| **Description** | Displays extra cooldown-tracked icons beside Blizzard's cooldown viewers. It uses a dual-viewer architecture (`utility` and `main`) so icons can either extend the essential viewer footprint or live beside the utility viewer independently. |
| **Source file** | [`Modules/ExtraIcons.lua`](../Modules/ExtraIcons.lua) |
| **Mixin** | `BarMixin.AddFrameMixin`; inherits [`BarMixin.FrameProto`](../BarMixin.lua) |
-| **Events listened to** |