diff --git a/EllesmereUIActionBars/EllesmereUIActionBars.lua b/EllesmereUIActionBars/EllesmereUIActionBars.lua index d68179238..884d1bc4c 100644 --- a/EllesmereUIActionBars/EllesmereUIActionBars.lua +++ b/EllesmereUIActionBars/EllesmereUIActionBars.lua @@ -1717,6 +1717,10 @@ _secureHandler:SetAttribute("_onattributechanged", [=[ -- Register all buttons and bar frames as refs on the secure handler. -- Must be called AFTER SetupBar creates buttons (barButtons is populated). local _secureRefsReady = false +-- One pass over every button that exists. Lazily built bars add buttons after +-- it has already run, so the reveal path in RefreshRuntimeVisibility clears +-- _secureRefsReady and calls this again; indices are reassigned consistently in +-- the same pass, and the only readers of btn._secureSlotIdx run after a full one. local function SecureSetupHandler_PrepareRefs() if _secureRefsReady then return end _secureRefsReady = true @@ -2987,7 +2991,13 @@ end ------------------------------------------------------------------------------- -- Bar Setup creates frames and buttons for each bar ------------------------------------------------------------------------------- -local function SetupBar(info, skipProtected) +-- Build a bar's buttons into an EXISTING bar frame and record its base size. +-- Split out of SetupBar so a bar whose buttons were skipped at load can have +-- them built later, at the reveal edge: CreateBarFrame always creates a frame, +-- so re-running SetupBar there would leave a second one behind. +-- On ns rather than a file local: this file is at Lua 5.1's 200-local cap for +-- the main chunk, and a second top-level local here overflows it. +ns.BuildBarButtons = function(info, frame, skipProtected) -- Shrink the clickable area to match a custom visual shape so a square -- hit rect can't steal clicks from diamond/circle/etc neighbours. Insets -- are a fraction of button size; "none" resets to full square. @@ -3013,7 +3023,6 @@ local function SetupBar(info, skipProtected) end local key = info.key - local frame = CreateBarFrame(info) local buttons = {} local buttonShape = EAB and EAB.db and EAB.db.profile and EAB.db.profile.bars[key] and EAB.db.profile.bars[key].buttonShape or "none" @@ -3121,6 +3130,7 @@ local function SetupBar(info, skipProtected) end barButtons[key] = buttons + ns._eabBarNoButtons[key] = nil -- Store original button size before any shape/scale modifications. -- StanceButtons and PetActionButtons are 30x30; action buttons are 45x45. @@ -3132,7 +3142,27 @@ local function SetupBar(info, skipProtected) h = math.floor((btn1 and btn1:GetHeight() or 45) + 0.5), } - return frame, buttons + return buttons +end + +local function SetupBar(info, skipProtected) + local key = info.key + local frame = CreateBarFrame(info) + -- A bar that can never become visible gets no buttons at load: the button + -- loop is 95 % of such a bar's setup cost, and most of that is Blizzard's + -- CreateFrame on the action button template, which only a lower button + -- count can reach. The frame IS still built, so barFrames[key] stays + -- non-nil for its 52 indexing sites and barButtons[key] is empty, not nil. + -- RefreshRuntimeVisibility builds them when the bar leaves the Never set. + if ns.IsNeverBar(info) then + barButtons[key] = {} + ns._eabBarNoButtons[key] = true + -- Placeholder only, same fallback the button-derived value would take + -- with no buttons. BuildBarButtons recomputes it from the real button. + barBaseSize[key] = { w = 45, h = 45 } + return frame, barButtons[key] + end + return frame, ns.BuildBarButtons(info, frame, skipProtected) end ------------------------------------------------------------------------------- @@ -3568,6 +3598,25 @@ ns._eabBarDormant = {} -- mid-combat, where a heavier reconcile would spike). ns._eabBarNever = {} ns._eabBarNeverWas = {} +-- Bars whose buttons were skipped at load because they were Never bars then. +-- Cleared by BuildBarButtons. Drives the lazy build in RefreshRuntimeVisibility. +ns._eabBarNoButtons = {} +-- Single source of truth for "this bar can never become visible through any +-- runtime condition". RecomputeNeverBars and the load-time button skip both +-- read it, so the two cannot drift apart -- a separately maintained restore +-- predicate already drifted once in this file (see RestoreGridSurfacedBars). +ns.IsNeverBar = function(info) + if info.isStance or info.isPetBar or info.visibilityOnly then return false end + local bars = EAB.db and EAB.db.profile and EAB.db.profile.bars + local s = bars and bars[info.key] + local never = s and (s.alwaysHidden or s.enabled == false) or false + -- Toggle override wins both ways: hiding an Always bar hard-disables its UI + -- work; showing a Never bar wakes it. Action bindings stay live. + local override = EAB._visOverride and EAB._visOverride[info.key] + if override == "never" then never = true + elseif override == "always" then never = false end + return never and true or false +end ns.RecomputeNeverBars = function() local bars = EAB.db and EAB.db.profile and EAB.db.profile.bars if not bars then return end @@ -3575,14 +3624,8 @@ ns.RecomputeNeverBars = function() local changed = false for _, info in ipairs(BAR_CONFIG) do if not info.isStance and not info.isPetBar and not info.visibilityOnly then - local s = bars[info.key] - local override = EAB._visOverride and EAB._visOverride[info.key] - local never = s and (s.alwaysHidden or s.enabled == false) or false - -- Toggle override wins both ways: hiding an Always bar hard-disables - -- its UI work; showing a Never bar wakes it. Action bindings stay live. - if override == "never" then never = true - elseif override == "always" then never = false end - never = never and true or nil + -- nil rather than false so the map stays sparse for its readers. + local never = ns.IsNeverBar(info) or nil if map[info.key] ~= never then if map[info.key] and not never then -- Leaving Never: remember to run the one heal the gates skipped (AlwaysShow grid). @@ -5923,7 +5966,12 @@ local function LayoutBar(key) fillS = table.concat(tf) end _lbStamp = table.concat({ - tostring(nIcoS), tostring(s.overrideNumRows or s.numRows or 1), + -- #buttons, not just the configured count: a Never bar is built with + -- none and gains them at the reveal edge, and every other stamp input + -- can be identical across that edge (the base size falls back to the + -- same 45x45 the real button reports). + tostring(nIcoS), tostring(#buttons), + tostring(s.overrideNumRows or s.numRows or 1), tostring(s.buttonPadding or 2), tostring(s.orientation), growDirS or "-", tostring(s.buttonShape), tostring(s.buttonWidth), tostring(s.buttonHeight), tostring(s._matchExtraPixels), tostring(s._matchExtraPixelsH), @@ -9466,6 +9514,42 @@ function EAB:RefreshRuntimeVisibility() -- through here (this is where drivers re-derive), so this is the single -- recompute site for the hard-dormancy map the event walks gate on. ns.RecomputeNeverBars() + -- Build the buttons of any bar that has left the Never set but had them + -- skipped at load (see SetupBar). Deliberately state-based rather than + -- edge-based: an options write taken in combat flips the map here but + -- cannot create secure frames, and the PLAYER_REGEN_ENABLED healer + -- re-enters this function with the map ALREADY flipped, so a transition + -- test would never fire again and the bar would stay buttonless until the + -- next reload. Testing the state instead means the deferred run heals it. + if next(ns._eabBarNoButtons) and not InCombatLockdown() then + local built = false + for _, info in ipairs(BAR_CONFIG) do + local frame = barFrames[info.key] + if frame and ns._eabBarNoButtons[info.key] + and not ns._eabBarNever[info.key] then + ns.BuildBarButtons(info, frame, false) + LayoutBar(info.key) + built = true + end + end + if built then + -- The refs pass already ran without these buttons in it. + _secureRefsReady = false + SecureSetupHandler_PrepareRefs() + -- ~200 override bindings are built from BAR_CONFIG x barButtons, so + -- a revealed bar has none until this runs. Defers itself in combat. + if _G._EAB_UpdateKeybinds then _G._EAB_UpdateKeybinds() end + -- Everything cosmetic (borders, shapes, fonts, backgrounds, button + -- art, cooldown visuals, range colouring) converges through the + -- full apply. Next frame, not inline: ApplyAll calls back into this + -- function, and the marker is already cleared by then so it finds + -- nothing to build. On ns, not on EAB, so the EUI_UnlockMode hook + -- on EAB.ApplyAll stays as it was. + C_Timer.After(0, function() + if ns._eabApplyAll then ns._eabApplyAll() end + end) + end + end self:_RefreshSoftTargetGate() for _, info in ipairs(ALL_BARS) do local key = info.key @@ -12377,6 +12461,12 @@ local function ApplyAll() _isApplyingAll = false end +-- Handle for the lazy bar build in RefreshRuntimeVisibility, which sits above +-- this definition and cannot see the local. On ns and not on EAB deliberately: +-- the EUI_UnlockMode hook watches EAB.ApplyAll, and putting it there would +-- start firing that hook for the first time. +ns._eabApplyAll = ApplyAll + ------------------------------------------------------------------------------- -- Position Save/Restore -------------------------------------------------------------------------------