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