From 244144a43e75ee0540794949015c0ec03fc4e2ac Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Sat, 5 Sep 2026 16:06:18 +0200 Subject: [PATCH] Optimize ItemButton cursor polling and drop glows --- UI/ItemButtonPerformanceClassicAPI.lua | 247 +++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 UI/ItemButtonPerformanceClassicAPI.lua diff --git a/UI/ItemButtonPerformanceClassicAPI.lua b/UI/ItemButtonPerformanceClassicAPI.lua new file mode 100644 index 0000000..3a7bf27 --- /dev/null +++ b/UI/ItemButtonPerformanceClassicAPI.lua @@ -0,0 +1,247 @@ +-- Guda ClassicAPI ItemButton performance layer +-- Keeps the original ItemButton behavior but removes permanent idle polling +-- and consolidates per-drop-target glow OnUpdate work into one shared driver. +-- Lua 5.0 compatible. + +local addon = Guda + +local ItemButtonPerformance = {} +addon.Modules.ItemButtonPerformance = ItemButtonPerformance + +local cursorWatcher = nil +local originalCursorOnEvent = nil +local originalCursorOnUpdate = nil +local cursorUpdateWrapper = nil +local cursorEventWrapper = nil +local cursorPollingEnabled = false + +local glowDrivers = {} +local glowDriverCount = 0 +local glowScanAccum = 0 +local glowPulseTime = 0 + +local sharedGlowDriver = CreateFrame("Frame", "Guda_SharedDropGlowDriver", UIParent) +sharedGlowDriver:Hide() + +local function IsShown(frame) + return frame and frame.IsShown and frame:IsShown() +end + +local function IsRelevantFrameOpen() + if IsShown(getglobal("Guda_BagFrame")) then return true end + if IsShown(getglobal("Guda_BankFrame")) then return true end + return false +end + +local function CursorIsCarrying() + return CursorHasItem and CursorHasItem() and true or false +end + +local function FindCursorWatcher() + if type(GetFramesRegisteredForEvent) ~= "function" then + return nil + end + + local frames = { GetFramesRegisteredForEvent("CURSOR_UPDATE") } + for i = 1, table.getn(frames) do + local frame = frames[i] + if frame + and frame._lastCarrying ~= nil + and frame._pollAccum ~= nil + and frame.GetScript + and frame:GetScript("OnUpdate") then + return frame + end + end + + return nil +end + +local function ShouldCursorPoll() + if IsRelevantFrameOpen() then return true end + if CursorIsCarrying() then return true end + if cursorWatcher and cursorWatcher.pendingCheck then return true end + return false +end + +local function ScanGlowDrivers() + if type(EnumerateFrames) ~= "function" then return end + + local frame = EnumerateFrames() + while frame do + if frame._t ~= nil and frame.GetParent then + local parent = frame:GetParent() + if parent + and parent.dropGlowDriver == frame + and parent.dropGlow then + if not glowDrivers[frame] then + glowDrivers[frame] = parent + glowDriverCount = glowDriverCount + 1 + end + + -- The original driver only computes the same sine wave for + -- its own button. The shared driver below does that once for + -- every active glow, so the per-button OnUpdate is redundant. + if frame.GetScript and frame:GetScript("OnUpdate") then + frame:SetScript("OnUpdate", nil) + end + end + end + + frame = EnumerateFrames(frame) + end +end + +local function StartSharedGlowDriver() + if not sharedGlowDriver:IsShown() then + glowScanAccum = 0.1 -- discover existing/new drivers immediately + sharedGlowDriver:Show() + end +end + +sharedGlowDriver:SetScript("OnUpdate", function() + glowPulseTime = glowPulseTime + arg1 + glowScanAccum = glowScanAccum + arg1 + + -- Drop-target drivers are created lazily by ItemButton.lua. Scan only + -- while an item is being dragged, and at 10 Hz rather than every frame. + if glowScanAccum >= 0.1 then + glowScanAccum = 0 + ScanGlowDrivers() + end + + local phase = (math.sin(glowPulseTime * 5.24) + 1) * 0.5 + local alpha = 0.15 + phase * 0.30 + local anyActive = false + + for driver, parent in pairs(glowDrivers) do + if driver and parent and driver.IsShown and driver:IsShown() + and parent.dropGlow and parent.dropGlow.IsShown + and parent.dropGlow:IsShown() then + parent.dropGlow:SetAlpha(alpha) + anyActive = true + end + end + + if not CursorIsCarrying() and not anyActive then + this:Hide() + end +end) + +local function SetCursorPolling(enabled) + if not cursorWatcher or not originalCursorOnUpdate then return end + + enabled = enabled and true or false + if enabled == cursorPollingEnabled then return end + cursorPollingEnabled = enabled + + if enabled then + cursorWatcher:SetScript("OnUpdate", cursorUpdateWrapper) + else + cursorWatcher:SetScript("OnUpdate", nil) + end +end + +local function RefreshCursorPolling() + if ShouldCursorPoll() then + SetCursorPolling(true) + if CursorIsCarrying() then + StartSharedGlowDriver() + end + else + SetCursorPolling(false) + end +end + +local function HookVisibilityFrame(frame) + if not frame or frame._gudaClassicAPIPerfHooked then return end + frame._gudaClassicAPIPerfHooked = true + + local oldOnShow = frame:GetScript("OnShow") + local oldOnHide = frame:GetScript("OnHide") + + frame:SetScript("OnShow", function() + if oldOnShow then oldOnShow() end + RefreshCursorPolling() + end) + + frame:SetScript("OnHide", function() + if oldOnHide then oldOnHide() end + + -- The other bag/bank frame can change visibility in the same call + -- stack, so defer the idle decision one frame. + if Guda_ScheduleTimer then + Guda_ScheduleTimer(0, RefreshCursorPolling) + else + RefreshCursorPolling() + end + end) +end + +function ItemButtonPerformance:Initialize() + if cursorWatcher then return true end + + cursorWatcher = FindCursorWatcher() + if not cursorWatcher then + addon:Debug("ClassicAPI ItemButton perf: cursor watcher not found") + return false + end + + originalCursorOnEvent = cursorWatcher:GetScript("OnEvent") + originalCursorOnUpdate = cursorWatcher:GetScript("OnUpdate") + + cursorUpdateWrapper = function() + -- Preserve the original 1.12 safety-net logic while relevant UI is + -- open or an item is being carried. + originalCursorOnUpdate() + + if CursorIsCarrying() then + StartSharedGlowDriver() + end + + if not ShouldCursorPoll() then + SetCursorPolling(false) + end + end + + cursorEventWrapper = function() + if originalCursorOnEvent then + originalCursorOnEvent() + end + RefreshCursorPolling() + end + + cursorWatcher:SetScript("OnEvent", cursorEventWrapper) + + HookVisibilityFrame(getglobal("Guda_BagFrame")) + HookVisibilityFrame(getglobal("Guda_BankFrame")) + + -- Prime discovery once so any lazily-created glow drivers that already + -- exist are converted to the shared driver immediately. + ScanGlowDrivers() + RefreshCursorPolling() + + addon:Debug("ClassicAPI ItemButton perf initialized: idle cursor polling gated, shared glow driver active") + return true +end + +function ItemButtonPerformance:GetStats() + return { + cursorWatcherFound = cursorWatcher ~= nil, + cursorPollingEnabled = cursorPollingEnabled, + sharedGlowDrivers = glowDriverCount, + sharedGlowActive = sharedGlowDriver:IsShown(), + } +end + +-- All XML frames are guaranteed to exist by PLAYER_LOGIN. The fallback retry +-- handles unusual load orders without keeping another permanent OnUpdate alive. +local initFrame = CreateFrame("Frame") +initFrame:RegisterEvent("PLAYER_LOGIN") +initFrame:RegisterEvent("PLAYER_ENTERING_WORLD") +initFrame:SetScript("OnEvent", function() + if ItemButtonPerformance:Initialize() then + this:UnregisterEvent("PLAYER_LOGIN") + this:UnregisterEvent("PLAYER_ENTERING_WORLD") + end +end)