From 766e56183914033dee35c202f797a3893d426bf5 Mon Sep 17 00:00:00 2001 From: Narcasung Date: Mon, 17 Aug 2026 22:19:50 +0200 Subject: [PATCH] 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. --- ElvUI_CoA.toc | 1 + Modules/Skinning.lua | 330 ++++++++++++++++++++++++++++++++++++++ Modules/TalentFrame.lua | 229 +++----------------------- Modules/VanityFrame.lua | 131 +++------------ Modules/WardrobeFrame.lua | 223 ++++---------------------- 5 files changed, 406 insertions(+), 508 deletions(-) create mode 100644 Modules/Skinning.lua diff --git a/ElvUI_CoA.toc b/ElvUI_CoA.toc index 5a3f7c0..6855703 100644 --- a/ElvUI_CoA.toc +++ b/ElvUI_CoA.toc @@ -14,6 +14,7 @@ Modules\ExtraActionBar.lua Modules\LayerPicker.lua Modules\DispelHighlight.lua Modules\ClassResources.lua +Modules\Skinning.lua Modules\TalentFrame.lua Modules\VanityFrame.lua Modules\WardrobeFrame.lua diff --git a/Modules/Skinning.lua b/Modules/Skinning.lua new file mode 100644 index 0000000..4642ec3 --- /dev/null +++ b/Modules/Skinning.lua @@ -0,0 +1,330 @@ +local E, L, V, P, G = unpack(ElvUI) +local S = E:GetModule("Skins") +local CoA = E:GetModule("CoA") + +-- Shared skinning helpers for the CoA frames (talent, vanity, wardrobe). +-- +-- Those three are built from the same handful of widget templates, so every +-- element that appears in more than one of them lives here rather than being +-- copied per module. The copies had already drifted -- different backdrop +-- insets, different close button sizes, tabs grown in one frame and not the +-- other -- and the drift is exactly what reads as "the same control looks +-- different depending on which tab I'm on". +local Skin = {} +CoA.Skin = Skin + +-- Every native close button on these frames is a different size (the vanity +-- store's is visibly larger than the talent frame's), and HandleCloseButton +-- keeps whatever size it's given -- it only centres a fixed 12px X inside the +-- box. Normalised here so the X lands on the same grid in all three, and the +-- click target is the same everywhere too. +local CLOSE_BUTTON_SIZE = 32 + +function Skin:CloseButton(close) + if not close then return end + + -- Strictly once: HandleCloseButton strips the button on every call, which + -- blanks the X texture it added on the first pass, and its own guard won't + -- rebuild it because the field still points at that blanked texture. + if not close.CoASkinned then + close.CoASkinned = true + + S:HandleCloseButton(close) + end + + close:Size(CLOSE_BUTTON_SIZE) +end + +-- The talent and wardrobe frames title in Arial Narrow, the vanity store in +-- Friz Quadrata (measured live). Arial Narrow is the one that matches the rest +-- of the layout, so it's pinned here rather than left to the frames -- and +-- pinned by name through LSM rather than as a raw path, so it stays the same +-- asset ElvUI itself would resolve. +-- +-- Size is written straight through, with no correction for the frames running +-- at different scales: the vanity store's is about 9% larger than the other +-- two (measured), so its title renders slightly bigger than theirs. Left as the +-- server has it for now. +-- +-- FontTemplate stashes the font and size it was handed on the fontstring, so an +-- ElvUI font change later re-applies these rather than resetting them. +local TITLE_FONT = "Arial Narrow" +local TITLE_SIZE = 13 + +function Skin:Title(title) + if not title then return end + + title:FontTemplate(E.Libs.LSM:Fetch("font", TITLE_FONT), TITLE_SIZE, "OUTLINE") +end + +-- Strip first, THEN template: SetTemplate adds its own backdrop as texture +-- regions on this same frame, so stripping afterwards wipes it straight back +-- off -- which is how the wardrobe's outer panel once came out fully invisible. +-- +-- keepTextures is for frames whose own regions aren't all decorative: the +-- vanity store draws its currency counters as regions of the frame itself, and +-- a blind strip blanks the counters along with the panel art. +function Skin:Panel(frame, keepTextures) + if not frame or frame.CoAPanelSkinned then return end + frame.CoAPanelSkinned = true + + if not keepTextures then + frame:StripTextures() + end + + frame:SetTemplate("Transparent") +end + +-- Ornate art with no flat equivalent (portrait medallions, nine-slice borders, +-- shadow overlays): it goes rather than getting reskinned. Hidden as well as +-- stripped, since some of these put their own art back. +function Skin:HideArt(frame) + if not frame then return end + + frame:StripTextures() + frame:Hide() +end + +-- The dropdown pills on the vanity and wardrobe frames are the same widget: +-- nine anonymous "Silver-Button" slices rather than the named Left/Middle/Right +-- fields or the Normal/Pushed/Disabled set S:HandleButton knows how to clear, +-- so its own clearing can't reach them -- and a blind StripTextures would take +-- the caret and the label with them. +-- +-- The native mouse-down handler re-arts one of these regions with 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. +local DROPDOWN_ART = "Silver%-Button" +local CARET_ART = "ChatFrameExpandArrow" +local CARET_SIZE = 14 + +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 and tostring(texture):find(DROPDOWN_ART) then + region:SetTexture(nil) + region.SetTexture = E.noop + end + end +end + +-- The caret is retextured after HandleButton, not before: HandleButton strips +-- the pill, and a caret replaced ahead of that gets cleared straight back off. +local function SkinDropdownCaret(dropdown) + 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(CARET_ART) then + -- The caret is a plain OVERLAY texture on the dropdown itself rather + -- than a separate button, so it can't go through + -- HandleNextPrevButton and is retextured directly. No ArrowDown + -- asset exists -- every other direction in ElvUI is ArrowUp rotated. + region:SetTexture(E.Media.Textures.ArrowUp) + region:SetVertexColor(1, 1, 1) + region:SetTexCoord(0, 1, 0, 1) + region:SetRotation(S.ArrowRotation.down) + region:SetSize(CARET_SIZE, CARET_SIZE) + end + end +end + +-- menu is the popout list this dropdown owns; it gets the panel only, matching +-- how the talent frame's own popup menus are treated. The option rows inside +-- are a later pass. +function Skin:Dropdown(dropdown, menu) + if not dropdown or dropdown.CoASkinned then return end + dropdown.CoASkinned = true + + StripDropdownArt(dropdown) + S:HandleButton(dropdown) + SkinDropdownCaret(dropdown) + + self:Panel(menu) +end + +-- Tabs ----------------------------------------------------------------------- +-- +-- S:HandleTab can't be used on any of these: it clears the tab body by name, +-- looking for a "Middle" piece, and every CoA tab names theirs "Center", so the +-- body survives and the ElvUI backdrop just lands behind the old art. +-- +-- One list covers both tab flavours: the talent frame's tabs carry the +-- Disabled variants as well, the wardrobe's category tabs don't, and the +-- lookups for the ones that don't exist simply come back nil. +local TAB_TEXTURES = {"Left", "Center", "Right", "LeftDisabled", "CenterDisabled", "RightDisabled"} + +-- Blizzard's own tab code re-sets these textures both when a frame reopens and, +-- separately, on every tab switch -- and a switch never fires the owning +-- frame's OnShow, only SetChecked. So this has to be idempotent and re-run from +-- everywhere rather than skinned once behind a guard. +function Skin:StripTabArt(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 + +local TAB_BACKDROP_INSET = 3 + +-- Native tab sizing is a retail leftover -- every other ElvUI tab row in this +-- client reads bigger -- so labelled tabs are grown, off the font's own current +-- size rather than a hardcoded number, and stay in step with the user's font +-- settings. Icon-only tabs (the wardrobe's category strip) have no label to +-- grow around and keep their native size; growing them would only push the +-- icons off their own layout. +local TAB_GROWTH = 8 +local TAB_FONT_GROWTH = 3 +local TAB_LABEL_OFFSET = 12 + +-- Grown height self-heals: on a plain /reload a tab's native height isn't +-- settled yet at skin time (Blizzard lays it out asynchronously), so a one-shot +-- SetHeight caught a stale value and left a shorter tab until something let the +-- native layout finish. +-- +-- Compared by equality rather than "did it shrink": the late layout can land on +-- a height larger than the stale one this grew from, and a shrink-only check +-- would then see current > target and never regrow. GetHeight doesn't read back +-- byte-exact after SetHeight either -- UI scale rounds it to a slightly +-- different float -- so a strict ~= compare never held and this grew every tick +-- without bound. Half a pixel of slack absorbs the rounding while still +-- catching a genuine Blizzard-driven change, which is always a full tab's worth +-- of height. +local TAB_HEIGHT_EPSILON = 0.5 + +local function UpdateTabSize(tab) + if not tab.CoAGrowTab then return end + + local height = tab:GetHeight() + + if not tab.CoAGrownHeight or math.abs(height - tab.CoAGrownHeight) > TAB_HEIGHT_EPSILON then + tab.CoAGrownHeight = height + TAB_GROWTH + tab:SetHeight(tab.CoAGrownHeight) + end +end + +-- Tabs that aren't children of the frame they belong to (the talent frame's +-- are separately-placed siblings) sit behind its panel art once grown, so they +-- get bumped above it. Re-applied every tick rather than on show or on select: +-- bumping from those two alone still left them behind the panel, so whatever +-- resets their level isn't either of those events. +local function BumpTabLevel(tab) + local parent = tab.CoALevelParent + if not parent then return end + + tab:SetFrameStrata(parent:GetFrameStrata()) + tab:SetFrameLevel(parent:GetFrameLevel() + 20) +end + +-- Only the tab being switched to fires anything at all -- the ones switched +-- away from never fire OnClick/OnShow/SetChecked again, yet their native art +-- still comes back, so there's no event to catch it from. Checked from OnUpdate +-- instead: a cheap GetTexture() compare that only pays for the full strip when +-- the art has actually reappeared. +local function UpdateTabArt(tab) + BumpTabLevel(tab) + UpdateTabSize(tab) + + local name = tab:GetName() + local tex = name and _G[name.."Left"] + + if tex and tex:GetTexture() then + Skin:StripTabArt(tab) + end +end + +-- No fill or border swap marks the open tab: the native tab code already turns +-- the label white on the checked one and leaves the rest their normal colour, +-- same as the Friends/Character tab rows. Only the art strip has to re-run. +local function OnTabChecked(tab) + Skin:StripTabArt(tab) +end + +-- levelParent is the frame the tab must draw above, and is only needed for tabs +-- that aren't its children (see BumpTabLevel). Pass nil for tabs parented to +-- the frame they belong to -- normal parent/child z-order already covers those. +function Skin:Tab(tab, levelParent) + if not tab then return end + + if not tab.CoASkinned then + tab.CoASkinned = true + tab.CoALevelParent = levelParent + + self:StripTabArt(tab) + + -- Default rather than Transparent: tabs sit below their frame over the + -- open world, so a see-through panel reads as washed out instead of as + -- the solid tabs the retail layout has. + -- + -- The backdrop's own level is left alone. CreateBackdrop keeps it level + -- with the tab, and regions render fine on top of a same-level child; + -- forcing it a level above the tab reproduces the + -- child-frame-covers-its-parent's-own-regions quirk (see StripRowArt in + -- TalentFrame), which blanks the label. + tab:CreateBackdrop("Default") + tab.backdrop:Point("TOPLEFT", TAB_BACKDROP_INSET, -TAB_BACKDROP_INSET) + tab.backdrop:Point("BOTTOMRIGHT", -TAB_BACKDROP_INSET, TAB_BACKDROP_INSET) + tab:SetHitRectInsets(TAB_BACKDROP_INSET, TAB_BACKDROP_INSET, TAB_BACKDROP_INSET, TAB_BACKDROP_INSET) + + local fontString = tab.GetFontString and tab:GetFontString() + if fontString then + tab.CoAGrowTab = true + + local font, size, flags = fontString:GetFont() + if font then + fontString:SetFont(font, size + TAB_FONT_GROWTH, flags) + end + + -- The label's native anchor sits right off the icon, sized for the + -- smaller native font, so grown text collides with the icon. Nudged + -- off whatever point the native layout gave it rather than a + -- hardcoded anchor that would fight that layout. + local point, relTo, relPoint, x, y = fontString:GetPoint(1) + if point then + fontString:SetPoint(point, relTo, relPoint, x + TAB_LABEL_OFFSET, y) + end + end + + -- Guarded: the talent frame's tabs are CheckButtons, the wardrobe's + -- category tabs aren't guaranteed to be, and hooksecurefunc errors + -- outright on a method that doesn't exist. + if tab.SetChecked then + hooksecurefunc(tab, "SetChecked", OnTabChecked) + end + + tab:HookScript("OnUpdate", UpdateTabArt) + end + + self:StripTabArt(tab) + BumpTabLevel(tab) +end + +-- These frames are created on-demand by their owning addon, the instant the +-- player first opens one -- a poll can't catch that before the 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 from it lands before the first-ever :Show(), killing the one-frame +-- flicker a poll-based catch can't avoid. Confirmed in-game. +function Skin:OnFrameAvailable(tryHook) + if tryHook() then return end + + local loader = CreateFrame("Frame") + loader:RegisterEvent("ADDON_LOADED") + loader:SetScript("OnEvent", function(self) + if tryHook() then + self:UnregisterEvent("ADDON_LOADED") + end + end) +end diff --git a/Modules/TalentFrame.lua b/Modules/TalentFrame.lua index 3450b3e..7edbfe0 100644 --- a/Modules/TalentFrame.lua +++ b/Modules/TalentFrame.lua @@ -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 = "CoATalentFrame" local TAB_NAME = "CollectionsPoolFrameCollectionTabTemplate%d" @@ -78,14 +79,7 @@ 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, and its own guard won't - -- rebuild it because the field still points at that blanked texture. - if not close.CoASkinned then - close.CoASkinned = true - - S:HandleCloseButton(close) - end + Skin:CloseButton(close) -- Re-applied on every pass, not once at skin time. The X is drawn on the -- close button's own frame, so it needs to outrank TreeView, and TreeView @@ -533,15 +527,8 @@ local function SkinBottomBar() if menu and not menu.CoASkinned then menu.CoASkinned = true - menu:StripTextures() - menu:SetTemplate("Transparent") - - -- Once only: HandleCloseButton strips the button on every call, - -- which blanks the X it added on the first pass. - local close = _G[menuName.."Close"] - if close then - S:HandleCloseButton(close) - end + Skin:Panel(menu) + Skin:CloseButton(_G[menuName.."Close"]) -- Rows don't exist until the menu is first opened, which happens -- long after the talent frame's own OnShow, so they're picked up @@ -608,178 +595,19 @@ local function CropBackground() end end --- S:HandleTab can't be used here: it clears the tab background by name, --- looking for a "Middle" piece, and these tabs name theirs "Center", so the --- body of the tab survives and the ElvUI backdrop just lands behind the old --- art. The unnamed ARTWORK region is the tab's icon and is left alone. -local TAB_TEXTURES = {"Left", "Center", "Right", "LeftDisabled", "CenterDisabled", "RightDisabled"} - --- Blizzard's own tab code re-sets these textures both when the frame reopens --- and, separately, on every tab switch -- and a switch never fires the talent --- frame's OnShow, only SetChecked. So this has to be idempotent and re-run --- from both places rather than skinned once behind a guard. -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 - --- Same sibling problem as the close button (see SkinCloseButton): the tabs --- aren't children of the talent frame, so growing one taller lets its top --- edge poke up behind the frame's own panel art instead of in front of it. --- --- The backdrop's own level is left alone -- CreateBackdrop keeps it level --- with the tab by default, and regions render fine on top of a same-level --- child. Forcing the backdrop a level *above* the tab (tried here once) --- reproduces the child-frame-covers-parent's-own-regions quirk noted in --- StripRowArt, which is why the label vanished that time. Raising the tab is --- enough; the backdrop, clamped to the tab's new level automatically, rises --- with it. -local function BumpTabLevel(tab) - local frame = _G[FRAME_NAME] - if not frame then return end - - tab:SetFrameStrata(frame:GetFrameStrata()) - tab:SetFrameLevel(frame:GetFrameLevel() + 20) -end - --- Grown height, self-healing the same way: on a plain /reload the tab's --- native height isn't settled yet at SkinTab time (Blizzard lays it out --- asynchronously), so a one-shot SetHeight there caught a stale value and --- produced a shorter tab until something (a switch, a reopen) let Blizzard's --- own layout finish and this caught the corrected height. --- --- Compared by equality rather than "did it shrink": Blizzard's late layout --- can land on a height *larger* than the stale one this originally grew from, --- and a shrink-only check would then see current > target and never regrow, --- leaving the tab at Blizzard's native size with none of the padding added. --- Checking for any drift away from what was last written here catches that --- direction too, but GetHeight doesn't read back byte-exact after SetHeight --- -- UI scale rounds it to a slightly different float -- so a strict ~= --- compare never held and this grew every single tick without bound. Half a --- pixel of slack absorbs that rounding while still catching a genuine --- Blizzard-driven change, which is always a full tab's worth of height, not --- a rounding error's worth. -local TAB_GROWTH = 8 -local TAB_HEIGHT_EPSILON = 0.5 - -local function UpdateTabSize(tab) - local height = tab:GetHeight() - - if not tab.CoAGrownHeight or math.abs(height - tab.CoAGrownHeight) > TAB_HEIGHT_EPSILON then - tab.CoAGrownHeight = height + TAB_GROWTH - tab:SetHeight(tab.CoAGrownHeight) - end -end - --- Only the active tab gets a SetChecked call on every switch -- the other two --- never fire it again after the first skin, but their art still comes back, --- so there's no event to hook for them. Checked from OnUpdate instead, same --- as RestoreArrow/UpdateRowArt: cheap GetTexture() compare, only pays for the --- full strip when the native art has actually reappeared. --- --- The frame-level bump is re-applied here too, every frame rather than only --- on OnShow/SetChecked: the earlier fix (bumping only from those two spots) --- still left the tabs behind the panel, which means whatever resets their --- level on a switch isn't SetChecked either. OnUpdate is the one hook proven --- to survive every path that reverts these tabs, so it's the catch-all. -local function UpdateTabArt(tab) - BumpTabLevel(tab) - UpdateTabSize(tab) - - local name = tab:GetName() - local tex = name and _G[name.."Left"] - - if tex and tex:GetTexture() then - StripTab(tab) - end -end - --- These are CheckButtons, but no fill or border swap is needed to mark the --- open one -- Blizzard's own tab code already turns the label white on the --- checked tab and leaves the rest their normal colour, same as the --- Friends/Character tab rows. Only the art strip needs to re-run here. -local function UpdateTabSelection(tab) - StripTab(tab) -end - -local function SkinTab(tab) - if tab.CoASkinned then return end - tab.CoASkinned = true - - StripTab(tab) - - -- Default rather than Transparent: these sit below the frame over open - -- world, so a see-through panel reads as washed out instead of as the - -- solid tabs the retail layout has. - -- - -- Insets alone only resize the plate within the tab's native bounds -- - -- padding around the (now bigger) label needs the tab itself taller. - -- Height is grown by UpdateTabSize (from the OnUpdate hook below) rather - -- than here, since the native height isn't settled yet at this point. - tab:CreateBackdrop("Default") - tab.backdrop:Point("TOPLEFT", 3, -3) - tab.backdrop:Point("BOTTOMRIGHT", -3, 3) - tab:SetHitRectInsets(3, 3, 3, 3) - - -- Native size is a retail leftover -- every other ElvUI tab row in this - -- client reads bigger. Grown in place off the font's own current size - -- rather than a hardcoded number, so it still scales with the user's - -- font settings. - local fontString = tab.GetFontString and tab:GetFontString() - if fontString then - local font, size, flags = fontString:GetFont() - if font then - fontString:SetFont(font, size + 3, flags) - end - - -- The label's native anchor sits right off the icon, sized for the - -- smaller native font. Grown text collides with the icon at that - -- offset, so it's nudged right off whatever point Blizzard anchored - -- it to rather than a hardcoded anchor that would fight the tab's - -- own layout. - local point, relTo, relPoint, x, y = fontString:GetPoint(1) - if point then - fontString:SetPoint(point, relTo, relPoint, x + 12, y) - end - end - - hooksecurefunc(tab, "SetChecked", UpdateTabSelection) - UpdateTabSelection(tab) - - tab:HookScript("OnUpdate", UpdateTabArt) -end - --- Same sibling problem as the close button (see SkinCloseButton): the tabs --- aren't children of the talent frame, so growing one taller lets its top --- edge poke up behind the frame's own panel art instead of in front of it. --- Re-applied every pass rather than once, since whatever re-raises the frame --- doesn't carry the tabs' absolute level along with it. --- --- The backdrop's own level is left alone -- CreateBackdrop keeps it level --- with the tab by default, and regions render fine on top of a same-level --- child. Forcing the backdrop a level *above* the tab (tried here once) --- reproduces the child-frame-covers-parent's-own-regions quirk noted in --- StripRowArt, which is why the label vanished. Raising the tab is enough; --- the backdrop, clamped to the tab's new level automatically, rises with it. +-- Tabs go through the shared handler (see Skinning.lua) so they come out +-- identical to the wardrobe's category tabs. The talent frame is passed as the +-- level parent because, unlike those, these tabs aren't its children -- they're +-- separately-placed siblings, so once grown their top edge pokes up behind the +-- frame's own panel art unless they're raised above it. local function SkinTabs() + local frame = _G[FRAME_NAME] + for i = 1, MAX_TABS do local tab = _G[TAB_NAME:format(i)] if not tab then break end - SkinTab(tab) - StripTab(tab) - BumpTabLevel(tab) + Skin:Tab(tab, frame) end end @@ -807,23 +635,13 @@ local function SkinFrame(frame) if not frame.CoASkinned then frame.CoASkinned = true - frame:StripTextures() - - local nineSlice = _G[FRAME_NAME.."NineSlice"] - if nineSlice then - nineSlice:StripTextures() - nineSlice:Hide() - end + Skin:HideArt(_G[FRAME_NAME.."NineSlice"]) -- The round portrait medallion overhangs the top-left corner and has -- no flat equivalent, so it goes rather than getting reskinned. - local portrait = _G[FRAME_NAME.."PortraitFrame"] - if portrait then - portrait:StripTextures() - portrait:Hide() - end + Skin:HideArt(_G[FRAME_NAME.."PortraitFrame"]) - frame:SetTemplate("Transparent") + Skin:Panel(frame) -- The choice cards are pulled from the pool as the view opens, so the -- talent frame's own show is too early to catch them. Run on the view's @@ -840,6 +658,7 @@ local function SkinFrame(frame) -- run again on every show rather than once at hook time. The isSkinned -- / backdrop guards inside ElvUI's handlers make re-runs cheap. frame:HookScript("OnShow", function(self) + Skin:Title(_G[FRAME_NAME.."TitleText"]) SkinCloseButton(self) SkinBottomBar() CropBackground() @@ -849,6 +668,7 @@ local function SkinFrame(frame) end) end + Skin:Title(_G[FRAME_NAME.."TitleText"]) SkinCloseButton(frame) SkinBottomBar() -- Not CropBackground() here: this runs pre-Show now (see InitializeTalentFrame), @@ -873,19 +693,6 @@ end function CoA:InitializeTalentFrame() if not E.private.skins.blizzard.enable then return end - if TryHook() then return end - -- Frame is created on-demand by its owning addon (e.g. Ascension_CoATalents), - -- the instant the player first opens it -- a poll can't catch that before the - -- 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(), killing the - -- one-frame flicker a poll-based catch can't avoid. Confirmed in-game. - 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 diff --git a/Modules/VanityFrame.lua b/Modules/VanityFrame.lua index 85de314..8a1cdb3 100644 --- a/Modules/VanityFrame.lua +++ b/Modules/VanityFrame.lua @@ -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 diff --git a/Modules/WardrobeFrame.lua b/Modules/WardrobeFrame.lua index 5c91dc1..49f2a85 100644 --- a/Modules/WardrobeFrame.lua +++ b/Modules/WardrobeFrame.lua @@ -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