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" local MAX_TABS = 10 -- The talent tree is the one part of the frame we must not touch: every node -- is an icon button whose border/overlay textures encode rank and -- availability, so ElvUI's generic button handling would flatten the -- information out of them. The nodes and connectors live in pools under -- SpecTree, with the scene art in FXFrame, so those subtrees are skipped by -- name. TreeView itself is walked, because the bottom bar is inside it. local EXCLUDED = {"PoolFrame", "SpecTree", "ClassTree", "FXFrame", "ShadowOverlay"} local function IsTreeSubtree(name) if not name then return false end for _, pattern in ipairs(EXCLUDED) do if name:find(pattern) then return true end end return false end local function IsDropDown(frame, name) return name ~= nil and _G[name.."Button"] ~= nil and _G[name.."Text"] ~= nil end -- The bottom bar's widgets aren't at a fixed depth (some sit directly on the -- frame, some are nested one or two containers deep), and their names aren't -- documented anywhere, so skinning is driven by walking the frame instead of -- by a hardcoded name list. Depth is capped so a container we didn't expect -- can't drag us down into the tree's pooled widgets. local MAX_DEPTH = 5 local SkinChildren function SkinChildren(frame, depth) if depth > MAX_DEPTH then return end for i = 1, frame:GetNumChildren() do local child = select(i, frame:GetChildren()) local name = child.GetName and child:GetName() if child.GetObjectType and not IsTreeSubtree(name) then local objType = child:GetObjectType() if objType == "EditBox" then S:HandleEditBox(child) elseif objType == "Button" then -- Close buttons are handled explicitly -- the frame's in -- SkinFrame, where it needs an anchor and a frame level the walk -- can't supply, and each menu's in SkinBottomBar. Matched on -- "Close" rather than "CloseButton": the menus name theirs just -- "...MenuClose", and it was coming out of the walk as an -- ordinary templated square with the X stripped off it. if not (name and name:find("Close")) then S:HandleButton(child) end elseif objType == "Frame" then if IsDropDown(child, name) then S:HandleDropDownBox(child) else SkinChildren(child, depth + 1) end end end end end -- TreeView is a sibling drawn above the close button's own frame level, so -- without the bump the ElvUI close texture ends up behind the tree panel and -- the corner just looks empty. Anchored explicitly because the native corner -- position was set relative to the NineSlice art we hide. local function SkinCloseButton(frame) local close = _G[FRAME_NAME.."CloseButton"] if not close then return 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 -- rides along whenever the talent frame is raised while the close button, -- being a sibling, keeps whatever absolute level it was given -- which is -- how the X ends up buried behind the tree panel with the button still -- clickable. close:SetFrameStrata(frame:GetFrameStrata()) close:SetFrameLevel(frame:GetFrameLevel() + 20) -- HandleCloseButton centres a 12px X inside the button's native 32px box, -- so anchoring the box to the frame corner drops the X well below the -- title. Centring the button on the title's own vertical midpoint puts the -- X on the title line whatever height the bar turns out to be. local title = _G[FRAME_NAME.."TitleText"] local top = frame:GetTop() local titleY = title and select(2, title:GetCenter()) close:ClearAllPoints() if top and titleY then close:Point("CENTER", frame, "TOPRIGHT", -16, titleY - top) else close:Point("TOPRIGHT", frame, "TOPRIGHT", -4, -4) end end local BOTTOM_BAR = FRAME_NAME.."TreeViewBottomBar" local BOTTOM_BAR_DROPDOWNS = {"SpecDropDown", "BuildDropDown"} -- The two menus don't name their list alike: the spec menu's inset and scroll -- frame hang off "List", the build creator's off "BuildList". Assuming they -- shared the layout is why nothing on the build creator's scrollbar was ever -- being found. local BOTTOM_BAR_MENUS = { {menu = "SpecializationMenu", list = "List"}, {menu = "BuildCreatorMenu", list = "BuildList"} } -- Rows are pooled, so this walks by index until it runs out rather than -- tracking how many the list currently holds. local MENU_ROW_LIMIT = 50 -- Each row's icon sits in a rounded empty-slot ring, which reads as a raised -- bevel against flat panels. The ring goes and the icon gets the usual ElvUI -- treatment: cropped edges and a backdrop sized to it. local function SkinMenuRowIcon(iconFrame) if not iconFrame or iconFrame.CoASkinned then return end iconFrame.CoASkinned = true for i = 1, iconFrame:GetNumRegions() do local region = select(i, iconFrame:GetRegions()) local texture = region.GetTexture and region:GetTexture() if texture and texture:find("EmptySlot") then region:SetTexture(nil) end end local name = iconFrame:GetName() local icon = name and _G[name..".Icon"] if icon then S:HandleIcon(icon, iconFrame) end end local ROW_BORDER_EDGES = {"TOP", "BOTTOM", "LEFT", "RIGHT"} -- The active row is edged rather than filled, and the edge is four textures on -- the row for the same reason the fill is a texture: a backdrop frame is a -- child, so it either covers the row's art or is covered by the neighbouring -- rows, which overlap far enough that only its bottom line came through. local function CreateRowBorder(row) local border = {} for _, edge in ipairs(ROW_BORDER_EDGES) do local line = row:CreateTexture(nil, "OVERLAY") line:Hide() border[edge] = line end border.TOP:SetPoint("TOPLEFT", row, "TOPLEFT") border.TOP:SetPoint("TOPRIGHT", row, "TOPRIGHT") border.TOP:SetHeight(E.mult) border.BOTTOM:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT") border.BOTTOM:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT") border.BOTTOM:SetHeight(E.mult) border.LEFT:SetPoint("TOPLEFT", row, "TOPLEFT") border.LEFT:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT") border.LEFT:SetWidth(E.mult) border.RIGHT:SetPoint("TOPRIGHT", row, "TOPRIGHT") border.RIGHT:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT") border.RIGHT:SetWidth(E.mult) row.CoABorder = border end -- Selection is read off the native overlay rather than tracked here: the row's -- own code decides which entry is active and there's no event for it, so the -- overlay is left in place as the signal and only its art is cleared. local function UpdateRowSelection(row) local border = row.CoABorder if not border then return end local overlay = row.CoASelected -- Alpha as well as visibility, because it isn't known which of the two the -- row uses to turn the overlay on. local selected = overlay ~= nil and overlay:IsShown() and overlay:GetAlpha() > 0 if selected == row.CoASelectionShown then return end row.CoASelectionShown = selected local r, g, b = unpack(E.media.rgbvaluecolor) for _, edge in ipairs(ROW_BORDER_EDGES) do local line = border[edge] line:SetTexture(r, g, b, 1) if selected then line:Show() else line:Hide() end end end -- The row's frame is a rounded plate from the PvP queue art, with a matching -- background behind it. Both come off in favour of a flat ElvUI panel, and the -- hover and selection plates have to go with them -- they're cut to the same -- rounded shape, so they read as bevels once the plate underneath is flat. -- Hover becomes the flat white wash ElvUI puts on list rows; selection moves -- onto the 1px edge from CreateRowBorder, which is how the active entry is -- marked everywhere else in ElvUI. -- -- Hidden as well as cleared, and re-run from both the row's show and its -- update: a single pass didn't hold, the rows put their art back as they're -- refilled from the pool. local function StripRowArt(row) local name = row:GetName() local border = name and _G[name..".Border"] if border then border:SetTexture(nil) border:Hide() end -- ".H" is the row's highlight texture, so retexturing it is enough to -- replace the hover state; there's no separate object to chase. local hover = name and _G[name..".H"] if hover and hover.SetTexture then hover:SetTexture(1, 1, 1, 0.15) hover:ClearAllPoints() hover:SetInside(row) row.CoAHover = hover end local selected = name and _G[name..".Selected"] if selected and selected.SetTexture then selected:SetTexture(nil) row.CoASelected = selected end for i = 1, row:GetNumRegions() do local region = select(i, row:GetRegions()) local texture = region.GetTexture and region:GetTexture() if texture and texture:find("GuildFrame") then region:SetTexture(nil) region:Hide() end end end -- Scrolling refills the pooled rows and puts their native hover and selection -- art back, and a pooled row doesn't fire OnShow when it's refilled, so there's -- no event to strip on -- which is why the glow only ever appeared after a -- scroll. The art is checked from the row's update instead. A colour-set -- texture reads back as "SolidTexture", so the check stays a string compare -- rather than a blind re-skin every frame. local function UpdateRowArt(row) local hover, selected = row.CoAHover, row.CoASelected if (hover and hover:GetTexture() ~= "SolidTexture") or (selected and selected:GetTexture() ~= nil) then StripRowArt(row) end end local function UpdateRow(row) UpdateRowArt(row) UpdateRowSelection(row) end local SkinMenuRow function SkinMenuRow(row) if not row then return end if not row.CoASkinned then row.CoASkinned = true -- The fill is painted on the row itself rather than through -- CreateBackdrop. The backdrop is a child frame, and here it covered -- the row's name, icon and status text at every frame level tried, -- including one below the row's own. A texture on the row's BACKGROUND -- layer is ordered against those regions inside a single frame instead, -- so it can't outrank them. Opaque rather than the transparent template, -- since the menu panel behind is already see-through and a second -- see-through layer left the rows reading as holes onto the talent scene. local background = row:CreateTexture(nil, "BACKGROUND") background:SetInside(row) background:SetTexture(unpack(E.media.backdropcolor)) CreateRowBorder(row) -- Nooped so a refill can't swap the highlight for a fresh texture -- object, which would leave the retextured one orphaned and the check in -- UpdateRowArt reading a state nothing draws from any more. row.SetHighlightTexture = E.noop row:HookScript("OnShow", SkinMenuRow) -- No event fires when the active spec changes or when a scroll refills -- the row, so both are resynced from the row's update. The comparisons -- in UpdateRowArt and UpdateRowSelection make every tick that changes -- nothing a no-op. row:HookScript("OnUpdate", UpdateRow) end StripRowArt(row) UpdateRowSelection(row) end -- These children are named with a literal dot ("Button1.SpecIcon"), so they -- only come out of _G by string key, never as plain identifiers. local function SkinMenuRows(listName) for i = 1, MENU_ROW_LIMIT do local rowName = listName.."ScrollFrameButton"..i local row = _G[rowName] if not row then break end SkinMenuRow(row) SkinMenuRowIcon(_G[rowName..".SpecIcon"]) end end -- S:HandleScrollBar can't be used on these: it assumes the thumb is a texture -- and calls SetTexture on it, but this thumb is a button, so the call errors -- and takes the rest of the OnShow skinning down with it. -- -- It's a proportional scrollbar: the thumb is drawn as three slices that the -- widget re-applies whenever it resizes, so stripping them doesn't hold. The -- slices are made transparent instead and a value-coloured panel is stretched -- from the first to the last, which is how ElvUI handles the same widget in -- HandleProportionalScroll. Slices are looked up as fields and as globals, -- since only the global names are confirmed here. local function SkinScrollThumb(thumb, thumbName) if not thumb or thumb.backdrop then return end local first = thumb.Begin or _G[thumbName.."Begin"] local last = thumb.End or _G[thumbName.."End"] local middle = thumb.Middle or _G[thumbName.."Middle"] if first then first:SetAlpha(0) end if last then last:SetAlpha(0) end if middle then middle:SetAlpha(0) end local r, g, b = unpack(E.media.rgbvaluecolor) thumb:CreateBackdrop("Transparent") thumb.backdrop:SetFrameLevel(thumb:GetFrameLevel() + 1) thumb.backdrop:SetBackdropColor(r, g, b, 0.25) if first and last then thumb.backdrop:Point("TOPLEFT", first) thumb.backdrop:Point("BOTTOMRIGHT", last) end thumb:HookScript("OnEnter", function(self) if self.backdrop then self.backdrop:SetBackdropColor(r, g, b, 0.75) end end) thumb:HookScript("OnLeave", function(self) if self.backdrop then self.backdrop:SetBackdropColor(r, g, b, 0.25) end end) end -- The scroll arrows come back as Blizzard chevrons after the list refreshes, -- and it isn't the ElvUI arrow being overwritten -- that texture is still in -- place underneath. The button's native art is a region StripTextures hid, and -- the scroll frame shows it again whenever it recalculates. -- -- Matched by file rather than as "any region that isn't one of ours": on this -- client ElvUI's own panel and border pieces are regions of the button too, so -- hiding everything unrecognised would take the ElvUI square with it. -- -- Compared case-insensitively: the client hands paths back from GetTexture in -- whatever case it stored them, not the case they were set in. local ARROW_TEXTURE = E.Media.Textures.ArrowUp:lower() local NATIVE_SCROLL_ART = "scrollbar" local function RestoreArrow(button) for i = 1, button:GetNumRegions() do local region = select(i, button:GetRegions()) local texture = region.GetTexture and region:GetTexture() if texture and texture:lower():find(NATIVE_SCROLL_ART) and region:IsShown() then region:Hide() end end -- The arrow itself is re-pointed as well, in case a refresh reaches the -- state textures and not only the art it re-shows. for _, texture in ipairs(button.CoAArrowTextures) do local current = texture:GetTexture() if not current or current:lower() ~= ARROW_TEXTURE then texture:SetTexture(E.Media.Textures.ArrowUp) texture:SetInside(button) texture:SetTexCoord(0, 1, 0, 1) texture:SetRotation(S.ArrowRotation[button.CoAArrowDirection]) end end end local function SkinArrow(button, direction) if not button or button.CoASkinned then return end button.CoASkinned = true S:HandleNextPrevButton(button, direction) button.CoAArrowDirection = direction button.CoAArrowTextures = { button:GetNormalTexture(), button:GetPushedTexture(), button:GetDisabledTexture() } -- Still worth closing off: whatever refreshes these would otherwise be free -- to swap in a fresh texture object and orphan the three above. button.SetNormalTexture = E.noop button.SetPushedTexture = E.noop button.SetDisabledTexture = E.noop button.SetHighlightTexture = E.noop button:HookScript("OnUpdate", RestoreArrow) end -- The list inside each dropdown popup: a scroll frame with the framed inset -- and overlay art around it. The arrows are named off the scroll frame rather -- than off the scrollbar, so they're looked up here instead. local function SkinMenuScroll(listName) local scrollBar = _G[listName.."ScrollFrameScrollBar"] if not scrollBar then return end local inset = _G[listName.."Inset"] if inset then inset:StripTextures() end local overlay = _G[listName.."ScrollFrameArtOverlay"] if overlay then overlay:StripTextures() end if scrollBar.backdrop then return end local frameLevel = scrollBar:GetFrameLevel() scrollBar:Width(18) scrollBar:StripTextures() scrollBar:CreateBackdrop() scrollBar.backdrop:SetAllPoints() scrollBar.backdrop:SetFrameLevel(frameLevel) local up = _G[listName.."ScrollFrameScrollUpButton"] if up then up:Point("BOTTOM", scrollBar, "TOP", 0, 1) SkinArrow(up, "up") end local down = _G[listName.."ScrollFrameScrollDownButton"] if down then down:Point("TOP", scrollBar, "BOTTOM", 0, -1) SkinArrow(down, "down") end local thumbName = listName.."ScrollFrameScrollBarThumb" local thumb = (scrollBar.GetThumbTexture and scrollBar:GetThumbTexture()) or _G[thumbName] SkinScrollThumb(thumb, thumbName) end -- The bar's plate art (BottomBarTexture) is the dark band under the buttons, -- and it's the bar's only region, so a plain StripTextures clears it without -- touching the scene art, which lives on TreeView one level up. The buttons, -- dropdowns and search box sitting on the bar are picked up by the walk. local function SkinBottomBar() local bar = _G[BOTTOM_BAR] if not bar then return end bar:StripTextures() -- Each dropdown's caret is a child button carrying the arrow as its own -- art, so the walk was treating it as an ordinary button and giving it a -- templated square. Skinned here, ahead of the walk, so it becomes the -- ElvUI chevron; HandleNextPrevButton's isSkinned flag then makes the walk -- leave it alone. noBackdrop keeps it a bare white arrow that takes the -- value colour on hover, and "up" matches the way these menus open. for _, suffix in ipairs(BOTTOM_BAR_DROPDOWNS) do local dropdown = _G[BOTTOM_BAR..suffix] local arrow = _G[BOTTOM_BAR..suffix.."Button"] if arrow then S:HandleNextPrevButton(arrow, "up", nil, true) arrow:Size(20, 20) end -- These buttons are wider than they look: the label art only ever -- covered the part up to the arrow, so templating the whole frame drew -- a panel running under the icon buttons to their right. Skinned here -- rather than in the walk so the backdrop can be built separately and -- stopped at the arrow, and the dead space to its right is taken out -- of the hit rect so it can't swallow clicks meant for those icons. if dropdown and arrow then S:HandleButton(dropdown, nil, nil, true) if dropdown.backdrop then dropdown.backdrop:ClearAllPoints() dropdown.backdrop:Point("TOPLEFT", dropdown, "TOPLEFT", -1, 1) dropdown.backdrop:Point("BOTTOMRIGHT", arrow, "BOTTOMRIGHT", 3, -1) end local dropdownRight, arrowRight = dropdown:GetRight(), arrow:GetRight() if dropdownRight and arrowRight then dropdown:SetHitRectInsets(0, math.max(0, dropdownRight - arrowRight - 3), 0, 0) end end end -- The two dropdown popups are plain frames, which the walk descends into -- for their buttons but never templates, so they get their panel here. for _, entry in ipairs(BOTTOM_BAR_MENUS) do local menuName = BOTTOM_BAR..entry.menu local listName = menuName..entry.list local menu = _G[menuName] if menu and not menu.CoASkinned then menu.CoASkinned = true 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 -- on the menu's. menu:HookScript("OnShow", function() SkinMenuRows(listName) end) end SkinMenuScroll(listName) SkinMenuRows(listName) end end -- The scene art belongs to TreeView, which runs on behind the bottom bar, so -- clearing the bar's own plate left the art showing through under the -- buttons. The pieces are stretched over the whole of TreeView and show their -- slice of the file through tex coords, which occupy only part of it -- the -- rest of the file holds other art. So the coords have to be trimmed in -- proportion to their existing values rather than replaced outright, or -- unrelated regions of the file scroll into view. -- -- Geometry comes from TreeView and the bar rather than from the texture's own -- rect, so that a re-run reads the same numbers instead of measuring a rect -- it already cropped. Coords are kept from the first pass for the same reason. local function CropTexture(tex, bar, keep) if not tex.CoAOriginalCoords then tex.CoAOriginalCoords = {tex:GetTexCoord()} end -- GetTexCoord yields the four corners: UL, LL, UR, LR. These are all -- axis-aligned, so the edges come off the corners that define them. local coords = tex.CoAOriginalCoords local left, top, bottom, right = coords[1], coords[2], coords[4], coords[5] tex:SetTexCoord(left, right, top, top + (bottom - top) * keep) tex:Point("BOTTOMRIGHT", bar, "TOPRIGHT", 0, 0) end local function CropBackground() local treeView = _G[FRAME_NAME.."TreeView"] local bar = _G[BOTTOM_BAR] if not (treeView and bar) then return end local treeTop, treeBottom, barTop = treeView:GetTop(), treeView:GetBottom(), bar:GetTop() if not (treeTop and treeBottom and barTop) then return end local height = treeTop - treeBottom if height <= 0 then return end local keep = (treeTop - barTop) / height if keep <= 0 or keep >= 1 then return end -- Scoped to TreeView's own regions: earlier this looked the names up as -- globals over a fixed range, which pulled in same-named textures owned by -- other frames and re-anchored those too. for i = 1, treeView:GetNumRegions() do local region = select(i, treeView:GetRegions()) local name = region.GetName and region:GetName() if name and name:find("Background") and region:GetObjectType() == "Texture" then CropTexture(region, bar, keep) end end end -- 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 Skin:Tab(tab, frame) end end local SPEC_CHOICE = FRAME_NAME.."SpecViewPoolFrameCoASpecChoiceTemplate%d" local MAX_SPEC_CHOICES = 10 -- Only each card's action button. The cards themselves keep their art: the -- portraits and the gold wash on the active spec are the whole point of the -- view, and there's nothing flat that would say the same thing. -- -- Skinned by name because the walk can't get here -- the cards hang off a -- PoolFrame, which EXCLUDED skips so the tree's pooled nodes stay untouched. -- Stripped as well as templated: the red plate isn't one of the Left/Middle/ -- Right pieces HandleButton clears on its own. local function SkinSpecChoices() for i = 1, MAX_SPEC_CHOICES do local cardName = SPEC_CHOICE:format(i) if not _G[cardName] then break end S:HandleButton(_G[cardName.."SelectButton"], true) end end local function SkinFrame(frame) if not frame.CoASkinned then frame.CoASkinned = true 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. Skin:HideArt(_G[FRAME_NAME.."PortraitFrame"]) 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 -- show, and again a tick later in case the pool is filled after it. local specView = _G[FRAME_NAME.."SpecView"] if specView then specView:HookScript("OnShow", function() SkinSpecChoices() E:Delay(0.1, SkinSpecChoices) end) end -- Children are created lazily as tabs are visited, so the walk has to -- 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() SkinChildren(self, 1) SkinTabs() SkinSpecChoices() end) end Skin:Title(_G[FRAME_NAME.."TitleText"]) SkinCloseButton(frame) SkinBottomBar() -- Not CropBackground() here: this runs pre-Show now (see InitializeTalentFrame), -- before Blizzard's own code sets the background texture's real art-tile -- coords -- it's still XML-template default (full 0..1) at this point. Crop -- caches "original" coords on first call and never recaptures, so cropping -- here would permanently bake in the wrong baseline. OnShow (below) is the -- only place this is safe to run. SkinChildren(frame, 1) SkinTabs() SkinSpecChoices() end local function TryHook() local frame = _G[FRAME_NAME] if not frame then return false end SkinFrame(frame) return true end function CoA:InitializeTalentFrame() if not E.private.skins.blizzard.enable then return end Skin:OnFrameAvailable(TryHook) end