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** | | +| **Events listened to** | | | **Layout ordering** | `ExtraIcons:UpdateLayout()` runs first inside `Runtime.updateAllLayouts()`, so the main viewer anchor reflects Blizzard icons plus appended extra icons before chained bars compute their own positions. | | **Dependencies** | | | **Blizzard APIs used** | | @@ -136,7 +136,8 @@ sequenceDiagram rect rgb(46,40,26) Blizz-->>Extra: BAG_UPDATE_COOLDOWN - Extra->>Extra: ThrottledRefresh("OnBagUpdateCooldown") + Extra->>Extra: Coalesce and defer UpdateLayout("OnBagUpdateCooldown") + Extra->>Runtime: RequestLayout only if main footprint changed Blizz-->>Extra: BAG_UPDATE_DELAYED Extra->>Runtime: RequestLayout("ExtraIcons:OnBagUpdateDelayed") Blizz-->>Extra: SPELL_UPDATE_COOLDOWN