Share one skinning path across the three CoA frames

The talent, vanity and wardrobe frames are built from the same handful of
widget templates, but each module carried its own copy of the handling:
close buttons, panels, dropdown pills, tabs and the ADDON_LOADED loader
were all written out three times. The copies had drifted -- tab backdrop
insets of 3 against 2, tabs grown in one frame and not the other, the
wardrobe clearing only three of the six tab textures -- and that drift is
what reads in-game as the same control looking different depending on which
tab you're on.

Modules/Skinning.lua now owns each of those behaviours once, and the three
modules call into it. Where the copies disagreed the values are unified;
tab growth is gated on the tab actually having a label, since growing an
icon-only tab just pushes its icon off the layout, and the SetChecked hook
is guarded because hooksecurefunc errors outright on a missing method.

Titles are pinned to Arial Narrow at 13: the vanity store titled in Friz
Quadrata while the other two used Arial Narrow. Frame scale is left as the
server set it, so the vanity title still renders slightly larger than the
others.

Two things deliberately not shared: the vanity frame still skips
StripTextures, since its currency counters are regions of the frame itself,
and the talent frame's bottom-bar dropdowns stay on their own path -- they
are a different widget, a native dropdown plus a separate caret button.
This commit is contained in:
2026-08-17 22:19:50 +02:00
parent 083cbcbf9c
commit 766e561839
5 changed files with 406 additions and 508 deletions
+23 -108
View File
@@ -1,6 +1,7 @@
local E, L, V, P, G = unpack(ElvUI)
local S = E:GetModule("Skins")
local CoA = E:GetModule("CoA")
local Skin = CoA.Skin
local FRAME_NAME = "StoreCollectionFrame"
@@ -12,19 +13,6 @@ local FRAME_NAME = "StoreCollectionFrame"
-- 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.
@@ -37,72 +25,10 @@ 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
-- The filter pill and its popout are the shared dropdown widget; see
-- Skinning.lua for why S:HandleButton alone can't clear it.
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
Skin:Dropdown(_G[FRAME_NAME.."Dropdown"], _G[FRAME_NAME.."DropdownMenu"])
end
-- Neither pager button is named (both are anonymous children of
@@ -128,28 +54,30 @@ local function SkinPagerArrows()
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)
local function SkinContents()
Skin:Title(_G[FRAME_NAME.."TitleText"])
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
SkinActionButtons()
SkinSearchBox()
SkinDropdown()
SkinPagerArrows()
end
local function SkinFrame(frame)
if not frame.CoASkinned then
frame.CoASkinned = true
-- Templated without stripping, unlike the talent and wardrobe frames:
-- the currency counters are drawn as regions of the frame itself (see
-- the note at the top of this file), so a strip blanks them.
Skin:Panel(frame, true)
frame:HookScript("OnShow", SkinContents)
end
SkinContents()
end
local function TryHook()
local frame = _G[FRAME_NAME]
if not frame then return false end
@@ -161,19 +89,6 @@ 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)
Skin:OnFrameAvailable(TryHook)
end