Stop disabled buttons taking the hover border colour

S:HandleButton ends with an unconditional OnEnter/OnLeave pair that swaps
the border to the value colour and back, and neither handler looks at
IsEnabled. A disabled button still fires both scripts on this client, so
the greyed Activate button, Save changes with nothing pending and Purchase
with nothing selected all lit up under the cursor and read as clickable.

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. Skin:Button wraps HandleButton with that hook, and every call site
in the plugin now goes through it, so the correction is inherited rather
than repeated per module.

The Activate button carries the border from its own update as well:
activating a spec disables it under the very cursor that just clicked it,
and OnEnter has been and gone by then, so nothing else would put the border
back until the pointer moved off.

Skin:IsEnabled hoists the 0/1-to-boolean normalisation this client needs
out of UpdateActivateState, which held the only copy of it.
This commit is contained in:
2026-08-18 16:30:27 +02:00
parent cddfc7e7af
commit d2e55eb3cb
4 changed files with 89 additions and 17 deletions
+69 -1
View File
@@ -138,6 +138,74 @@ function Skin:HideArt(frame)
frame:Hide() frame:Hide()
end 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, ...)
-- 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
-- Several of these controls draw their art as anonymous regions of one atlas -- 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/ -- 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 -- Pushed/Disabled set S:HandleButton knows how to clear, so its own clearing
@@ -205,7 +273,7 @@ function Skin:Dropdown(dropdown, menu)
dropdown.CoASkinned = true dropdown.CoASkinned = true
self:StripArtByFile(dropdown, DROPDOWN_ART) self:StripArtByFile(dropdown, DROPDOWN_ART)
S:HandleButton(dropdown) self:Button(dropdown)
SkinDropdownCaret(dropdown) SkinDropdownCaret(dropdown)
self:Panel(menu) self:Panel(menu)
+14 -10
View File
@@ -56,7 +56,7 @@ function SkinChildren(frame, depth)
-- "...MenuClose", and it was coming out of the walk as an -- "...MenuClose", and it was coming out of the walk as an
-- ordinary templated square with the X stripped off it. -- ordinary templated square with the X stripped off it.
if not (name and name:find("Close")) then if not (name and name:find("Close")) then
S:HandleButton(child) Skin:Button(child)
end end
elseif objType == "Frame" then elseif objType == "Frame" then
if IsDropDown(child, name) then if IsDropDown(child, name) then
@@ -353,17 +353,21 @@ local ACTIVATE_DISABLED_COLOR = {0.55, 0.55, 0.55}
-- says so -- the ElvUI panel is drawn the same either way. The label is greyed -- says so -- the ElvUI panel is drawn the same either way. The label is greyed
-- instead, which is how ElvUI marks a dead button everywhere else. -- instead, which is how ElvUI marks a dead button everywhere else.
-- --
-- IsEnabled hands back 0 and 1 on this client rather than false and true -- The enabled state is compared against the cached one rather than written
-- (probed), and 0 is truthy in Lua, so it's normalised before it's compared -- blind because this runs from the button's update: there's no event for a spec
-- against the cached value. Compared rather than written blind because this -- becoming active. Skin:IsEnabled does the 0/1-to-boolean normalisation this
-- runs from the button's update: there's no event for a spec becoming active. -- client needs (see Skinning.lua).
local function UpdateActivateState(button) local function UpdateActivateState(button)
local state = button:IsEnabled() local enabled = Skin:IsEnabled(button)
local enabled = state ~= nil and state ~= false and state ~= 0
if enabled == button.CoAActivateEnabled then return end if enabled == button.CoAActivateEnabled then return end
button.CoAActivateEnabled = enabled button.CoAActivateEnabled = enabled
-- Activating a spec disables this button under the very cursor that just
-- clicked it, so its hover border would stay lit until the pointer moved
-- off. This update already watches the state, so it carries the border too.
Skin:RefreshButtonBorder(button)
local text = ActivateLabel(button) local text = ActivateLabel(button)
if not text then return end if not text then return end
@@ -380,7 +384,7 @@ local function SkinActivateButton(button)
button.CoASkinned = true button.CoASkinned = true
Skin:StripArtByFile(button, Skin.RedButtonArt) Skin:StripArtByFile(button, Skin.RedButtonArt)
S:HandleButton(button) Skin:Button(button)
button:HookScript("OnUpdate", UpdateActivateState) button:HookScript("OnUpdate", UpdateActivateState)
end end
@@ -601,7 +605,7 @@ local function SkinBottomBar()
-- stopped at the arrow, and the dead space to its right is taken out -- 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. -- of the hit rect so it can't swallow clicks meant for those icons.
if dropdown and arrow then if dropdown and arrow then
S:HandleButton(dropdown, nil, nil, true) Skin:Button(dropdown, nil, nil, true)
if dropdown.backdrop then if dropdown.backdrop then
dropdown.backdrop:ClearAllPoints() dropdown.backdrop:ClearAllPoints()
@@ -719,7 +723,7 @@ local function SkinSpecChoices()
local cardName = SPEC_CHOICE:format(i) local cardName = SPEC_CHOICE:format(i)
if not _G[cardName] then break end if not _G[cardName] then break end
S:HandleButton(_G[cardName.."SelectButton"], true) Skin:Button(_G[cardName.."SelectButton"], true)
end end
end end
+2 -2
View File
@@ -17,8 +17,8 @@ local FRAME_NAME = "StoreCollectionFrame"
-- Highlight textures), confirmed by probe -- S:HandleButton's own texture -- Highlight textures), confirmed by probe -- S:HandleButton's own texture
-- clearing handles these directly, no manual stripping needed. -- clearing handles these directly, no manual stripping needed.
local function SkinActionButtons() local function SkinActionButtons()
S:HandleButton(_G[FRAME_NAME.."ActivateStoreButton"]) Skin:Button(_G[FRAME_NAME.."ActivateStoreButton"])
S:HandleButton(_G[FRAME_NAME.."BuyStoreButton"]) Skin:Button(_G[FRAME_NAME.."BuyStoreButton"])
end end
local function SkinSearchBox() local function SkinSearchBox()
+4 -4
View File
@@ -8,9 +8,9 @@ local FRAME_NAME = "AppearanceWardrobeFrame"
-- Standard UIPanelButtonTemplate art, same as Vanity's action buttons -- -- Standard UIPanelButtonTemplate art, same as Vanity's action buttons --
-- S:HandleButton's own texture clearing handles these directly. -- S:HandleButton's own texture clearing handles these directly.
local function SkinActionButtons() local function SkinActionButtons()
S:HandleButton(_G[FRAME_NAME.."PlayerModelSaveOutfitButton"]) Skin:Button(_G[FRAME_NAME.."PlayerModelSaveOutfitButton"])
S:HandleButton(_G[FRAME_NAME.."DisableTransmogButton"]) Skin:Button(_G[FRAME_NAME.."DisableTransmogButton"])
S:HandleButton(_G[FRAME_NAME.."DisableSpellVisualsButton"]) Skin:Button(_G[FRAME_NAME.."DisableSpellVisualsButton"])
end end
-- Apply and Cancel only exist while a transmog change is pending, which is why -- Apply and Cancel only exist while a transmog change is pending, which is why
@@ -46,7 +46,7 @@ local function SkinPendingButton(name)
-- Stripped before templating: HandleButton adds its backdrop as regions of -- Stripped before templating: HandleButton adds its backdrop as regions of
-- this same button, so a strip afterwards takes the backdrop with the pill. -- this same button, so a strip afterwards takes the backdrop with the pill.
S:HandleButton(button) Skin:Button(button)
local text = button.GetFontString and button:GetFontString() local text = button.GetFontString and button:GetFontString()
if text then text:SetTextColor(unpack(LABEL_COLOR)) end if text then text:SetTextColor(unpack(LABEL_COLOR)) end