63e179e96e
The scroll frame re-shows the button's own native art whenever it recalculates, and one skinning pass can't hold against it: ElvUI's HandleNextPrevButton guards on isSkinned, so a second call does nothing. The talent frame already carried the answer -- re-hide the art by file from OnUpdate and pin the three state textures -- so it moves into the shared layer as Skin:ScrollArrow and the Challenges list scrolls take it too. Routing the talent frame through it also picks up the disabled-hover fix every other button already has.
835 lines
34 KiB
Lua
835 lines
34 KiB
Lua
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
|
|
|
|
-- Scale is a per-frame setting rather than one shared number: the three frames
|
|
-- don't ship at the same scale (the vanity store's is about 9% larger than the
|
|
-- other two, measured), and that's the server's choice, so the slider is a
|
|
-- multiplier on whatever each frame was given rather than an absolute. A
|
|
-- setting of 1 therefore leaves every frame exactly as it came.
|
|
--
|
|
-- Scale is applied to Collections, the container all three windows and the tab
|
|
-- row are children of (confirmed by probe), rather than to a window itself.
|
|
-- Collections is also the only one of them with mouse enabled -- it's what the
|
|
-- player drags -- so scaling a window directly shrank the art while leaving the
|
|
-- drag target at full size, which is why a shrunk window had to be grabbed by
|
|
-- clicking outside itself. Scaling the container moves its hit area with it,
|
|
-- and the tabs come along as its children.
|
|
--
|
|
-- The setting is still per-window: only one of them is ever open, and each
|
|
-- re-applies its own on show. The native scale is captured before anything here
|
|
-- has written one, so re-applying can't compound.
|
|
local CONTAINER_NAME = "Collections"
|
|
|
|
local scaleKey
|
|
|
|
local function GetScaleSetting(key)
|
|
local db = CoA.db and CoA.db.profile.skins.collections
|
|
|
|
return (db and db[key]) or 1
|
|
end
|
|
|
|
function Skin:ApplyWindowScale(key)
|
|
local container = _G[CONTAINER_NAME]
|
|
if not container then return end
|
|
|
|
if not container.CoANativeScale then
|
|
container.CoANativeScale = container:GetScale()
|
|
end
|
|
|
|
-- Remembered so the slider can re-apply for whichever window is open.
|
|
scaleKey = key
|
|
|
|
local scale = container.CoANativeScale * GetScaleSetting(key)
|
|
|
|
if container:GetScale() ~= scale then
|
|
container:SetScale(scale)
|
|
end
|
|
end
|
|
|
|
-- Live update from the slider: a window only re-runs its own skin pass on show,
|
|
-- and a scale change should land while one is open.
|
|
function CoA:UpdateFrameScales()
|
|
if scaleKey then
|
|
Skin:ApplyWindowScale(scaleKey)
|
|
end
|
|
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
|
|
|
|
-- Buttons ---------------------------------------------------------------------
|
|
--
|
|
-- IsEnabled hands back 0 and 1 on this client rather than false and true
|
|
-- (probed), and 0 is truthy in Lua, so every enabled check has to normalise
|
|
-- what it gets back before comparing it.
|
|
function Skin:IsEnabled(button)
|
|
local state = button.IsEnabled and button:IsEnabled()
|
|
|
|
return state ~= nil and state ~= false and state ~= 0
|
|
end
|
|
|
|
-- The target is resolved the way ElvUI's own SetModifiedBackdrop and
|
|
-- SetOriginalBackdrop resolve it -- the backdrop when the button has one, the
|
|
-- button itself otherwise -- or the colour lands on something that isn't the
|
|
-- object drawing the border.
|
|
local function SetButtonBorder(button, lit)
|
|
local backdrop = button.backdrop or button
|
|
if not backdrop.SetBackdropBorderColor then return end
|
|
|
|
backdrop:SetBackdropBorderColor(unpack(lit and E.media.rgbvaluecolor or E.media.bordercolor))
|
|
end
|
|
|
|
-- S:HandleButton ends with an unconditional pair of OnEnter/OnLeave hooks that
|
|
-- swap the border to the value colour and back, and neither of them looks at
|
|
-- IsEnabled. A disabled button still fires both scripts on this client, so a
|
|
-- dead button lights up under the cursor and reads as clickable -- the greyed
|
|
-- Activate button, Save changes with nothing pending, Purchase with nothing
|
|
-- selected. HookScript can't be undone, but handlers fire in registration
|
|
-- order, so a hook registered after HandleButton runs last and has the final
|
|
-- say on the colour.
|
|
--
|
|
-- This one deliberately doesn't consult IsMouseOver: OnEnter can fire a pixel
|
|
-- before IsMouseOver turns true (ElvUI's own aura code pads around the same
|
|
-- quirk), and reading it here would drop the highlight off a live button.
|
|
local function OnButtonEnter(button)
|
|
if Skin:IsEnabled(button) then return end
|
|
|
|
SetButtonBorder(button, false)
|
|
end
|
|
|
|
-- For a button that can be disabled while the cursor is already on it: OnEnter
|
|
-- has been and gone by then, so nothing corrects the border until the pointer
|
|
-- leaves. Only worth wiring up where something already watches the button's
|
|
-- state -- it does not justify an OnUpdate of its own.
|
|
function Skin:RefreshButtonBorder(button)
|
|
SetButtonBorder(button, self:IsEnabled(button) and button:IsMouseOver())
|
|
end
|
|
|
|
-- Every S:HandleButton call site in the plugin goes through here, so the
|
|
-- disabled-hover correction is inherited rather than repeated per module. The
|
|
-- extra arguments are HandleButton's own (strip, isDeclineButton,
|
|
-- useCreateBackdrop, noSetTemplate) and are passed straight through.
|
|
function Skin:Button(button, ...)
|
|
if not button then return end
|
|
|
|
S:HandleButton(button, ...)
|
|
|
|
-- Unguarded: a pass over a handful of regions is cheap, and re-running it
|
|
-- catches a region added to the button after the first skin pass.
|
|
self:StripHighlightArt(button)
|
|
|
|
-- Guarded separately from HandleButton's own isSkinned flag: these frames
|
|
-- re-run their skin pass on every show, and HookScript stacks handlers
|
|
-- rather than replacing them, so an unguarded re-hook adds another copy
|
|
-- per show for the life of the session.
|
|
if not button.CoAButtonSkinned then
|
|
button.CoAButtonSkinned = true
|
|
|
|
button:HookScript("OnEnter", OnButtonEnter)
|
|
end
|
|
end
|
|
|
|
-- S:HandleNextPrevButton routes through S:HandleButton, so it inherits exactly
|
|
-- the lit-border-on-a-dead-button behaviour OnButtonEnter is here to correct,
|
|
-- and scroll arrows spend a good deal of their life disabled at one end of their
|
|
-- list or the other. Same separate guard as Skin:Button, and for the same
|
|
-- reason: these frames re-run their skin passes, and HookScript stacks.
|
|
function Skin:NextPrevButton(button, direction)
|
|
if not button then return end
|
|
|
|
S:HandleNextPrevButton(button, direction)
|
|
|
|
if not button.CoAButtonSkinned then
|
|
button.CoAButtonSkinned = true
|
|
|
|
button:HookScript("OnEnter", OnButtonEnter)
|
|
end
|
|
end
|
|
|
|
-- Several of these controls draw their art as anonymous regions of one atlas
|
|
-- file rather than through 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 or the
|
|
-- label with it. Those are cleared by file instead.
|
|
--
|
|
-- SetTexture is noop'd per region rather than just cleared: whatever re-arts
|
|
-- the control on a state change (a mouse-down swapping in the pressed variant,
|
|
-- a tex coord swap between two variants of the same file) is free to re-set the
|
|
-- file as well, and a cleared texture would come straight back.
|
|
function Skin:StripArtByFile(frame, pattern)
|
|
if not frame then return end
|
|
|
|
for i = 1, frame:GetNumRegions() do
|
|
local region = select(i, frame:GetRegions())
|
|
local texture = region.GetTexture and region:GetTexture()
|
|
|
|
if texture and tostring(texture):find(pattern) then
|
|
region:SetTexture(nil)
|
|
region.SetTexture = E.noop
|
|
end
|
|
end
|
|
end
|
|
|
|
-- S:HandleButton clears the button's own Normal/Highlight/Pushed/Disabled
|
|
-- textures and the named Left/Middle/Right pieces, and nothing else -- so a
|
|
-- native highlight drawn as a plain region of the button survives it and lights
|
|
-- up under the cursor against the flat ElvUI backdrop. That was the wardrobe
|
|
-- Cancel button's red glow, and clearing it by file there fixed exactly that
|
|
-- one button: probing the layer across everything the plugin skins turned up
|
|
-- the Save outfit button wearing the same red file, and Disable transmog and
|
|
-- Disable spell visuals wearing the dialog-box glow, none of which any
|
|
-- clear-by-file call site was ever going to reach.
|
|
--
|
|
-- The whole layer goes rather than named files, because on these frames it only
|
|
-- ever carries native hover art. Every button probed has exactly one HIGHLIGHT
|
|
-- region, and on the ones that already looked right it is blank -- so there is
|
|
-- nothing else living there to lose. ElvUI's own hover treatment isn't caught:
|
|
-- it's the backdrop border swap HandleButton hooks, and a backdrop is drawn
|
|
-- through SetBackdrop plus two child border frames, not as a region of the
|
|
-- button at all.
|
|
--
|
|
-- Noop'd rather than only cleared, as in StripArtByFile: a state change that
|
|
-- re-arts the button would otherwise bring the glow straight back.
|
|
function Skin:StripHighlightArt(frame)
|
|
if not frame then return end
|
|
|
|
for i = 1, frame:GetNumRegions() do
|
|
local region = select(i, frame:GetRegions())
|
|
|
|
if region:GetObjectType() == "Texture" and region:GetDrawLayer() == "HIGHLIGHT" then
|
|
region:SetTexture(nil)
|
|
region.SetTexture = E.noop
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The pill behind the talent frame's Activate button and the wardrobe's Apply
|
|
-- and Cancel buttons. Two files rather than one: "128GoldRedButton" carries the
|
|
-- green and grey variants as tex coords, and Cancel alone is drawn from
|
|
-- "128RedButton" (probed). The shared suffix matches both, and nothing else on
|
|
-- these frames uses either. Only the pill itself is this pattern's job now --
|
|
-- the hover art on both files goes with the rest of the HIGHLIGHT layer.
|
|
Skin.RedButtonArt = "RedButton"
|
|
|
|
-- The dropdown pills on the vanity and wardrobe frames are the same widget:
|
|
-- nine anonymous "Silver-Button" slices.
|
|
local DROPDOWN_ART = "Silver%-Button"
|
|
local CARET_ART = "ChatFrameExpandArrow"
|
|
local CARET_SIZE = 14
|
|
|
|
-- 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
|
|
|
|
self:StripArtByFile(dropdown, DROPDOWN_ART)
|
|
self:Button(dropdown)
|
|
SkinDropdownCaret(dropdown)
|
|
|
|
self:Panel(menu)
|
|
end
|
|
|
|
-- The CoA search boxes draw their border and magnifier from the stock
|
|
-- "CommonSearch" file, tinted to the server's own colour, as anonymous regions
|
|
-- -- so S:HandleEditBox can't reach them: it only alpha-0s the named Left/
|
|
-- Middle/Right pieces a stock Blizzard edit box has.
|
|
--
|
|
-- Cleared by file rather than by a blind strip, which is what a first pass did:
|
|
-- an EditBox draws its own caret and its selection highlight as plain solid
|
|
-- textures of its own (probed: four of them, sub-pixel wide and text-high, shown
|
|
-- only while the box has focus), and StripTextures takes those with the border,
|
|
-- leaving a search box you can type in with no cursor in it.
|
|
local SEARCH_BOX_ART = "CommonSearch"
|
|
|
|
function Skin:SearchBox(box)
|
|
if not box or box.CoASearchSkinned then return end
|
|
box.CoASearchSkinned = true
|
|
|
|
self:StripArtByFile(box, SEARCH_BOX_ART)
|
|
S:HandleEditBox(box)
|
|
end
|
|
|
|
-- Scroll bars ------------------------------------------------------------------
|
|
--
|
|
-- S:HandleScrollBar can't be used on these: the CoA bars are hand-built rather
|
|
-- than the stock Slider widget, so the thumb is a Button with art of its own
|
|
-- (probed) where a stock bar has a thumb *texture*, and HandleScrollBar reaches
|
|
-- for the thumb through GetThumbTexture and then calls SetTexture on what it
|
|
-- finds. It also looks the arrow buttons up as children of the bar, and these
|
|
-- are named off the scroll frame instead. The treatment below is the same one it
|
|
-- applies, only with each piece handed in by the caller.
|
|
--
|
|
-- The art on both pieces is applied lazily, and that is what defeated two
|
|
-- earlier passes at this: at skin time the thumb's textures read back nil, so
|
|
-- matching them by file found nothing to clear, and the scroll code arts them a
|
|
-- moment later when it first measures the list. Clearing, noop'ing the regions'
|
|
-- SetTexture, and alpha'ing them all failed the same way -- not because the art
|
|
-- came back, but because it was never there yet to be taken.
|
|
--
|
|
-- So it's handled from OnUpdate, the same answer the tab art needed and for the
|
|
-- same reason: nothing fires when it happens, and the check is a three-region
|
|
-- alpha compare that only writes when something has actually re-arted the
|
|
-- widget. Blanket rather than matched by file: neither the track nor the thumb
|
|
-- carries anything but its own art -- no label, no counter -- which is the only
|
|
-- thing StripArtByFile's file matching is there to protect.
|
|
local function HideScrollArt(frame)
|
|
for i = 1, frame:GetNumRegions() do
|
|
local region = select(i, frame:GetRegions())
|
|
|
|
if region:GetObjectType() == "Texture" and region:GetAlpha() ~= 0 then
|
|
region:SetAlpha(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Colours and states taken from ElvUI's own proportional scroll bars
|
|
-- (S:HandleProportionalScroll), which is what every other scroll bar in this UI
|
|
-- looks like: the value colour at a quarter alpha, three quarters while hovered
|
|
-- or held. Its handlers are locals in ElvUI's file, so the four scripts are
|
|
-- written out here rather than reached for.
|
|
--
|
|
-- The active flag is what keeps a thumb lit while it's being dragged: the
|
|
-- pointer routinely leaves a thin thumb mid-drag, and OnLeave would otherwise
|
|
-- dim it while the list is still moving under it.
|
|
local SCROLL_BUTTON_GAP = 1
|
|
local SCROLL_THUMB_ALPHA = 0.25
|
|
local SCROLL_THUMB_ACTIVE_ALPHA = 0.75
|
|
|
|
local function SetThumbColor(thumb, alpha)
|
|
if not thumb.backdrop then return end
|
|
|
|
local r, g, b = unpack(E.media.rgbvaluecolor)
|
|
thumb.backdrop:SetBackdropColor(r, g, b, alpha)
|
|
end
|
|
|
|
local function ThumbOnEnter(thumb)
|
|
SetThumbColor(thumb, SCROLL_THUMB_ACTIVE_ALPHA)
|
|
end
|
|
|
|
local function ThumbOnLeave(thumb)
|
|
if thumb.CoAThumbActive then return end
|
|
|
|
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
|
|
end
|
|
|
|
local function ThumbOnMouseDown(thumb)
|
|
thumb.CoAThumbActive = true
|
|
|
|
SetThumbColor(thumb, SCROLL_THUMB_ACTIVE_ALPHA)
|
|
end
|
|
|
|
local function ThumbOnMouseUp(thumb)
|
|
thumb.CoAThumbActive = nil
|
|
|
|
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
|
|
end
|
|
|
|
-- The arrows re-art themselves the same way the track and thumb do, and one
|
|
-- pass can't hold against it: S:HandleNextPrevButton guards on its own
|
|
-- isSkinned flag, so calling it a second time does nothing at all. What comes
|
|
-- back isn't the ElvUI arrow being overwritten either -- that texture is still
|
|
-- in place underneath (probed: the native "scrollbarproportional" region shown
|
|
-- alongside ArrowUp). It's the button's own art, which StripTextures only hid,
|
|
-- and the scroll frame shows it again whenever it recalculates.
|
|
--
|
|
-- So the native art is re-hidden by file from OnUpdate, the same answer the
|
|
-- track, the thumb and the tab art all needed and for the same reason: nothing
|
|
-- fires when it happens. The three state textures are captured and their
|
|
-- setters noop'd as well -- still worth closing off, since whatever refreshes
|
|
-- these is free to swap in a fresh texture object and orphan the three.
|
|
--
|
|
-- Matched by file rather than as "any region that isn't one of ours": 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 tostring(texture):lower():find(NATIVE_SCROLL_ART) and region:IsShown() then
|
|
region:Hide()
|
|
end
|
|
end
|
|
|
|
for _, texture in ipairs(button.CoAArrowTextures) do
|
|
local current = texture:GetTexture()
|
|
|
|
if not current or tostring(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
|
|
|
|
function Skin:ScrollArrow(button, direction)
|
|
if not button or button.CoAArrowSkinned then return end
|
|
button.CoAArrowSkinned = true
|
|
|
|
self:NextPrevButton(button, direction)
|
|
|
|
button.CoAArrowDirection = direction
|
|
button.CoAArrowTextures = {
|
|
button:GetNormalTexture(),
|
|
button:GetPushedTexture(),
|
|
button:GetDisabledTexture(),
|
|
}
|
|
|
|
button.SetNormalTexture = E.noop
|
|
button.SetPushedTexture = E.noop
|
|
button.SetDisabledTexture = E.noop
|
|
button.SetHighlightTexture = E.noop
|
|
|
|
button:HookScript("OnUpdate", RestoreArrow)
|
|
end
|
|
|
|
function Skin:ScrollBar(bar, thumb, up, down)
|
|
if not bar or bar.CoASkinned then return end
|
|
bar.CoASkinned = true
|
|
|
|
-- The track gets no backdrop of its own, only its art taken off, which is
|
|
-- what ElvUI's own proportional scroll bars do: against these panels an empty
|
|
-- channel reads cleaner than a second filled block behind the thumb, and the
|
|
-- thumb is legible on its own at the value colour.
|
|
bar:HookScript("OnUpdate", HideScrollArt)
|
|
|
|
if thumb then
|
|
thumb:HookScript("OnUpdate", HideScrollArt)
|
|
|
|
-- Filling the thumb rather than inset inside it: the thumb is already
|
|
-- narrower than the track it runs in, so an inset backdrop reads as a thin
|
|
-- line rather than as a grip. The track's own gutter is the separation.
|
|
--
|
|
-- Levelled off the thumb rather than off the track: the thumb sits a level
|
|
-- above the track already, so a backdrop built at the track's level would
|
|
-- land underneath the thumb's own regions.
|
|
thumb:CreateBackdrop("Transparent")
|
|
thumb.backdrop:SetFrameLevel(thumb:GetFrameLevel() + 1)
|
|
thumb.backdrop:SetAllPoints()
|
|
|
|
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
|
|
|
|
thumb:HookScript("OnEnter", ThumbOnEnter)
|
|
thumb:HookScript("OnLeave", ThumbOnLeave)
|
|
thumb:HookScript("OnMouseDown", ThumbOnMouseDown)
|
|
thumb:HookScript("OnMouseUp", ThumbOnMouseUp)
|
|
end
|
|
|
|
-- Re-anchored onto the track's ends after skinning, and cleared first rather
|
|
-- than pointed on top of what's there: HandleNextPrevButton resizes them to
|
|
-- ElvUI's own 18px, so whatever the native layout anchored them by no longer
|
|
-- lines up with the track, and a second point left in place would stretch the
|
|
-- button between the two.
|
|
if up then
|
|
self:ScrollArrow(up, "up")
|
|
up:ClearAllPoints()
|
|
up:Point("BOTTOM", bar, "TOP", 0, SCROLL_BUTTON_GAP)
|
|
end
|
|
|
|
if down then
|
|
self:ScrollArrow(down, "down")
|
|
down:ClearAllPoints()
|
|
down:Point("TOP", bar, "BOTTOM", 0, -SCROLL_BUTTON_GAP)
|
|
end
|
|
end
|
|
|
|
-- Tabs -----------------------------------------------------------------------
|
|
--
|
|
-- S:HandleTab still isn't used on any of these. On the server-authored tabs it
|
|
-- can't be: it clears the tab body by name, looking for a "Middle" piece, and
|
|
-- those name theirs "Center", so the body survives and the ElvUI backdrop just
|
|
-- lands behind the old art. The Challenges window's tabs are stock Blizzard
|
|
-- ones that do use "Middle" (probed), so HandleTab would reach them -- they go
|
|
-- through the same strip anyway, because one code path is what keeps every tab
|
|
-- row in this UI looking alike.
|
|
--
|
|
-- One list covers all three tab flavours: the talent frame's tabs carry the
|
|
-- Disabled variants as well, the wardrobe's category tabs don't, the Challenges
|
|
-- tabs use Blizzard's Left/Middle/Right plus the Disabled set Blizzard swaps in
|
|
-- to draw the selected tab, and the lookups for the ones that don't exist
|
|
-- simply come back nil.
|
|
local TAB_TEXTURES = {
|
|
"Left", "Center", "Middle", "Right",
|
|
"LeftDisabled", "CenterDisabled", "MiddleDisabled", "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
|
|
|
|
-- Exposed: a module that re-anchors a tab row against the frame it belongs to
|
|
-- has to know how far inside its tab the backdrop actually starts, or it lines
|
|
-- the tab up flush and leaves the visible edge floating by this much.
|
|
local TAB_BACKDROP_INSET = 3
|
|
Skin.TabBackdropInset = TAB_BACKDROP_INSET
|
|
|
|
-- 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. The same slack covers the width below.
|
|
local TAB_SIZE_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_SIZE_EPSILON then
|
|
tab.CoAGrownHeight = height + TAB_GROWTH
|
|
tab:SetHeight(tab.CoAGrownHeight)
|
|
end
|
|
end
|
|
|
|
-- Native panel tabs interlock: each is anchored back into the one before it by
|
|
-- the width of the art's end cap, so the ornate ends overlap rather than butting
|
|
-- together. Strip the art, put a flat backdrop in its place, and that overlap
|
|
-- has nothing left to hide it -- the row reads as one merged bar, and the tabs'
|
|
-- hit rects overlap along with their backdrops.
|
|
--
|
|
-- The tab is narrowed by exactly what its anchor was pulled back by, rather than
|
|
-- being pushed along to make room: pushing grows the row by the overlap times
|
|
-- the number of tabs, which on the Challenges window overhangs the frame by four
|
|
-- times the slack the row has. Absorbing it instead leaves the row's total
|
|
-- extent exactly where the native layout put it.
|
|
--
|
|
-- Opt-in per row rather than detected from the anchor: a tab anchored onto its
|
|
-- neighbour with a negative offset is the signature of an interlocking row, but
|
|
-- it would also match any row that merely sits tight, and silently resizing the
|
|
-- server-authored rows on that guess isn't worth the convenience.
|
|
local function UnoverlapTab(tab)
|
|
local point, relativeTo, relativePoint, x, y = tab:GetPoint(1)
|
|
if not (point and x) or x >= 0 then return end
|
|
|
|
tab.CoATabOverlap = -x
|
|
tab:SetPoint(point, relativeTo, relativePoint, 0, y)
|
|
end
|
|
|
|
-- Re-checked every tick with the same self-healing shape as the grown height,
|
|
-- and for the same reason: the native tab code re-measures a tab off its own
|
|
-- label, and a width written once would be handed straight back.
|
|
local function UpdateTabWidth(tab)
|
|
local overlap = tab.CoATabOverlap
|
|
if not overlap then return end
|
|
|
|
local width = tab:GetWidth()
|
|
|
|
if not tab.CoAUnoverlappedWidth or math.abs(width - tab.CoAUnoverlappedWidth) > TAB_SIZE_EPSILON then
|
|
tab.CoAUnoverlappedWidth = width - overlap
|
|
tab:SetWidth(tab.CoAUnoverlappedWidth)
|
|
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.
|
|
--
|
|
-- Two probes rather than one: a stock Blizzard tab draws its selected state by
|
|
-- hiding the Left/Middle/Right set and showing the Disabled set in its place,
|
|
-- so watching only the inactive half would miss art returning on whichever tab
|
|
-- is currently open. The three pieces of a set are always re-arted together, so
|
|
-- one probe per set is enough.
|
|
local TAB_ART_PROBES = {"Left", "LeftDisabled"}
|
|
|
|
local function UpdateTabArt(tab)
|
|
BumpTabLevel(tab)
|
|
UpdateTabSize(tab)
|
|
UpdateTabWidth(tab)
|
|
|
|
local name = tab:GetName()
|
|
if not name then return end
|
|
|
|
for _, suffix in ipairs(TAB_ART_PROBES) do
|
|
local tex = _G[name..suffix]
|
|
|
|
if tex and tex:GetTexture() then
|
|
Skin:StripTabArt(tab)
|
|
return
|
|
end
|
|
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
|
|
|
|
-- The tab row along the bottom of the talent window is one set of buttons
|
|
-- shared by all three windows it switches between, not one row per window,
|
|
-- which is why it lives here rather than in any single frame's module. It
|
|
-- doesn't need scaling of its own -- it's a child of Collections, so it follows
|
|
-- whatever ApplyWindowScale sets.
|
|
local COLLECTION_TAB = "CollectionsPoolFrameCollectionTabTemplate%d"
|
|
local MAX_COLLECTION_TABS = 10
|
|
|
|
function Skin:CollectionTabs(owner)
|
|
for i = 1, MAX_COLLECTION_TABS do
|
|
local tab = _G[COLLECTION_TAB:format(i)]
|
|
if not tab then break end
|
|
|
|
self:Tab(tab, owner)
|
|
end
|
|
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.
|
|
--
|
|
-- unoverlap is for rows of interlocking native tabs (see UnoverlapTab). Only the
|
|
-- tabs anchored onto a neighbour take it; the first tab in a row has nothing to
|
|
-- pull back from and is left alone by the offset check.
|
|
function Skin:Tab(tab, levelParent, unoverlap)
|
|
if not tab then return end
|
|
|
|
-- Set on every call rather than only the first: the collection tabs are
|
|
-- shared, so the frame they have to draw above is whichever window is open.
|
|
if levelParent then
|
|
tab.CoALevelParent = levelParent
|
|
end
|
|
|
|
if not tab.CoASkinned then
|
|
tab.CoASkinned = true
|
|
|
|
self:StripTabArt(tab)
|
|
|
|
if unoverlap then
|
|
UnoverlapTab(tab)
|
|
end
|
|
|
|
-- 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
|
|
|
|
-- Only for tabs that actually carry an icon. On those (the talent
|
|
-- window's) the label's native anchor sits right off the icon, sized
|
|
-- for the smaller native font, so grown text collides with it, and
|
|
-- the label is nudged off whatever point the native layout gave it
|
|
-- rather than a hardcoded anchor that would fight that layout. The
|
|
-- wardrobe's category tabs have no icon and centre their label, so
|
|
-- the same nudge just pushed every one of them off-centre to the
|
|
-- right.
|
|
local name = tab:GetName()
|
|
local icon = name and _G[name.."Icon"]
|
|
|
|
if icon and icon.GetTexture and icon:GetTexture() and icon:IsShown() then
|
|
local point, relTo, relPoint, x, y = fontString:GetPoint(1)
|
|
|
|
if point then
|
|
fontString:SetPoint(point, relTo, relPoint, x + TAB_LABEL_OFFSET, y)
|
|
end
|
|
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
|