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
+34 -189
View File
@@ -1,22 +1,10 @@
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 = "AppearanceWardrobeFrame"
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, same as Vanity's action buttons --
-- S:HandleButton's own texture clearing handles these directly.
local function SkinActionButtons()
@@ -29,68 +17,12 @@ local function SkinSearchBox()
S:HandleEditBox(_G[FRAME_NAME.."CollectionSearchBox"])
end
-- Filter and Order By share Vanity's exact dropdown art -- same nine
-- anonymous "Silver-Button" slices, confirmed by probe, that S:HandleButton
-- can't reach on its own, plus the same native mouse-down re-art problem on
-- click (SetTexture noop'd per region after clearing, same trick).
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
-- suffix is "Filter" or "Sorting" -- both are CollectionX buttons with a
-- matching CollectionXMenu popout (confirmed by probe), same shape as
-- Vanity's single dropdown.
-- matching CollectionXMenu popout (confirmed by probe), and both are the same
-- widget as the vanity frame's single dropdown, so both go through the shared
-- handler.
local function SkinCollectionDropdown(suffix)
local dropdown = _G[FRAME_NAME.."Collection"..suffix]
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.."Collection"..suffix.."Menu"]
if menu and not menu.CoASkinned then
menu.CoASkinned = true
-- Panel only, matching how Vanity's own popup menu is treated -- the
-- option rows inside are a later pass.
menu:StripTextures()
menu:SetTemplate("Transparent")
end
Skin:Dropdown(_G[FRAME_NAME.."Collection"..suffix], _G[FRAME_NAME.."Collection"..suffix.."Menu"])
end
local function SkinPagerArrows()
@@ -101,57 +33,12 @@ local function SkinPagerArrows()
if nextButton then S:HandleNextPrevButton(nextButton, "right") end
end
-- Same Left/Center/Right art naming as the talent frame's tabs (confirmed by
-- probe), but these are proper descendants of Collection rather than
-- separately-placed siblings, so the frame-level bump Talent needs to clear
-- its own frame's regions doesn't apply here -- normal parent/child z-order
-- already puts them on top.
local TAB_TEXTURES = {"Left", "Center", "Right"}
local function StripTab(tab)
local name = tab:GetName()
if name then
for _, suffix in ipairs(TAB_TEXTURES) do
local tex = _G[name..suffix]
if tex then tex:SetTexture(nil) end
end
end
local highlight = tab.GetHighlightTexture and tab:GetHighlightTexture()
if highlight then highlight:SetTexture(nil) end
local checked = tab.GetCheckedTexture and tab:GetCheckedTexture()
if checked then checked:SetTexture(nil) end
end
-- Only the tab being switched to gets any kind of hook fired -- the ones
-- switched away from never fire OnClick/OnShow/SetChecked again, but their
-- native art still comes back, so there's no event to catch it from. Same
-- fix as the talent frame's tabs: a cheap OnUpdate texture check, only pays
-- for the full strip when the native art has actually reappeared.
local function UpdateTabArt(tab)
local name = tab:GetName()
local tex = name and _G[name.."Left"]
if tex and tex:GetTexture() then
StripTab(tab)
end
end
local function SkinTab(tab)
if not tab.CoASkinned then
tab.CoASkinned = true
tab:CreateBackdrop("Default")
tab.backdrop:Point("TOPLEFT", 2, -2)
tab.backdrop:Point("BOTTOMRIGHT", -2, 2)
tab:HookScript("OnUpdate", UpdateTabArt)
end
StripTab(tab)
end
-- Same art naming as the talent frame's tabs (confirmed by probe), so they go
-- through the same shared handler. No level parent is passed: these are proper
-- descendants of Collection rather than separately-placed siblings, so normal
-- parent/child z-order already puts them above the panel and the frame-level
-- bump the talent tabs need doesn't apply.
--
-- Named PoolFrameAppearanceTypeTabTemplate1 through 8 (confirmed by probe),
-- not pooled/created dynamically like the talent frame's spec choices, so a
-- plain indexed loop is enough -- no OnShow hook needed to catch late pool
@@ -160,11 +47,21 @@ local TAB_COUNT = 8
local function SkinCategoryTabs()
for i = 1, TAB_COUNT do
local tab = _G[FRAME_NAME.."CollectionPoolFrameAppearanceTypeTabTemplate"..i]
if tab then SkinTab(tab) end
Skin:Tab(_G[FRAME_NAME.."CollectionPoolFrameAppearanceTypeTabTemplate"..i])
end
end
local function SkinContents()
Skin:Title(_G[FRAME_NAME.."TitleText"])
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
SkinActionButtons()
SkinSearchBox()
SkinCollectionDropdown("Filter")
SkinCollectionDropdown("Sorting")
SkinPagerArrows()
SkinCategoryTabs()
end
local function SkinFrame(frame)
if not frame.CoASkinned then
frame.CoASkinned = true
@@ -172,42 +69,26 @@ local function SkinFrame(frame)
-- The native NineSlice panel draws its own ornate border/background on
-- top of the Transparent template's border, doubling up. Same fix as
-- the talent frame: strip and hide it.
local nineSlice = _G[FRAME_NAME.."NineSlice"]
if nineSlice then
nineSlice:StripTextures()
nineSlice:Hide()
end
Skin:HideArt(_G[FRAME_NAME.."NineSlice"])
-- The round medallion overhangs the top-left corner (confirmed by
-- probe: 61x61, TOPLEFT -6,8 -- matches the frame's own corner, not a
-- counter badge elsewhere, unlike Vanity's same-named "Portrait2"
-- texture). No flat equivalent, so it goes rather than getting
-- reskinned, same treatment as the talent frame's portrait.
local portrait = _G[FRAME_NAME.."PortraitFrame"]
if portrait then
portrait:StripTextures()
portrait:Hide()
end
Skin:HideArt(_G[FRAME_NAME.."PortraitFrame"])
-- The frame's own regions (confirmed by probe: 4 total, all BACKGROUND/
-- BORDER/OVERLAY textures, none of them functional -- unlike Vanity's
-- frame, nothing here doubles as a counter badge) are the wood-panel
-- background art. Strip it first, THEN apply the template -- SetTemplate
-- adds its own backdrop as real texture regions on this same frame, so
-- stripping afterward wiped the backdrop right back off (outer panel
-- came out fully invisible).
frame:StripTextures()
frame:SetTemplate("Transparent")
-- background art, so this one is safe to strip before templating.
Skin:Panel(frame)
-- InsetOverlay's NineSlice and the ShadowOverlay are separate decorative
-- art layered over the item grid area (8-piece atlas borders/shadow
-- edges, confirmed via probe), stripped outright -- no functional
-- content lives on either.
local insetOverlayNineSlice = _G[FRAME_NAME.."CollectionInsetOverlayNineSlice"]
if insetOverlayNineSlice then
insetOverlayNineSlice:StripTextures()
insetOverlayNineSlice:Hide()
end
Skin:HideArt(_G[FRAME_NAME.."CollectionInsetOverlayNineSlice"])
-- The actual panel behind the grid: Collection itself (confirmed via
-- /fstack -- InsetOverlay was the wrong target, its own rect doesn't
@@ -226,11 +107,7 @@ local function SkinFrame(frame)
-- same order as the outer frame -- SetTemplate's own WHITE8X8 backdrop
-- pieces land as regions on this same frame too, so stripping after
-- would wipe them right back off.
local collection = _G[FRAME_NAME.."Collection"]
if collection then
collection:StripTextures()
collection:SetTemplate("Transparent")
end
Skin:Panel(_G[FRAME_NAME.."Collection"])
-- The 3D model preview's vanilla border is an anonymous child (first
-- of PlayerModel's own, confirmed by probe: 8-piece "UIFrame" atlas
@@ -241,34 +118,15 @@ local function SkinFrame(frame)
local playerModel = _G[FRAME_NAME.."PlayerModel"]
local modelBorder = playerModel and select(1, playerModel:GetChildren())
if modelBorder and modelBorder:GetObjectType() == "Frame" then
modelBorder:StripTextures()
modelBorder:Hide()
Skin:HideArt(modelBorder)
end
local shadowOverlay = _G[FRAME_NAME.."CollectionShadowOverlay"]
if shadowOverlay then
shadowOverlay:StripTextures()
shadowOverlay:Hide()
end
Skin:HideArt(_G[FRAME_NAME.."CollectionShadowOverlay"])
frame:HookScript("OnShow", function(self)
SkinCloseButton(self)
SkinActionButtons()
SkinSearchBox()
SkinCollectionDropdown("Filter")
SkinCollectionDropdown("Sorting")
SkinPagerArrows()
SkinCategoryTabs()
end)
frame:HookScript("OnShow", SkinContents)
end
SkinCloseButton(frame)
SkinActionButtons()
SkinSearchBox()
SkinCollectionDropdown("Filter")
SkinCollectionDropdown("Sorting")
SkinPagerArrows()
SkinCategoryTabs()
SkinContents()
end
local function TryHook()
@@ -282,19 +140,6 @@ end
function CoA:InitializeWardrobeFrame()
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