Files
ElvUI_CoA/Modules/VanityFrame.lua
T
Narcasung 7862871e3f Kill first-render flicker on Talent/Vanity Blizzard-frame skins
The 0.5s ScheduleRepeatingTimer poll that caught these frames couldn't
catch them before their first-ever :Show() this session, since they're
created on-demand the instant the player opens them -- native art got
one frame of paint before the poll's next tick could skin it.

Switched to a generic ADDON_LOADED listener instead: it fires
synchronously (before control returns to whatever opens the frame)
the moment the owning addon finishes loading, so skinning now lands
before the frame is ever shown. Confirmed in-game via an ADDON_LOADED
probe on the talent frame's addon before implementing.

Also fixes a regression this introduced: TalentFrame's CropBackground
was caching the background texture's "original" tex coords on its
first-ever call for later crop math. That first call now happens
pre-Show, before Blizzard's own code sets the texture's real art-tile
region, so it was permanently baking in the XML-template default
(full 0..1) as the baseline. Moved the crop to run only from OnShow,
where the real coords are already set.
2026-08-17 21:24:42 +02:00

180 lines
5.8 KiB
Lua

local E, L, V, P, G = unpack(ElvUI)
local S = E:GetModule("Skins")
local CoA = E:GetModule("CoA")
local FRAME_NAME = "StoreCollectionFrame"
-- Confirmed live by probing size/position: these two OVERLAY "Portrait2"
-- regions sit at the currency counters (aligned with SPCounterHintButton and
-- DPCounterHintButton), not at the top-left corner. An earlier version of
-- this matched them as the portrait and stripped them, which blanked the
-- counter badges instead. Whatever the real top-left portrait is hasn't been
-- identified yet -- it isn't among StoreCollectionFrame's own regions or
-- children -- so nothing removes it for now.
local function SkinCloseButton(frame)
local close = _G[FRAME_NAME.."CloseButton"]
if not close then return end
-- Strictly once: HandleCloseButton strips the button every call, which
-- blanks the X texture it added on the first pass.
if not close.CoASkinned then
close.CoASkinned = true
S:HandleCloseButton(close)
end
end
-- Standard UIPanelButtonTemplate art (FontString + Normal/Pushed/Disabled/
-- Highlight textures), confirmed by probe -- S:HandleButton's own texture
-- clearing handles these directly, no manual stripping needed.
local function SkinActionButtons()
S:HandleButton(_G[FRAME_NAME.."ActivateStoreButton"])
S:HandleButton(_G[FRAME_NAME.."BuyStoreButton"])
end
local function SkinSearchBox()
S:HandleEditBox(_G[FRAME_NAME.."SearchBox"])
end
-- Unlike the action buttons, the dropdown's art is nine anonymous
-- "Silver-Button-Up"/"-Highlight" slices rather than named Left/Middle/Right
-- fields or a Normal/Pushed/Disabled texture set, so S:HandleButton's own
-- clearing can't reach them and a blind StripTextures would take the arrow
-- and label with them (the same mistake made on the portrait). Matched by
-- texture path instead, same fix as the portrait.
--
-- The native mouse-down handler re-sets one of these regions to a pressed
-- variant of the same file on every click, which is why the pill came back
-- skinless while held. SetTexture is noop'd per region after clearing it,
-- the same trick TalentFrame uses on rows the pool keeps re-arting.
local DROPDOWN_ART_PATTERNS = {"Silver%-Button"}
local function StripDropdownArt(dropdown)
for i = 1, dropdown:GetNumRegions() do
local region = select(i, dropdown:GetRegions())
local texture = region.GetTexture and region:GetTexture()
if texture then
for _, pattern in ipairs(DROPDOWN_ART_PATTERNS) do
if tostring(texture):find(pattern) then
region:SetTexture(nil)
region.SetTexture = E.noop
break
end
end
end
end
end
local function SkinDropdown()
local dropdown = _G[FRAME_NAME.."Dropdown"]
if not dropdown or dropdown.CoASkinned then return end
dropdown.CoASkinned = true
StripDropdownArt(dropdown)
S:HandleButton(dropdown)
-- The caret is a plain OVERLAY texture on the dropdown itself, not a
-- separate button, so it can't go through HandleNextPrevButton -- it's
-- retextured directly instead.
for i = 1, dropdown:GetNumRegions() do
local region = select(i, dropdown:GetRegions())
local texture = region.GetTexture and region:GetTexture()
if texture and tostring(texture):find("ChatFrameExpandArrow") then
-- No ArrowDown asset exists -- every other direction in ElvUI is
-- ArrowUp rotated, so this matches that convention.
region:SetTexture(E.Media.Textures.ArrowUp)
region:SetVertexColor(1, 1, 1)
region:SetTexCoord(0, 1, 0, 1)
region:SetRotation(S.ArrowRotation.down)
region:SetSize(14, 14)
end
end
local menu = _G[FRAME_NAME.."DropdownMenu"]
if menu and not menu.CoASkinned then
menu.CoASkinned = true
-- Panel only, matching how the talent frame's own popup menus are
-- treated -- the option rows inside are a later pass.
menu:StripTextures()
menu:SetTemplate("Transparent")
end
end
-- Neither pager button is named (both are anonymous children of
-- CollectionList), so direction is worked out from their own anchor offset
-- rather than a name match, and re-checked on every pass instead of cached --
-- cheap, and self-corrects if the two ever come back in a different order.
local function SkinPagerArrows()
local list = _G[FRAME_NAME.."CollectionList"]
if not list then return end
local a, b = select(3, list:GetChildren()), select(4, list:GetChildren())
if not (a and b and a:GetObjectType() == "Button" and b:GetObjectType() == "Button") then return end
local _, _, _, ax = a:GetPoint()
local _, _, _, bx = b:GetPoint()
local prevButton, nextButton = a, b
if bx and ax and bx < ax then
prevButton, nextButton = b, a
end
S:HandleNextPrevButton(prevButton, "left")
S:HandleNextPrevButton(nextButton, "right")
end
local function SkinFrame(frame)
if not frame.CoASkinned then
frame.CoASkinned = true
frame:SetTemplate("Transparent")
frame:HookScript("OnShow", function(self)
SkinCloseButton(self)
SkinActionButtons()
SkinSearchBox()
SkinDropdown()
SkinPagerArrows()
end)
end
SkinCloseButton(frame)
SkinActionButtons()
SkinSearchBox()
SkinDropdown()
SkinPagerArrows()
end
local function TryHook()
local frame = _G[FRAME_NAME]
if not frame then return false end
SkinFrame(frame)
return true
end
function CoA:InitializeVanityFrame()
if not E.private.skins.blizzard.enable then return end
if TryHook() then return end
-- Frame is created on-demand by its owning addon, the instant the player
-- first opens it -- a poll can't catch that before native art gets a
-- paint. ADDON_LOADED fires (synchronously, before control returns to
-- whatever code calls :Show()) the moment that addon finishes loading, so
-- skinning here lands before the first-ever :Show(). Confirmed in-game on
-- the talent frame's identical pattern; see TalentFrame.lua.
local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")
loader:SetScript("OnEvent", function(self)
if TryHook() then
self:UnregisterEvent("ADDON_LOADED")
end
end)
end