diff --git a/CHANGELOG.md b/CHANGELOG.md index a275570c..54726d9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Improvements * (Pinned Frames) Pinned frame settings are now **global per party/raid mode** and no longer saved into auto layouts — a raid auto layout only controls whether each pinned set is **shown** for that layout. This removes the stale/blank pinned data and editor mismatches that came from pinned settings being stored per-layout, and pinned edits now take effect live. (by Krathe) +* (Targeted Spells) The party and personal targeted-spell icons now use the **full border toolkit** — style, colour, alpha, inset, blend mode, shadow and animation — the same controls as the rest of the addon, and the important-spell highlight is now its **own border** you can style separately. Existing highlight settings carry over. (by Krathe) ### Bug Fixes @@ -29,6 +30,8 @@ * (Aura Designer) Text-only icons no longer draw a leftover border (static or expiring). (by Krathe) * (Aura Designer) Aura icon and square borders from older profiles keep their original look after the border rework, instead of appearing thinner or floating in a gap. (by Krathe) * (Buff/Debuff) Icon borders from older profiles no longer float in a gap after the border rework — they hug the icon as before. (by Krathe) +* (Borders) A border set to thickness **0** now hides cleanly across every style, while any border animation keeps running. (by Krathe) +* (Borders) Fixed a doubled glow that could appear on the **PROC** border animation when it re-triggered; the PROC start-flash is now an opt-in option. (by Krathe) ## [4.4.1] diff --git a/Config.lua b/Config.lua index daf8d3cb..c8f6c311 100644 --- a/Config.lua +++ b/Config.lua @@ -1706,6 +1706,33 @@ DF.PartyDefaults = { personalTargetedSpellHighlightInset = 3, personalTargetedSpellHighlightSize = 3, personalTargetedSpellHighlightStyle = "glow", + personalTargetedSpellImportantBorderAnimationColor = {r = 1, g = 0.8, b = 0, a = 1}, + personalTargetedSpellImportantBorderAnimationCornerLength = 10, + personalTargetedSpellImportantBorderAnimationFrequency = 0.25, + personalTargetedSpellImportantBorderAnimationInset = 0, + personalTargetedSpellImportantBorderAnimationLength = 8, + personalTargetedSpellImportantBorderAnimationMask = false, + personalTargetedSpellImportantBorderAnimationOffsetX = 0, + personalTargetedSpellImportantBorderAnimationOffsetY = 0, + personalTargetedSpellImportantBorderAnimationParticles = 8, + personalTargetedSpellImportantBorderAnimationScale = 1, + personalTargetedSpellImportantBorderAnimationSidesAxis = "HORIZONTAL", + personalTargetedSpellImportantBorderAnimationThickness = 3, + personalTargetedSpellImportantBorderAnimationType = "PROC", + personalTargetedSpellImportantBorderBlendMode = "BLEND", + personalTargetedSpellImportantBorderColor = {r = 1, g = 0.8, b = 0, a = 1}, + personalTargetedSpellImportantBorderGradientDirection = "HORIZONTAL", + personalTargetedSpellImportantBorderGradientEndColor = {r = 0.5, g = 0.5, b = 0.5, a = 1}, + personalTargetedSpellImportantBorderGradientStartColor = {r = 0, g = 0, b = 0, a = 1}, + personalTargetedSpellImportantBorderInset = 3, + personalTargetedSpellImportantBorderShadowColor = {r = 0, g = 0, b = 0, a = 0.8}, + personalTargetedSpellImportantBorderShadowEnabled = false, + personalTargetedSpellImportantBorderShadowOffsetX = 1, + personalTargetedSpellImportantBorderShadowOffsetY = -1, + personalTargetedSpellImportantBorderShadowSize = 1, + personalTargetedSpellImportantBorderSize = 3, + personalTargetedSpellImportantBorderStyle = "SOLID", + personalTargetedSpellImportantBorderTexture = "SOLID", personalTargetedSpellImportantOnly = false, personalTargetedSpellInArena = true, personalTargetedSpellInBattlegrounds = true, @@ -2069,8 +2096,60 @@ DF.PartyDefaults = { -- Targeted Spells (on-frame) targetedSpellAlpha = 1, targetedSpellAnchor = "BOTTOM", + targetedSpellBorderAnimationColor = {r = 0.95, g = 0.95, b = 0.32, a = 1}, + targetedSpellBorderAnimationCornerLength = 10, + targetedSpellBorderAnimationFrequency = 0.25, + targetedSpellBorderAnimationInset = 0, + targetedSpellBorderAnimationLength = 8, + targetedSpellBorderAnimationMask = false, + targetedSpellBorderAnimationOffsetX = 0, + targetedSpellBorderAnimationOffsetY = 0, + targetedSpellBorderAnimationParticles = 8, + targetedSpellBorderAnimationScale = 1, + targetedSpellBorderAnimationSidesAxis = "HORIZONTAL", + targetedSpellBorderAnimationThickness = 3, + targetedSpellBorderAnimationType = "NONE", + targetedSpellBorderBlendMode = "BLEND", targetedSpellBorderColor = {r = 1, g = 0.3, b = 0}, + targetedSpellBorderGradientDirection = "HORIZONTAL", + targetedSpellBorderGradientEndColor = {r = 0.5, g = 0.5, b = 0.5, a = 1}, + targetedSpellBorderGradientStartColor = {r = 0, g = 0, b = 0, a = 1}, + targetedSpellBorderInset = 0, + targetedSpellBorderShadowColor = {r = 0, g = 0, b = 0, a = 0.8}, + targetedSpellBorderShadowEnabled = false, + targetedSpellBorderShadowOffsetX = 1, + targetedSpellBorderShadowOffsetY = -1, + targetedSpellBorderShadowSize = 1, targetedSpellBorderSize = 2, + targetedSpellBorderStyle = "SOLID", + targetedSpellBorderTexture = "SOLID", + targetedSpellImportantBorderAnimationColor = {r = 1, g = 0.8, b = 0, a = 1}, + targetedSpellImportantBorderAnimationCornerLength = 10, + targetedSpellImportantBorderAnimationFrequency = 0.25, + targetedSpellImportantBorderAnimationInset = 0, + targetedSpellImportantBorderAnimationLength = 8, + targetedSpellImportantBorderAnimationMask = false, + targetedSpellImportantBorderAnimationOffsetX = 0, + targetedSpellImportantBorderAnimationOffsetY = 0, + targetedSpellImportantBorderAnimationParticles = 8, + targetedSpellImportantBorderAnimationScale = 1, + targetedSpellImportantBorderAnimationSidesAxis = "HORIZONTAL", + targetedSpellImportantBorderAnimationThickness = 3, + targetedSpellImportantBorderAnimationType = "PROC", + targetedSpellImportantBorderBlendMode = "BLEND", + targetedSpellImportantBorderColor = {r = 1, g = 0.8, b = 0, a = 1}, + targetedSpellImportantBorderGradientDirection = "HORIZONTAL", + targetedSpellImportantBorderGradientEndColor = {r = 0.5, g = 0.5, b = 0.5, a = 1}, + targetedSpellImportantBorderGradientStartColor = {r = 0, g = 0, b = 0, a = 1}, + targetedSpellImportantBorderInset = 2, + targetedSpellImportantBorderShadowColor = {r = 0, g = 0, b = 0, a = 0.8}, + targetedSpellImportantBorderShadowEnabled = false, + targetedSpellImportantBorderShadowOffsetX = 1, + targetedSpellImportantBorderShadowOffsetY = -1, + targetedSpellImportantBorderShadowSize = 1, + targetedSpellImportantBorderSize = 3, + targetedSpellImportantBorderStyle = "SOLID", + targetedSpellImportantBorderTexture = "SOLID", targetedSpellDisableMouse = false, targetedSpellDurationColor = {r = 1, g = 1, b = 1}, targetedSpellDurationColorByTime = false, @@ -2148,7 +2227,7 @@ DF.PartyDefaults = { targetedListHideOwnCasts = false, targetedListHideOutOfCombat = true, targetedListHighlightImportant = true, - targetedListHighlightColor = {r = 1, g = 0.8, b = 0}, + targetedListHighlightColor = {r = 1, g = 0.8, b = 0, a = 1}, targetedListIconPosition = "LEFT", targetedListImportantOnly = false, targetedListInArena = true, @@ -2231,7 +2310,8 @@ DF.PartyDefaults = { testShowReducedMaxHealth = true, testShowSelection = false, testShowStatusIcons = true, - testShowTargetedSpell = false, + testShowTargetedSpell = true, + testShowPersonalTargeted = true, testShowClassPower = true, testShowAuraDesigner = false, testShowTextDesigner = true, @@ -3306,6 +3386,33 @@ DF.RaidDefaults = { personalTargetedSpellHighlightInset = 3, personalTargetedSpellHighlightSize = 3, personalTargetedSpellHighlightStyle = "glow", + personalTargetedSpellImportantBorderAnimationColor = {r = 1, g = 0.8, b = 0, a = 1}, + personalTargetedSpellImportantBorderAnimationCornerLength = 10, + personalTargetedSpellImportantBorderAnimationFrequency = 0.25, + personalTargetedSpellImportantBorderAnimationInset = 0, + personalTargetedSpellImportantBorderAnimationLength = 8, + personalTargetedSpellImportantBorderAnimationMask = false, + personalTargetedSpellImportantBorderAnimationOffsetX = 0, + personalTargetedSpellImportantBorderAnimationOffsetY = 0, + personalTargetedSpellImportantBorderAnimationParticles = 8, + personalTargetedSpellImportantBorderAnimationScale = 1, + personalTargetedSpellImportantBorderAnimationSidesAxis = "HORIZONTAL", + personalTargetedSpellImportantBorderAnimationThickness = 3, + personalTargetedSpellImportantBorderAnimationType = "PROC", + personalTargetedSpellImportantBorderBlendMode = "BLEND", + personalTargetedSpellImportantBorderColor = {r = 1, g = 0.8, b = 0, a = 1}, + personalTargetedSpellImportantBorderGradientDirection = "HORIZONTAL", + personalTargetedSpellImportantBorderGradientEndColor = {r = 0.5, g = 0.5, b = 0.5, a = 1}, + personalTargetedSpellImportantBorderGradientStartColor = {r = 0, g = 0, b = 0, a = 1}, + personalTargetedSpellImportantBorderInset = 3, + personalTargetedSpellImportantBorderShadowColor = {r = 0, g = 0, b = 0, a = 0.8}, + personalTargetedSpellImportantBorderShadowEnabled = false, + personalTargetedSpellImportantBorderShadowOffsetX = 1, + personalTargetedSpellImportantBorderShadowOffsetY = -1, + personalTargetedSpellImportantBorderShadowSize = 1, + personalTargetedSpellImportantBorderSize = 3, + personalTargetedSpellImportantBorderStyle = "SOLID", + personalTargetedSpellImportantBorderTexture = "SOLID", personalTargetedSpellImportantOnly = false, personalTargetedSpellInArena = true, personalTargetedSpellInBattlegrounds = true, @@ -3669,8 +3776,60 @@ DF.RaidDefaults = { -- Targeted Spells (on-frame) targetedSpellAlpha = 1, targetedSpellAnchor = "BOTTOM", + targetedSpellBorderAnimationColor = {r = 0.95, g = 0.95, b = 0.32, a = 1}, + targetedSpellBorderAnimationCornerLength = 10, + targetedSpellBorderAnimationFrequency = 0.25, + targetedSpellBorderAnimationInset = 0, + targetedSpellBorderAnimationLength = 8, + targetedSpellBorderAnimationMask = false, + targetedSpellBorderAnimationOffsetX = 0, + targetedSpellBorderAnimationOffsetY = 0, + targetedSpellBorderAnimationParticles = 8, + targetedSpellBorderAnimationScale = 1, + targetedSpellBorderAnimationSidesAxis = "HORIZONTAL", + targetedSpellBorderAnimationThickness = 3, + targetedSpellBorderAnimationType = "NONE", + targetedSpellBorderBlendMode = "BLEND", targetedSpellBorderColor = {r = 1, g = 0.3, b = 0}, + targetedSpellBorderGradientDirection = "HORIZONTAL", + targetedSpellBorderGradientEndColor = {r = 0.5, g = 0.5, b = 0.5, a = 1}, + targetedSpellBorderGradientStartColor = {r = 0, g = 0, b = 0, a = 1}, + targetedSpellBorderInset = 0, + targetedSpellBorderShadowColor = {r = 0, g = 0, b = 0, a = 0.8}, + targetedSpellBorderShadowEnabled = false, + targetedSpellBorderShadowOffsetX = 1, + targetedSpellBorderShadowOffsetY = -1, + targetedSpellBorderShadowSize = 1, targetedSpellBorderSize = 2, + targetedSpellBorderStyle = "SOLID", + targetedSpellBorderTexture = "SOLID", + targetedSpellImportantBorderAnimationColor = {r = 1, g = 0.8, b = 0, a = 1}, + targetedSpellImportantBorderAnimationCornerLength = 10, + targetedSpellImportantBorderAnimationFrequency = 0.25, + targetedSpellImportantBorderAnimationInset = 0, + targetedSpellImportantBorderAnimationLength = 8, + targetedSpellImportantBorderAnimationMask = false, + targetedSpellImportantBorderAnimationOffsetX = 0, + targetedSpellImportantBorderAnimationOffsetY = 0, + targetedSpellImportantBorderAnimationParticles = 8, + targetedSpellImportantBorderAnimationScale = 1, + targetedSpellImportantBorderAnimationSidesAxis = "HORIZONTAL", + targetedSpellImportantBorderAnimationThickness = 3, + targetedSpellImportantBorderAnimationType = "PROC", + targetedSpellImportantBorderBlendMode = "BLEND", + targetedSpellImportantBorderColor = {r = 1, g = 0.8, b = 0, a = 1}, + targetedSpellImportantBorderGradientDirection = "HORIZONTAL", + targetedSpellImportantBorderGradientEndColor = {r = 0.5, g = 0.5, b = 0.5, a = 1}, + targetedSpellImportantBorderGradientStartColor = {r = 0, g = 0, b = 0, a = 1}, + targetedSpellImportantBorderInset = 2, + targetedSpellImportantBorderShadowColor = {r = 0, g = 0, b = 0, a = 0.8}, + targetedSpellImportantBorderShadowEnabled = false, + targetedSpellImportantBorderShadowOffsetX = 1, + targetedSpellImportantBorderShadowOffsetY = -1, + targetedSpellImportantBorderShadowSize = 1, + targetedSpellImportantBorderSize = 3, + targetedSpellImportantBorderStyle = "SOLID", + targetedSpellImportantBorderTexture = "SOLID", targetedSpellDisableMouse = false, targetedSpellDurationColor = {r = 1, g = 1, b = 1}, targetedSpellDurationColorByTime = false, @@ -3736,7 +3895,8 @@ DF.RaidDefaults = { testShowReducedMaxHealth = true, testShowSelection = false, testShowStatusIcons = true, - testShowTargetedSpell = false, + testShowTargetedSpell = true, + testShowPersonalTargeted = true, testShowClassPower = true, testShowAuraDesigner = false, testShowTextDesigner = true, diff --git a/Core.lua b/Core.lua index a4b49abc..1a351b16 100644 --- a/Core.lua +++ b/Core.lua @@ -3543,6 +3543,63 @@ function DF:MigrateBorderInsetFold() end end +-- One-time: carry the old bespoke important-spell highlight settings +-- (targetedSpellHighlightStyle/Color/Size/Inset) into the new Important Spell +-- Border key set (targetedSpellImportantBorder*), which is a second DF.Border +-- gated by the Highlight-Important toggle. Defaults already match the old +-- defaults, so untouched profiles need nothing; this only preserves customised +-- highlights. Per-profile guarded. Style maps onto a DF.Border animation type. +function DF:MigrateTargetedSpellImportantBorder() + if not DandersFramesDB_v2 or not DandersFramesDB_v2.profiles then return end + local styleToAnim = { glow = "PROC", marchingAnts = "DF_DASH", pulse = "DF_PULSATE", + solidBorder = "NONE", none = "NONE" } + -- Copy a feature's old Highlight* keys into its new + -- ImportantBorder* set. Gated ONLY by the per-profile _…V1 flag in the + -- caller (so it runs exactly once); do NOT also guard on the new key being nil — + -- the ADDON_LOADED default-merge fills the new …ImportantBorder* keys before this + -- runs, so a nil-guard would never fire and the old highlight settings would be + -- lost. At first run the user can't have set the new keys yet, so overwriting the + -- just-merged defaults with their old highlight values is exactly the intent. + -- Mirrors MigrateBorderInsetFold. Shared by the group (targetedSpell) and personal + -- (personalTargetedSpell) sets. + local function mapHighlight(m, p) + if m[p.."HighlightColor"] ~= nil then + m[p.."ImportantBorderColor"] = m[p.."HighlightColor"] + m[p.."ImportantBorderAnimationColor"] = m[p.."HighlightColor"] + end + if m[p.."HighlightSize"] ~= nil then + m[p.."ImportantBorderSize"] = m[p.."HighlightSize"] + end + if m[p.."HighlightInset"] ~= nil then + m[p.."ImportantBorderInset"] = m[p.."HighlightInset"] + end + if m[p.."HighlightStyle"] ~= nil then + m[p.."ImportantBorderAnimationType"] = styleToAnim[m[p.."HighlightStyle"]] or "PROC" + end + end + for _, profile in pairs(DandersFramesDB_v2.profiles) do + if type(profile) == "table" then + -- Group/party Targeted Spells. Guarded independently from personal so a + -- profile already through this step still receives the personal one. + if not profile._tsImportantBorderV1 then + for _, modeKey in ipairs({ "party", "raid" }) do + local m = profile[modeKey] + if type(m) == "table" then mapHighlight(m, "targetedSpell") end + end + profile._tsImportantBorderV1 = true + end + -- Personal Targeted Spell. + if not profile._personalTsImportantBorderV1 then + for _, modeKey in ipairs({ "party", "raid" }) do + local m = profile[modeKey] + if type(m) == "table" then mapHighlight(m, "personalTargetedSpell") end + end + profile._personalTsImportantBorderV1 = true + end + end + end +end + -- The handler body is stored on DF as _MainEventDispatcher so the profiler -- can swap it for an instrumented version at runtime. The frame's actual -- script is a thin trampoline that calls through DF — re-binding takes @@ -5286,6 +5343,12 @@ DF._MainEventDispatcher = function(self, event, arg1) DF:MigrateDesignerPresets() end + -- Carry old important-spell highlight settings into the new + -- Important Spell Border key set (per-profile guarded, no-op once run). + if DF.MigrateTargetedSpellImportantBorder then + DF:MigrateTargetedSpellImportantBorder() + end + if DF.MigrateTextDesignerFromLegacy then DF:MigrateTextDesignerFromLegacy() end diff --git a/ExportCategories.lua b/ExportCategories.lua index bcd5b34f..a130b714 100644 --- a/ExportCategories.lua +++ b/ExportCategories.lua @@ -852,6 +852,58 @@ DF.ExportCategories = { "targetedSpellInArena", "targetedSpellInRaids", "targetedSpellInBattlegrounds", + "targetedSpellBorderAnimationColor", + "targetedSpellBorderAnimationCornerLength", + "targetedSpellBorderAnimationFrequency", + "targetedSpellBorderAnimationInset", + "targetedSpellBorderAnimationLength", + "targetedSpellBorderAnimationMask", + "targetedSpellBorderAnimationOffsetX", + "targetedSpellBorderAnimationOffsetY", + "targetedSpellBorderAnimationParticles", + "targetedSpellBorderAnimationScale", + "targetedSpellBorderAnimationSidesAxis", + "targetedSpellBorderAnimationThickness", + "targetedSpellBorderAnimationType", + "targetedSpellBorderBlendMode", + "targetedSpellBorderGradientDirection", + "targetedSpellBorderGradientEndColor", + "targetedSpellBorderGradientStartColor", + "targetedSpellBorderInset", + "targetedSpellBorderShadowColor", + "targetedSpellBorderShadowEnabled", + "targetedSpellBorderShadowOffsetX", + "targetedSpellBorderShadowOffsetY", + "targetedSpellBorderShadowSize", + "targetedSpellBorderStyle", + "targetedSpellBorderTexture", + "targetedSpellImportantBorderAnimationColor", + "targetedSpellImportantBorderAnimationCornerLength", + "targetedSpellImportantBorderAnimationFrequency", + "targetedSpellImportantBorderAnimationInset", + "targetedSpellImportantBorderAnimationLength", + "targetedSpellImportantBorderAnimationMask", + "targetedSpellImportantBorderAnimationOffsetX", + "targetedSpellImportantBorderAnimationOffsetY", + "targetedSpellImportantBorderAnimationParticles", + "targetedSpellImportantBorderAnimationScale", + "targetedSpellImportantBorderAnimationSidesAxis", + "targetedSpellImportantBorderAnimationThickness", + "targetedSpellImportantBorderAnimationType", + "targetedSpellImportantBorderBlendMode", + "targetedSpellImportantBorderColor", + "targetedSpellImportantBorderGradientDirection", + "targetedSpellImportantBorderGradientEndColor", + "targetedSpellImportantBorderGradientStartColor", + "targetedSpellImportantBorderInset", + "targetedSpellImportantBorderShadowColor", + "targetedSpellImportantBorderShadowEnabled", + "targetedSpellImportantBorderShadowOffsetX", + "targetedSpellImportantBorderShadowOffsetY", + "targetedSpellImportantBorderShadowSize", + "targetedSpellImportantBorderSize", + "targetedSpellImportantBorderStyle", + "targetedSpellImportantBorderTexture", -- Personal Targeted Spells "personalTargetedSpellEnabled", @@ -917,6 +969,33 @@ DF.ExportCategories = { "personalTargetedSpellInRaids", "personalTargetedSpellInArena", "personalTargetedSpellInBattlegrounds", + "personalTargetedSpellImportantBorderAnimationColor", + "personalTargetedSpellImportantBorderAnimationCornerLength", + "personalTargetedSpellImportantBorderAnimationFrequency", + "personalTargetedSpellImportantBorderAnimationInset", + "personalTargetedSpellImportantBorderAnimationLength", + "personalTargetedSpellImportantBorderAnimationMask", + "personalTargetedSpellImportantBorderAnimationOffsetX", + "personalTargetedSpellImportantBorderAnimationOffsetY", + "personalTargetedSpellImportantBorderAnimationParticles", + "personalTargetedSpellImportantBorderAnimationScale", + "personalTargetedSpellImportantBorderAnimationSidesAxis", + "personalTargetedSpellImportantBorderAnimationThickness", + "personalTargetedSpellImportantBorderAnimationType", + "personalTargetedSpellImportantBorderBlendMode", + "personalTargetedSpellImportantBorderColor", + "personalTargetedSpellImportantBorderGradientDirection", + "personalTargetedSpellImportantBorderGradientEndColor", + "personalTargetedSpellImportantBorderGradientStartColor", + "personalTargetedSpellImportantBorderInset", + "personalTargetedSpellImportantBorderShadowColor", + "personalTargetedSpellImportantBorderShadowEnabled", + "personalTargetedSpellImportantBorderShadowOffsetX", + "personalTargetedSpellImportantBorderShadowOffsetY", + "personalTargetedSpellImportantBorderShadowSize", + "personalTargetedSpellImportantBorderSize", + "personalTargetedSpellImportantBorderStyle", + "personalTargetedSpellImportantBorderTexture", -- Dispel Indicator "dispelOverlaySource", diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index 90a65f65..cbe5bc05 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -89,352 +89,6 @@ local MAX_HISTORY = 50 local eventFrame = CreateFrame("Frame") eventFrame:Hide() --- ============================================================ --- HIGHLIGHT STYLE ANIMATIONS --- ============================================================ - --- Animation settings for marching ants -local ANIM_SPEED = 40 -local DASH_LENGTH = 4 -local GAP_LENGTH = 4 -local PATTERN_LENGTH = DASH_LENGTH + GAP_LENGTH - --- Global animator for marching ants and pulse on targeted spell icons -local TargetedSpellAnimator = CreateFrame("Frame") -TargetedSpellAnimator.elapsed = 0 -TargetedSpellAnimator.frames = {} -TargetedSpellAnimator.pulseFrames = {} -TargetedSpellAnimator.hasWork = false -- Track whether any frames are registered - -local function TargetedSpellAnimator_OnUpdate(self, elapsed) - -- PERF TEST: Skip animations if disabled - if DF.PerfTest and not DF.PerfTest.enableAnimations then return end - - -- Marching ants animation - self.elapsed = self.elapsed + elapsed - local offset = (self.elapsed * ANIM_SPEED) % PATTERN_LENGTH - for highlightFrame in pairs(self.frames) do - if highlightFrame:IsShown() and highlightFrame.animBorder then - DF:UpdateTargetedSpellAnimatedBorder(highlightFrame, offset) - end - end - - -- Pulse animation (animates border texture alpha, not frame alpha) - for highlightFrame in pairs(self.pulseFrames) do - if highlightFrame:IsShown() and highlightFrame.pulseState and highlightFrame.glowBorder then - local state = highlightFrame.pulseState - state.elapsed = state.elapsed + elapsed - - -- Calculate current alpha based on time - local progress = state.elapsed / state.duration - if progress >= 1 then - -- Reverse direction - state.direction = -state.direction - state.elapsed = 0 - progress = 0 - end - - -- Smooth interpolation (smoothstep) - local smoothProgress = progress * progress * (3 - 2 * progress) - - local alpha - if state.direction == 1 then - alpha = state.minAlpha + (state.maxAlpha - state.minAlpha) * smoothProgress - else - alpha = state.maxAlpha - (state.maxAlpha - state.minAlpha) * smoothProgress - end - - -- Apply alpha to border textures - local border = highlightFrame.glowBorder - local r = highlightFrame.pulseR or 1 - local g = highlightFrame.pulseG or 0.8 - local b = highlightFrame.pulseB or 0 - - if border.top then border.top:SetColorTexture(r, g, b, alpha * 0.8) end - if border.bottom then border.bottom:SetColorTexture(r, g, b, alpha * 0.8) end - if border.left then border.left:SetColorTexture(r, g, b, alpha * 0.8) end - if border.right then border.right:SetColorTexture(r, g, b, alpha * 0.8) end - end - end -end - --- Check if animator has any work to do and enable/disable accordingly -local function TargetedSpellAnimator_UpdateState() - local hasWork = next(TargetedSpellAnimator.frames) or next(TargetedSpellAnimator.pulseFrames) - if hasWork and not TargetedSpellAnimator.hasWork then - TargetedSpellAnimator.hasWork = true - TargetedSpellAnimator:SetScript("OnUpdate", TargetedSpellAnimator_OnUpdate) - elseif not hasWork and TargetedSpellAnimator.hasWork then - TargetedSpellAnimator.hasWork = false - TargetedSpellAnimator:SetScript("OnUpdate", nil) - end -end - --- Export for test mode access -DF.TargetedSpellAnimator = TargetedSpellAnimator - --- Create dashes for one edge of the animated border -local function CreateEdgeDashes(parent, count) - local dashes = {} - for i = 1, count do - local dash = parent:CreateTexture(nil, "OVERLAY") - dash:SetColorTexture(1, 1, 1, 1) - dash:Hide() - dashes[i] = dash - end - return dashes -end - --- Initialize animated border on a highlight frame -local function InitAnimatedBorder(highlightFrame) - if highlightFrame.animBorder then return highlightFrame.animBorder end - highlightFrame.animBorder = { - topDashes = CreateEdgeDashes(highlightFrame, 15), - bottomDashes = CreateEdgeDashes(highlightFrame, 15), - leftDashes = CreateEdgeDashes(highlightFrame, 15), - rightDashes = CreateEdgeDashes(highlightFrame, 15), - } - return highlightFrame.animBorder -end -DF.InitAnimatedBorder = InitAnimatedBorder - --- Update animated border with current offset -function DF:UpdateTargetedSpellAnimatedBorder(highlightFrame, offset) - local border = highlightFrame.animBorder - if not border then return end - local thick = highlightFrame.animThickness or 2 - local r, g, b, a = highlightFrame.animR or 1, highlightFrame.animG or 0.8, highlightFrame.animB or 0, highlightFrame.animA or 1 - local frameWidth, frameHeight = highlightFrame:GetWidth(), highlightFrame:GetHeight() - if frameWidth <= 0 or frameHeight <= 0 then return end - - local function DrawHorizontalEdge(dashes, isTop, edgeOffset) - local numDashes = math.ceil(frameWidth / PATTERN_LENGTH) + 2 - for i, dash in ipairs(dashes) do dash:Hide() end - local startPos = -(edgeOffset % PATTERN_LENGTH) - for i = 1, numDashes do - local dashStart = startPos + (i - 1) * PATTERN_LENGTH - local dashEnd = dashStart + DASH_LENGTH - local visStart, visEnd = math.max(0, dashStart), math.min(frameWidth, dashEnd) - if visEnd > visStart and dashes[i] then - local dash = dashes[i] - dash:ClearAllPoints() - dash:SetSize(visEnd - visStart, thick) - if isTop then - dash:SetPoint("TOPLEFT", highlightFrame, "TOPLEFT", visStart, 0) - else - dash:SetPoint("BOTTOMLEFT", highlightFrame, "BOTTOMLEFT", visStart, 0) - end - dash:SetColorTexture(r, g, b, a) - dash:Show() - end - end - end - - local function DrawVerticalEdge(dashes, isRight, edgeOffset) - local numDashes = math.ceil(frameHeight / PATTERN_LENGTH) + 2 - for i, dash in ipairs(dashes) do dash:Hide() end - local startPos = -(edgeOffset % PATTERN_LENGTH) - for i = 1, numDashes do - local dashStart = startPos + (i - 1) * PATTERN_LENGTH - local dashEnd = dashStart + DASH_LENGTH - local visStart, visEnd = math.max(0, dashStart), math.min(frameHeight, dashEnd) - if visEnd > visStart and dashes[i] then - local dash = dashes[i] - dash:ClearAllPoints() - dash:SetSize(thick, visEnd - visStart) - if isRight then - dash:SetPoint("TOPRIGHT", highlightFrame, "TOPRIGHT", 0, -visStart) - else - dash:SetPoint("TOPLEFT", highlightFrame, "TOPLEFT", 0, -visStart) - end - dash:SetColorTexture(r, g, b, a) - dash:Show() - end - end - end - - -- Counter-clockwise marching ants - DrawHorizontalEdge(border.bottomDashes, false, offset) - DrawVerticalEdge(border.leftDashes, false, frameWidth + offset) - DrawHorizontalEdge(border.topDashes, true, frameWidth + frameHeight - offset) - DrawVerticalEdge(border.rightDashes, true, (2 * frameWidth) + frameHeight - offset) -end - --- Hide animated border -local function HideAnimatedBorder(highlightFrame) - if not highlightFrame.animBorder then return end - for _, dashes in pairs(highlightFrame.animBorder) do - for _, dash in ipairs(dashes) do dash:Hide() end - end -end -DF.HideAnimatedBorder = HideAnimatedBorder - --- Create solid border (4 edge textures) -local function InitSolidBorder(highlightFrame) - if highlightFrame.solidBorder then return highlightFrame.solidBorder end - highlightFrame.solidBorder = { - top = highlightFrame:CreateTexture(nil, "BORDER"), - bottom = highlightFrame:CreateTexture(nil, "BORDER"), - left = highlightFrame:CreateTexture(nil, "BORDER"), - right = highlightFrame:CreateTexture(nil, "BORDER"), - } - return highlightFrame.solidBorder -end -DF.InitSolidBorder = InitSolidBorder - --- Update solid border -local function UpdateSolidBorder(highlightFrame, thickness, r, g, b, a) - local border = highlightFrame.solidBorder - if not border then return end - - border.top:ClearAllPoints() - border.top:SetPoint("TOPLEFT", highlightFrame, "TOPLEFT", 0, 0) - border.top:SetPoint("TOPRIGHT", highlightFrame, "TOPRIGHT", 0, 0) - border.top:SetHeight(thickness) - border.top:SetColorTexture(r, g, b, a) - border.top:SetBlendMode("BLEND") - border.top:Show() - - border.bottom:ClearAllPoints() - border.bottom:SetPoint("BOTTOMLEFT", highlightFrame, "BOTTOMLEFT", 0, 0) - border.bottom:SetPoint("BOTTOMRIGHT", highlightFrame, "BOTTOMRIGHT", 0, 0) - border.bottom:SetHeight(thickness) - border.bottom:SetColorTexture(r, g, b, a) - border.bottom:SetBlendMode("BLEND") - border.bottom:Show() - - border.left:ClearAllPoints() - border.left:SetPoint("TOPLEFT", highlightFrame, "TOPLEFT", 0, -thickness) - border.left:SetPoint("BOTTOMLEFT", highlightFrame, "BOTTOMLEFT", 0, thickness) - border.left:SetWidth(thickness) - border.left:SetColorTexture(r, g, b, a) - border.left:SetBlendMode("BLEND") - border.left:Show() - - border.right:ClearAllPoints() - border.right:SetPoint("TOPRIGHT", highlightFrame, "TOPRIGHT", 0, -thickness) - border.right:SetPoint("BOTTOMRIGHT", highlightFrame, "BOTTOMRIGHT", 0, thickness) - border.right:SetWidth(thickness) - border.right:SetColorTexture(r, g, b, a) - border.right:SetBlendMode("BLEND") - border.right:Show() -end -DF.UpdateSolidBorder = UpdateSolidBorder - --- Hide solid border -local function HideSolidBorder(highlightFrame) - if not highlightFrame or not highlightFrame.solidBorder then return end - highlightFrame.solidBorder.top:Hide() - highlightFrame.solidBorder.bottom:Hide() - highlightFrame.solidBorder.left:Hide() - highlightFrame.solidBorder.right:Hide() -end -DF.HideSolidBorder = HideSolidBorder - --- Create glow border (4 edge textures with ADD blend mode for glow effect) -local function InitGlowBorder(highlightFrame) - if highlightFrame.glowBorder then return highlightFrame.glowBorder end - highlightFrame.glowBorder = { - top = highlightFrame:CreateTexture(nil, "OVERLAY"), - bottom = highlightFrame:CreateTexture(nil, "OVERLAY"), - left = highlightFrame:CreateTexture(nil, "OVERLAY"), - right = highlightFrame:CreateTexture(nil, "OVERLAY"), - } - -- Set ADD blend mode for glow effect - for _, tex in pairs(highlightFrame.glowBorder) do - tex:SetBlendMode("ADD") - end - return highlightFrame.glowBorder -end -DF.InitGlowBorder = InitGlowBorder - --- Update glow border -local function UpdateGlowBorder(highlightFrame, thickness, r, g, b, a) - local border = highlightFrame.glowBorder - if not border then return end - - border.top:ClearAllPoints() - border.top:SetPoint("TOPLEFT", highlightFrame, "TOPLEFT", 0, 0) - border.top:SetPoint("TOPRIGHT", highlightFrame, "TOPRIGHT", 0, 0) - border.top:SetHeight(thickness) - border.top:SetColorTexture(r, g, b, a) - border.top:SetBlendMode("ADD") - border.top:Show() - - border.bottom:ClearAllPoints() - border.bottom:SetPoint("BOTTOMLEFT", highlightFrame, "BOTTOMLEFT", 0, 0) - border.bottom:SetPoint("BOTTOMRIGHT", highlightFrame, "BOTTOMRIGHT", 0, 0) - border.bottom:SetHeight(thickness) - border.bottom:SetColorTexture(r, g, b, a) - border.bottom:SetBlendMode("ADD") - border.bottom:Show() - - border.left:ClearAllPoints() - border.left:SetPoint("TOPLEFT", highlightFrame, "TOPLEFT", 0, -thickness) - border.left:SetPoint("BOTTOMLEFT", highlightFrame, "BOTTOMLEFT", 0, thickness) - border.left:SetWidth(thickness) - border.left:SetColorTexture(r, g, b, a) - border.left:SetBlendMode("ADD") - border.left:Show() - - border.right:ClearAllPoints() - border.right:SetPoint("TOPRIGHT", highlightFrame, "TOPRIGHT", 0, -thickness) - border.right:SetPoint("BOTTOMRIGHT", highlightFrame, "BOTTOMRIGHT", 0, thickness) - border.right:SetWidth(thickness) - border.right:SetColorTexture(r, g, b, a) - border.right:SetBlendMode("ADD") - border.right:Show() -end -DF.UpdateGlowBorder = UpdateGlowBorder - --- Hide glow border -local function HideGlowBorder(highlightFrame) - if not highlightFrame or not highlightFrame.glowBorder then return end - highlightFrame.glowBorder.top:Hide() - highlightFrame.glowBorder.bottom:Hide() - highlightFrame.glowBorder.left:Hide() - highlightFrame.glowBorder.right:Hide() -end -DF.HideGlowBorder = HideGlowBorder - --- Create pulse animation group - animates border texture alpha, not frame alpha --- This prevents the animation from overriding SetAlphaFromBoolean on the frame -local function InitPulseAnimation(highlightFrame) - if highlightFrame.pulseAnim then return highlightFrame.pulseAnim end - - -- Store pulse state on the frame - highlightFrame.pulseState = { - elapsed = 0, - minAlpha = 0.3, - maxAlpha = 1.0, - duration = 0.5, - direction = 1, -- 1 = fading in, -1 = fading out - } - - -- Create a dummy animation group that we use to track if pulsing is active - local ag = {} - ag.isPlaying = false - ag.Play = function(self) - self.isPlaying = true - highlightFrame.pulseState.elapsed = 0 - highlightFrame.pulseState.direction = 1 - -- Register with animator - TargetedSpellAnimator.pulseFrames[highlightFrame] = true - TargetedSpellAnimator_UpdateState() - end - ag.Stop = function(self) - self.isPlaying = false - TargetedSpellAnimator.pulseFrames[highlightFrame] = nil - TargetedSpellAnimator_UpdateState() - end - ag.IsPlaying = function(self) - return self.isPlaying - end - - highlightFrame.pulseAnim = ag - return ag -end -DF.InitPulseAnimation = InitPulseAnimation @@ -631,39 +285,11 @@ local function CreateSingleIcon(parent, index) iconFrame:SetHitRectInsets(10000, 10000, 10000, 10000) container.iconFrame = iconFrame - -- Icon border - 4 edge textures (consistent with defensive/missing buff icons) + -- Icon border via the unified DF.Border backend (iconMode). The per-update + -- ApplyIconSettings drives BuildSpec + Apply; here we just allocate it. local defBorderSize = 2 - local borderLeft = iconFrame:CreateTexture(nil, "BACKGROUND") - borderLeft:SetPoint("TOPLEFT", 0, 0) - borderLeft:SetPoint("BOTTOMLEFT", 0, 0) - borderLeft:SetWidth(defBorderSize) - borderLeft:SetColorTexture(1, 0.3, 0, 1) - container.borderLeft = borderLeft - iconFrame.borderLeft = borderLeft - - local borderRight = iconFrame:CreateTexture(nil, "BACKGROUND") - borderRight:SetPoint("TOPRIGHT", 0, 0) - borderRight:SetPoint("BOTTOMRIGHT", 0, 0) - borderRight:SetWidth(defBorderSize) - borderRight:SetColorTexture(1, 0.3, 0, 1) - container.borderRight = borderRight - iconFrame.borderRight = borderRight - - local borderTop = iconFrame:CreateTexture(nil, "BACKGROUND") - borderTop:SetPoint("TOPLEFT", defBorderSize, 0) - borderTop:SetPoint("TOPRIGHT", -defBorderSize, 0) - borderTop:SetHeight(defBorderSize) - borderTop:SetColorTexture(1, 0.3, 0, 1) - container.borderTop = borderTop - iconFrame.borderTop = borderTop - - local borderBottom = iconFrame:CreateTexture(nil, "BACKGROUND") - borderBottom:SetPoint("BOTTOMLEFT", defBorderSize, 0) - borderBottom:SetPoint("BOTTOMRIGHT", -defBorderSize, 0) - borderBottom:SetHeight(defBorderSize) - borderBottom:SetColorTexture(1, 0.3, 0, 1) - container.borderBottom = borderBottom - iconFrame.borderBottom = borderBottom + iconFrame.border = DF.Border:New(iconFrame) + container.border = iconFrame.border -- Important spell highlight frame - use a frame so we can SetAlphaFromBoolean -- Set frame level ABOVE iconFrame so it renders on top when inset @@ -677,7 +303,11 @@ local function CreateSingleIcon(parent, index) container.highlightFrame = highlightFrame iconFrame.highlightFrame = highlightFrame - + -- DF.Border overlay for the important-spell highlight (Stage 2). highlightFrame + -- stays the secret-safe alpha gate; this DF.Border child draws the highlight. + container.highlightBorder = DF.Border:New(highlightFrame) + iconFrame.highlightBorder = container.highlightBorder + -- Icon texture - positioned with inset for border, with TexCoord cropping local icon = iconFrame:CreateTexture(nil, "ARTWORK") icon:SetPoint("TOPLEFT", defBorderSize, -defBorderSize) @@ -996,10 +626,9 @@ local function ApplyIconSettings(icon, db, spellID) local durationColor = db.targetedSpellDurationColor or {r = 1, g = 1, b = 1} local alpha = db.targetedSpellAlpha or 1.0 local highlightImportant = db.targetedSpellHighlightImportant ~= false - local highlightStyle = db.targetedSpellHighlightStyle or "glow" - local highlightColor = db.targetedSpellHighlightColor or {r = 1, g = 0.8, b = 0} - local highlightSize = db.targetedSpellHighlightSize or 3 - local highlightInset = db.targetedSpellHighlightInset or 0 + -- Important-spell highlight now reads the targetedSpellImportant* border keys + -- directly via BuildSpec (see the highlight block below); the old + -- targetedSpellHighlightStyle/Color/Size/Inset locals are retired here. local importantOnly = db.targetedSpellImportantOnly if durationOutline == "NONE" then durationOutline = "" end @@ -1026,132 +655,53 @@ local function ApplyIconSettings(icon, db, spellID) -- Important spell highlight if icon.highlightFrame then - -- Calculate position with inset (negative inset = larger, positive = smaller/inward) - local offset = borderSize + highlightSize - highlightInset - - -- Position the highlight frame + -- Important Spell Border: a second DF.Border (full toolkit via BuildSpec), + -- shown on important spells, gated by the Highlight-Important toggle + the + -- secret-safe isImportant alpha. Positioned just outside the base border; + -- sized/inset from the targetedSpellImportantBorder* keys. + -- Frame offset is inset-INDEPENDENT: it just clears the base border + the + -- highlight band. The Border Inset is owned by the engine (BuildSpec sets + -- spec.inset), which nudges the band symmetrically on all four edges, so it + -- stays centred as the slider moves. (The old `- hlInset` double-applied it.) + local hlSize = db.targetedSpellImportantBorderSize or 3 + local offset = borderSize + hlSize icon.highlightFrame:ClearAllPoints() icon.highlightFrame:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", -offset, offset) icon.highlightFrame:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", offset, -offset) - - -- Hide all highlight styles first - HideAnimatedBorder(icon.highlightFrame) - HideSolidBorder(icon.highlightFrame) - HideGlowBorder(icon.highlightFrame) - if icon.highlightFrame.pulseAnim then icon.highlightFrame.pulseAnim:Stop() end - TargetedSpellAnimator.frames[icon.highlightFrame] = nil - TargetedSpellAnimator_UpdateState() - - if highlightImportant and spellID and highlightStyle ~= "none" then + if highlightImportant and spellID and icon.highlightBorder then local isImportant = C_Spell.IsSpellImportant(spellID) - - if highlightStyle == "glow" then - -- Glow effect using edge borders with ADD blend mode - InitGlowBorder(icon.highlightFrame) - UpdateGlowBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 0.8) - icon.highlightFrame:Show() - icon.highlightFrame:SetAlphaFromBoolean(isImportant) - - elseif highlightStyle == "marchingAnts" then - -- Animated marching ants border - InitAnimatedBorder(icon.highlightFrame) - icon.highlightFrame.animThickness = math.max(1, highlightSize) - icon.highlightFrame.animR = highlightColor.r - icon.highlightFrame.animG = highlightColor.g - icon.highlightFrame.animB = highlightColor.b - icon.highlightFrame.animA = 1 - icon.highlightFrame:Show() - icon.highlightFrame:SetAlphaFromBoolean(isImportant) - TargetedSpellAnimator.frames[icon.highlightFrame] = true - TargetedSpellAnimator_UpdateState() - - elseif highlightStyle == "solidBorder" then - -- Solid colored border (4 edge textures, no fill) - InitSolidBorder(icon.highlightFrame) - UpdateSolidBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 1) - icon.highlightFrame:Show() - icon.highlightFrame:SetAlphaFromBoolean(isImportant) - - elseif highlightStyle == "pulse" then - -- Pulsing glow using edge borders with ADD blend - InitGlowBorder(icon.highlightFrame) - UpdateGlowBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 0.8) - InitPulseAnimation(icon.highlightFrame) - -- Store color for pulse animation to use - icon.highlightFrame.pulseR = highlightColor.r - icon.highlightFrame.pulseG = highlightColor.g - icon.highlightFrame.pulseB = highlightColor.b - icon.highlightFrame:Show() - icon.highlightFrame:SetAlphaFromBoolean(isImportant) - icon.highlightFrame.pulseAnim:Play() - end + local spec = DF.Border:BuildSpec(db, "targetedSpellImportant", { iconMode = true }) + spec.enabled = true + DF.Border:Apply(icon.highlightBorder, spec) + icon.highlightFrame:Show() + icon.highlightFrame:SetAlphaFromBoolean(isImportant) else + if icon.highlightBorder then DF.Border:Apply(icon.highlightBorder, { enabled = false }) end icon.highlightFrame:Hide() end end - - -- Border - -- Border - 4 edge textures (consistent with defensive/missing buff icons) - if showBorder then - if icon.borderLeft then - icon.borderLeft:SetColorTexture(borderColor.r, borderColor.g, borderColor.b, 1) - icon.borderLeft:SetWidth(borderSize) - icon.borderLeft:Show() - end - if icon.borderRight then - icon.borderRight:SetColorTexture(borderColor.r, borderColor.g, borderColor.b, 1) - icon.borderRight:SetWidth(borderSize) - icon.borderRight:Show() - end - if icon.borderTop then - icon.borderTop:SetColorTexture(borderColor.r, borderColor.g, borderColor.b, 1) - icon.borderTop:SetHeight(borderSize) - icon.borderTop:ClearAllPoints() - icon.borderTop:SetPoint("TOPLEFT", borderSize, 0) - icon.borderTop:SetPoint("TOPRIGHT", -borderSize, 0) - icon.borderTop:Show() - end - if icon.borderBottom then - icon.borderBottom:SetColorTexture(borderColor.r, borderColor.g, borderColor.b, 1) - icon.borderBottom:SetHeight(borderSize) - icon.borderBottom:ClearAllPoints() - icon.borderBottom:SetPoint("BOTTOMLEFT", borderSize, 0) - icon.borderBottom:SetPoint("BOTTOMRIGHT", -borderSize, 0) - icon.borderBottom:Show() - end - - -- Adjust icon texture position for border - if icon.icon then - icon.icon:ClearAllPoints() - icon.icon:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", borderSize, -borderSize) - icon.icon:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -borderSize, borderSize) - end - - -- Adjust cooldown to match icon texture - if icon.cooldown then - icon.cooldown:ClearAllPoints() - icon.cooldown:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", borderSize, -borderSize) - icon.cooldown:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -borderSize, borderSize) - end - else - -- Hide all border edges - if icon.borderLeft then icon.borderLeft:Hide() end - if icon.borderRight then icon.borderRight:Hide() end - if icon.borderTop then icon.borderTop:Hide() end - if icon.borderBottom then icon.borderBottom:Hide() end - - -- Full size icon when no border + + -- Border via the unified DF.Border backend (iconMode) — parity with Personal + -- Targeted Spell / Targeted List. BuildSpec reads the targetedSpell* keys; + -- spec.size carries the (pixel-perfect-adjusted) thickness, and the art + + -- cooldown inset by that thickness when the border is shown. + if icon.border then + local spec = DF.Border:BuildSpec(db, "targetedSpell", { iconMode = true }) + spec.enabled = showBorder + spec.size = borderSize + DF.Border:Apply(icon.border, spec) + end + do + local ai = showBorder and borderSize or 0 if icon.icon then icon.icon:ClearAllPoints() - icon.icon:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", 0, 0) - icon.icon:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", 0, 0) + icon.icon:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", ai, -ai) + icon.icon:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -ai, ai) end - - -- Adjust cooldown to match if icon.cooldown then icon.cooldown:ClearAllPoints() - icon.cooldown:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", 0, 0) - icon.cooldown:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", 0, 0) + icon.cooldown:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", ai, -ai) + icon.cooldown:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -ai, ai) end end @@ -1336,14 +886,7 @@ function DF:HideTargetedSpellIcon(frame, casterKey, skipInterruptAnim) end if icon.highlightFrame then icon.highlightFrame:Hide() - -- Clean up animator reference - TargetedSpellAnimator.frames[icon.highlightFrame] = nil - TargetedSpellAnimator_UpdateState() - HideAnimatedBorder(icon.highlightFrame) - HideSolidBorder(icon.highlightFrame) - if icon.highlightFrame.pulseAnim then - icon.highlightFrame.pulseAnim:Stop() - end + if icon.highlightBorder then DF.Border:Apply(icon.highlightBorder, { enabled = false }) end end if icon.icon then icon.icon:SetDesaturated(false) @@ -1386,14 +929,7 @@ function DF:HideAllTargetedSpells(frame) end if icon.highlightFrame then icon.highlightFrame:Hide() - -- Clean up animator reference - TargetedSpellAnimator.frames[icon.highlightFrame] = nil - TargetedSpellAnimator_UpdateState() - HideAnimatedBorder(icon.highlightFrame) - HideSolidBorder(icon.highlightFrame) - if icon.highlightFrame.pulseAnim then - icon.highlightFrame.pulseAnim:Stop() - end + if icon.highlightBorder then DF.Border:Apply(icon.highlightBorder, { enabled = false }) end end if icon.icon then icon.icon:SetDesaturated(false) @@ -2435,7 +1971,9 @@ local function CreatePersonalIcon(index) highlightFrame:EnableMouse(false) highlightFrame:SetHitRectInsets(10000, 10000, 10000, 10000) icon.highlightFrame = highlightFrame - + -- DF.Border overlay for the important-spell highlight (Stage 2). + icon.highlightBorder = DF.Border:New(highlightFrame) + -- Icon texture - positioned with default 2px inset so it lines up -- with the border at creation time. ApplyPersonalIconSettings -- recomputes the inset from the db's BorderSize on every render @@ -2577,10 +2115,9 @@ local function ApplyPersonalIconSettings(icon, db, spellID) local durationY = db.personalTargetedSpellDurationY or 0 local durationColor = db.personalTargetedSpellDurationColor or {r = 1, g = 1, b = 1} local highlightImportant = db.personalTargetedSpellHighlightImportant ~= false - local highlightStyle = db.personalTargetedSpellHighlightStyle or "glow" - local highlightColor = db.personalTargetedSpellHighlightColor or {r = 1, g = 0.8, b = 0} - local highlightSize = db.personalTargetedSpellHighlightSize or 3 - local highlightInset = db.personalTargetedSpellHighlightInset or 0 + -- Important-spell highlight reads the personalTargetedSpellImportant* border keys + -- directly via BuildSpec (see the highlight block below); the old + -- personalTargetedSpellHighlightStyle/Color/Size/Inset locals are retired here. local importantOnly = db.personalTargetedSpellImportantOnly if durationOutline == "NONE" then durationOutline = "" end @@ -2602,68 +2139,25 @@ local function ApplyPersonalIconSettings(icon, db, spellID) end end - -- Important spell highlight + -- Important Spell Border: a second DF.Border (full toolkit via BuildSpec), + -- shown on important spells, gated by the Highlight-Important toggle + the + -- secret-safe isImportant alpha. Frame offset is inset-INDEPENDENT — the + -- engine's spec.inset owns the inset symmetrically (keeps it centred). if icon.highlightFrame then - -- Calculate position with inset (negative inset = larger, positive = smaller/inward) - local offset = borderSize + highlightSize - highlightInset - - -- Position the highlight frame + local hlSize = db.personalTargetedSpellImportantBorderSize or 3 + local offset = borderSize + hlSize icon.highlightFrame:ClearAllPoints() icon.highlightFrame:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", -offset, offset) icon.highlightFrame:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", offset, -offset) - - -- Hide all highlight styles first - HideAnimatedBorder(icon.highlightFrame) - HideSolidBorder(icon.highlightFrame) - HideGlowBorder(icon.highlightFrame) - if icon.highlightFrame.pulseAnim then icon.highlightFrame.pulseAnim:Stop() end - TargetedSpellAnimator.frames[icon.highlightFrame] = nil - TargetedSpellAnimator_UpdateState() - - if highlightImportant and spellID and highlightStyle ~= "none" then + if highlightImportant and spellID and icon.highlightBorder then local isImportant = C_Spell.IsSpellImportant(spellID) - - if highlightStyle == "glow" then - -- Glow effect using edge borders with ADD blend mode - InitGlowBorder(icon.highlightFrame) - UpdateGlowBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 0.8) - icon.highlightFrame:Show() - icon.highlightFrame:SetAlphaFromBoolean(isImportant) - - elseif highlightStyle == "marchingAnts" then - -- Animated marching ants border - InitAnimatedBorder(icon.highlightFrame) - icon.highlightFrame.animThickness = math.max(1, highlightSize) - icon.highlightFrame.animR = highlightColor.r - icon.highlightFrame.animG = highlightColor.g - icon.highlightFrame.animB = highlightColor.b - icon.highlightFrame.animA = 1 - icon.highlightFrame:Show() - icon.highlightFrame:SetAlphaFromBoolean(isImportant) - TargetedSpellAnimator.frames[icon.highlightFrame] = true - TargetedSpellAnimator_UpdateState() - - elseif highlightStyle == "solidBorder" then - -- Solid colored border (4 edge textures, no fill) - InitSolidBorder(icon.highlightFrame) - UpdateSolidBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 1) - icon.highlightFrame:Show() - icon.highlightFrame:SetAlphaFromBoolean(isImportant) - - elseif highlightStyle == "pulse" then - -- Pulsing glow using edge borders with ADD blend - InitGlowBorder(icon.highlightFrame) - UpdateGlowBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 0.8) - InitPulseAnimation(icon.highlightFrame) - -- Store color for pulse animation to use - icon.highlightFrame.pulseR = highlightColor.r - icon.highlightFrame.pulseG = highlightColor.g - icon.highlightFrame.pulseB = highlightColor.b - icon.highlightFrame:Show() - icon.highlightFrame:SetAlphaFromBoolean(isImportant) - icon.highlightFrame.pulseAnim:Play() - end + local spec = DF.Border:BuildSpec(db, "personalTargetedSpellImportant", { iconMode = true }) + spec.enabled = true + DF.Border:Apply(icon.highlightBorder, spec) + icon.highlightFrame:Show() + icon.highlightFrame:SetAlphaFromBoolean(isImportant) else + if icon.highlightBorder then DF.Border:Apply(icon.highlightBorder, { enabled = false }) end icon.highlightFrame:Hide() end end @@ -2920,14 +2414,7 @@ function DF:HidePersonalTargetedSpellIcon(casterKey, immediate, fromTimer) icon:Hide() if icon.highlightFrame then icon.highlightFrame:Hide() - -- Clean up animator reference - TargetedSpellAnimator.frames[icon.highlightFrame] = nil - TargetedSpellAnimator_UpdateState() - HideAnimatedBorder(icon.highlightFrame) - HideSolidBorder(icon.highlightFrame) - if icon.highlightFrame.pulseAnim then - icon.highlightFrame.pulseAnim:Stop() - end + if icon.highlightBorder then DF.Border:Apply(icon.highlightBorder, { enabled = false }) end end icon.interruptOverlay:Hide() if icon.icon then @@ -2959,14 +2446,7 @@ function DF:HideAllPersonalTargetedSpells() icon:Hide() if icon.highlightFrame then icon.highlightFrame:Hide() - -- Clean up animator reference - TargetedSpellAnimator.frames[icon.highlightFrame] = nil - TargetedSpellAnimator_UpdateState() - HideAnimatedBorder(icon.highlightFrame) - HideSolidBorder(icon.highlightFrame) - if icon.highlightFrame.pulseAnim then - icon.highlightFrame.pulseAnim:Stop() - end + if icon.highlightBorder then DF.Border:Apply(icon.highlightBorder, { enabled = false }) end end icon.interruptOverlay:Hide() if icon.icon then @@ -5159,10 +4639,14 @@ local function TargetedList_ApplyBarContent(bar, activeRec) if bar.highlightFrame then if party and party.targetedListHighlightImportant then local hc = party.targetedListHighlightColor or {r=1, g=0.8, b=0} - if DF.InitGlowBorder then DF.InitGlowBorder(bar.highlightFrame) end - if DF.UpdateGlowBorder then - DF.UpdateGlowBorder(bar.highlightFrame, 2, hc.r, hc.g, hc.b, 0.8) - end + bar.highlightBorder = bar.highlightBorder or DF.Border:New(bar.highlightFrame) + -- Static solid highlight border (no animation): the Targeted List + -- highlight has only an enable toggle + colour, and historically was + -- a calm glow — a flashing PROC here reads as broken. + DF.Border:Apply(bar.highlightBorder, { + enabled = true, size = 2, inset = 0, style = "SOLID", + color = { r = hc.r, g = hc.g, b = hc.b, a = hc.a or 1 }, + }) bar.highlightFrame:Show() if isTest and activeRec.testIsImportant ~= nil then -- Clean bool — use SetShown directly @@ -6215,10 +5699,14 @@ function DF:LightweightUpdateTargetedListHighlightColor() if not db then return end local hc = db.targetedListHighlightColor or {r=1, g=0.8, b=0} for _, bar in pairs(casterToBar) do - if bar.highlightFrame and bar.highlightFrame:IsShown() then - if DF.UpdateGlowBorder then - DF.UpdateGlowBorder(bar.highlightFrame, 2, hc.r, hc.g, hc.b, 0.8) - end + if bar.highlightFrame and bar.highlightFrame:IsShown() and bar.highlightBorder then + -- Static solid highlight border (no animation): the Targeted List + -- highlight has only an enable toggle + colour, and historically was + -- a calm glow — a flashing PROC here reads as broken. + DF.Border:Apply(bar.highlightBorder, { + enabled = true, size = 2, inset = 0, style = "SOLID", + color = { r = hc.r, g = hc.g, b = hc.b, a = hc.a or 1 }, + }) end end end diff --git a/Frames/Border.lua b/Frames/Border.lua index fd95c649..ddc6004d 100644 --- a/Frames/Border.lua +++ b/Frames/Border.lua @@ -245,6 +245,11 @@ function Border:BuildSpec(dbTable, prefix, ctx) mask = dbTable[k("BorderAnimationMask")], sidesAxis = dbTable[k("BorderAnimationSidesAxis")], cornerLength = dbTable[k("BorderAnimationCornerLength")], + -- PROC only: play the one-shot "proc start" flash on each start. + -- Opt-in (default off) because PROC is used here as a CONTINUOUS + -- border animation that re-applies often; the flash is a one-shot + -- effect and re-fires/doubles on rapid re-apply when enabled. + procStart = dbTable[k("BorderAnimationProcStart")], } end -- Icon consumers (ctx.iconMode) frame the art with an OUTWARD band — the @@ -883,6 +888,21 @@ function Border:StopAnimation(border) -- mid-upgrade who might still have a glow running on the old frame.) local anchor = border.anchorTo or border local function stopAll(t) + -- Reset a reused ProcGlow frame's textures BEFORE releasing it. + -- LCG's pool resetter only hides the frame + clears the reference; + -- it leaves the ProcStart (big start-flash) / ProcLoop (small loop) + -- texture visibility intact. So a frame re-acquired from the pool + -- could come back with BOTH textures showing — the double glow + -- (one inset, one further out) seen on alternate re-applies. Stop + -- the animation groups and clear both textures so the next Acquire + -- (and its OnShow) starts from a clean state. + local pg = t and t["_ProcGlow" .. key] + if pg then + if pg.ProcStartAnim then pg.ProcStartAnim:Stop() end + if pg.ProcLoopAnim then pg.ProcLoopAnim:Stop() end + if pg.ProcStart then pg.ProcStart:SetAlpha(0); pg.ProcStart:Hide() end + if pg.ProcLoop then pg.ProcLoop:SetAlpha(0); pg.ProcLoop:Hide() end + end if LCG.PixelGlow_Stop then LCG.PixelGlow_Stop(t, key) end if LCG.AutoCastGlow_Stop then LCG.AutoCastGlow_Stop(t, key) end if LCG.ButtonGlow_Stop then LCG.ButtonGlow_Stop(t) end @@ -955,6 +975,7 @@ local function animSpecHash(anim) tostring(anim.inset), tostring(anim.offsetX), tostring(anim.offsetY), tostring(anim.mask), tostring(anim.sidesAxis), tostring(anim.cornerLength), + tostring(anim.procStart), tostring(cr), tostring(cg), tostring(cb), tostring(ca), }, "|") end @@ -1051,10 +1072,17 @@ function Border:StartAnimation(border, spec) -- other effects' Frequency control (cycles per second). local duration = (anim.frequency and anim.frequency > 0) and (1 / anim.frequency) or 1 + -- The one-shot "proc start" flash is OPT-IN (anim.procStart). + -- Default off: PROC is used here as a CONTINUOUS border animation + -- that re-applies often, and the flash (begins large, shrinks to + -- the border) re-fires on every re-apply — on rapid re-toggle the + -- big start texture lingers alongside the loop, rendering two + -- glows at two sizes. With it on, normal single-cast use plays it + -- cleanly; only rapid re-toggling can double it. LCG.ProcGlow_Start(target, { color = color, duration = duration, - startAnim = true, + startAnim = anim.procStart and true or false, key = key, }) end @@ -1414,6 +1442,13 @@ function Border:Apply(border, spec) e:Show() end end + -- Thickness 0 collapses the edges to zero width/height; hide them + -- outright so a degenerate texture can't leave a hairline. Animation + -- overlays are separate frames and keep running (they're gated by the + -- border being shown, not by thickness). + if size <= 0 then + for _, e in ipairs(edges) do if e then e:Hide() end end + end else -- Texture mode: a BackdropTemplate child with the LSM border edgeFile. -- spec.blendMode is intentionally ignored here — see doc above. @@ -1423,10 +1458,18 @@ function Border:Apply(border, spec) border.bd:SetAllPoints(border) end local bd = border.bd - bd:SetBackdrop({ edgeFile = edgeFile, edgeSize = (size > 0 and size) or 1 }) - bd:SetBackdropBorderColor(cr, cg, cb, ca) - bd:Show() - border.activeTexture = texture + -- Thickness 0 = no border: hide the backdrop instead of clamping the + -- edge to 1px (parity with the solid/gradient path above). The + -- animation overlay is a separate frame and keeps running. + if size <= 0 then + bd:Hide() + border.activeTexture = nil + else + bd:SetBackdrop({ edgeFile = edgeFile, edgeSize = size }) + bd:SetBackdropBorderColor(cr, cg, cb, ca) + bd:Show() + border.activeTexture = texture + end end -- Drop shadow: solid 4-edge ring, lazy-created, parented next to the diff --git a/GUI/GUI.lua b/GUI/GUI.lua index 94585b64..9e0de359 100644 --- a/GUI/GUI.lua +++ b/GUI/GUI.lua @@ -3879,6 +3879,13 @@ function GUI:CreateAnimationControls(group, dbTable, animPrefix, opts) dbTable, aKey("Mask"), fullUpdate), 30) w.animationMask.hideOn = hideUnless(pulsateOnly) + -- PROC only: opt in to the one-shot "proc start" flash (off by default — + -- see ProcGlow_Start in Frames/Border.lua for why it's not on for a + -- continuous border animation). + w.animationProcStart = group:AddWidget(GUI:CreateCheckbox(parent, L["Proc Start Flash"], + dbTable, aKey("ProcStart"), fullUpdate), 30) + w.animationProcStart.hideOn = hideUnless({ PROC = 1 }) + w.animationSidesAxis = group:AddWidget(GUI:CreateDropdown(parent, L["Sides Axis"], { HORIZONTAL = L["Horizontal"], VERTICAL = L["Vertical"] }, dbTable, aKey("SidesAxis"), fullUpdate), 55) @@ -3924,11 +3931,18 @@ function GUI:CreateBorderControls(group, dbTable, prefix, opts) local w = {} - w.show = group:AddWidget(GUI:CreateCheckbox(parent, L["Show Border"], dbTable, showKey, function() - if refreshStates then refreshStates() end - fullUpdate() - end), 30) - w.show.hideOn = hideShow + -- opts.noShowToggle: suppress the built-in "Show Border" checkbox for + -- consumers that gate the whole border on an external toggle (e.g. the + -- Targeted Spells "Highlight Important Spells" master). With the checkbox + -- gone, showKey stays nil so hideOff() reduces to hideShow() — the toolkit + -- shows/hides purely on the external hideWhen. + if not opts.noShowToggle then + w.show = group:AddWidget(GUI:CreateCheckbox(parent, L["Show Border"], dbTable, showKey, function() + if refreshStates then refreshStates() end + fullUpdate() + end), 30) + w.show.hideOn = hideShow + end -- Slider label reads "Border Thickness" (more meaningful than "Size") but -- the underlying db key stays `BorderSize` and spec.size in the diff --git a/Locales/enUS.lua b/Locales/enUS.lua index 577203a8..2db39033 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -1824,6 +1824,7 @@ L["Animation Offset X"] = true L["Animation Offset Y"] = true L["Animations run per-border and may impact FPS in larger raids. Use sparingly on high-priority alerts."] = true L["Pulsate Backing Frame"] = true +L["Proc Start Flash"] = true L["Proc"] = true L["Animation Length"] = true L["Animation Particles"] = true diff --git a/Options/Options.lua b/Options/Options.lua index af85f6cb..63d1696a 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -6519,7 +6519,7 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) lightColors = function() DF:LightweightUpdateMissingBuffBorderColor() end, refreshStates = function() self:RefreshStates() end, hideWhen = function(d) return not d.missingBuffIconEnabled end, - sizeMin = 1, sizeMax = 6, sizeStep = 1, + sizeMin = 0, sizeMax = 6, sizeStep = 1, -- 0 = animation-only (no solid edge) }) borderGroup.hideOn = HideMissingBuffOptions Add(borderGroup, nil, 1) @@ -6872,6 +6872,8 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) tsIconSize.disableOn = HideTargetedSpellOptions local tsScale = sizeGroup:AddWidget(GUI:CreateSlider(self.child, L["Scale"], 0.5, 4.0, 0.1, db, "targetedSpellScale", FullUpdate, TargetedSpellLightweightUpdate, true), 55) tsScale.disableOn = HideTargetedSpellOptions + local tsAlpha = sizeGroup:AddWidget(GUI:CreateSlider(self.child, L["Alpha"], 0.0, 1.0, 0.05, db, "targetedSpellAlpha", FullUpdate, TargetedSpellLightweightUpdate, true), 55) + tsAlpha.disableOn = HideTargetedSpellOptions local tsSpacing = sizeGroup:AddWidget(GUI:CreateSlider(self.child, L["Spacing"], 0, 10, 1, db, "targetedSpellSpacing", FullUpdate, TargetedSpellLightweightUpdate, true), 55) tsSpacing.disableOn = HideTargetedSpellOptions local tsMaxIcons = sizeGroup:AddWidget(GUI:CreateSlider(self.child, L["Max Icons"], 1, 10, 1, db, "targetedSpellMaxIcons", FullUpdate, TargetedSpellLightweightUpdate, true), 55) @@ -6890,19 +6892,21 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) -- Border Group (col1) local borderGroup = GUI:CreateSettingsGroup(self.child, 260) borderGroup:AddWidget(GUI:CreateHeader(self.child, L["Border"]), 40) - local tsAlpha = borderGroup:AddWidget(GUI:CreateSlider(self.child, L["Alpha"], 0.0, 1.0, 0.05, db, "targetedSpellAlpha", FullUpdate, TargetedSpellLightweightUpdate, true), 55) - tsAlpha.disableOn = HideTargetedSpellOptions - local hideSwipe = borderGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Hide Cooldown Swipe"], db, "targetedSpellHideSwipe", FullUpdate), 30) - hideSwipe.disableOn = HideTargetedSpellOptions - local tsShowBorder = borderGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Show Border"], db, "targetedSpellShowBorder", function() - self:RefreshStates() - FullUpdate() - end), 30) - tsShowBorder.disableOn = HideTargetedSpellOptions - local tsBorderSize = borderGroup:AddWidget(GUI:CreateSlider(self.child, L["Border Size"], 0, 8, 1, db, "targetedSpellBorderSize", FullUpdate, TargetedSpellLightweightUpdate, true), 55) - tsBorderSize.disableOn = HideBorderOptions - local tsColor = borderGroup:AddWidget(GUI:CreateColorPicker(self.child, L["Border Color"], db, "targetedSpellBorderColor", false, FullUpdate), 35) - tsColor.disableOn = HideBorderOptions + -- Full DF.Border toolkit (matches Personal Targeted Spell): Show Border, + -- Size, Style/Gradient, Colour, Alpha, Inset, Blend Mode, Shadow, Animate. + -- BuildSpec in ApplyIconSettings already reads every targetedSpell* border + -- key, so these controls light up the whole engine. + GUI:CreateBorderControls(borderGroup, db, "targetedSpell", { + parent = self.child, + include = { alpha = true, inset = true, blendMode = true, + gradient = true, shadow = true, animate = true }, + fullUpdate = FullUpdate, + lightUpdate = TargetedSpellLightweightUpdate, + lightColors = FullUpdate, + refreshStates = function() self:RefreshStates() end, + hideWhen = HideTargetedSpellOptions, + sizeMin = 0, sizeMax = 8, sizeStep = 1, + }) AddToSection(borderGroup, nil, 1) -- Duration Group (col2) @@ -6913,6 +6917,10 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) FullUpdate() end), 30) showDur.disableOn = HideTargetedSpellOptions + -- The cooldown swipe is the radial cooldown sweep on the icon (independent + -- of the numeric duration text), so it's gated only on the feature itself. + local hideSwipe = durationGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Hide Cooldown Swipe"], db, "targetedSpellHideSwipe", FullUpdate), 30) + hideSwipe.disableOn = HideTargetedSpellOptions local tsDurFont = durationGroup:AddWidget(GUI:CreateFontDropdown(self.child, L["Font"], db, "targetedSpellDurationFont", FullUpdate), 55) tsDurFont.disableOn = HideTargetedDurationOptions local tsDurScale = durationGroup:AddWidget(GUI:CreateSlider(self.child, L["Scale"], 0.5, 2.0, 0.05, db, "targetedSpellDurationScale", FullUpdate, TargetedSpellLightweightUpdate, true), 55) @@ -6937,8 +6945,7 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) currentSection = importantSection local function HideHighlightOptions(d) return not d.targetedSpellEnabled or not d.targetedSpellHighlightImportant end - local highlightStyleOptions = { glow = L["Glow"], marchingAnts = L["Marching Ants"], solidBorder = L["Solid Border"], pulse = L["Pulse"], none = L["None"] } - + local highlightGroup = GUI:CreateSettingsGroup(self.child, 260) highlightGroup:AddWidget(GUI:CreateHeader(self.child, L["Highlight Settings"]), 40) local tsHighlightImportant = highlightGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Highlight Important Spells"], db, "targetedSpellHighlightImportant", function() @@ -6946,14 +6953,20 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) FullUpdate() end), 30) tsHighlightImportant.disableOn = HideTargetedSpellOptions - local tsHighlightStyle = highlightGroup:AddWidget(GUI:CreateDropdown(self.child, L["Highlight Style"], highlightStyleOptions, db, "targetedSpellHighlightStyle", FullUpdate), 55) - tsHighlightStyle.disableOn = HideHighlightOptions - local tsHighlightColor = highlightGroup:AddWidget(GUI:CreateColorPicker(self.child, L["Highlight Color"], db, "targetedSpellHighlightColor", false, FullUpdate), 35) - tsHighlightColor.disableOn = HideHighlightOptions - local tsHighlightSize = highlightGroup:AddWidget(GUI:CreateSlider(self.child, L["Border Thickness"], 1, 8, 1, db, "targetedSpellHighlightSize", FullUpdate, TargetedSpellLightweightUpdate, true), 55) - tsHighlightSize.disableOn = HideHighlightOptions - local tsHighlightInset = highlightGroup:AddWidget(GUI:CreateSlider(self.child, L["Border Inset"], -4, 8, 1, db, "targetedSpellHighlightInset", FullUpdate, TargetedSpellLightweightUpdate, true), 55) - tsHighlightInset.disableOn = HideHighlightOptions + -- Important Spell Border: the highlight on its own DF.Border (full toolkit), + -- gated by the Highlight Important Spells toggle above. + GUI:CreateBorderControls(highlightGroup, db, "targetedSpellImportant", { + parent = self.child, + noShowToggle = true, -- the Highlight Important Spells checkbox is the gate + include = { alpha = true, inset = true, blendMode = true, + gradient = true, shadow = true, animate = true }, + fullUpdate = FullUpdate, + lightUpdate = TargetedSpellLightweightUpdate, + lightColors = FullUpdate, + refreshStates = function() self:RefreshStates() end, + hideWhen = HideHighlightOptions, + sizeMin = 0, sizeMax = 8, sizeStep = 1, + }) AddToSection(highlightGroup, nil, 1) currentSection = nil @@ -7245,13 +7258,13 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) end), 30) tlHighlight.disableOn = HideTLOptions local function HideHighlightOptions(d) return not d.targetedListEnabled or not d.targetedListHighlightImportant end - local tlHighlightColor = colorGroup:AddWidget(GUI:CreateColorPicker(self.child, L["Highlight Color"], db, "targetedListHighlightColor", false, TargetedListUpdate, function() if DF.LightweightUpdateTargetedListHighlightColor then DF:LightweightUpdateTargetedListHighlightColor() end end, true), 35) + local tlHighlightColor = colorGroup:AddWidget(GUI:CreateColorPicker(self.child, L["Highlight Color"], db, "targetedListHighlightColor", true, TargetedListUpdate, function() if DF.LightweightUpdateTargetedListHighlightColor then DF:LightweightUpdateTargetedListHighlightColor() end end, true), 35) tlHighlightColor.disableOn = HideHighlightOptions local tlResetColors = colorGroup:AddWidget(GUI:CreateButton(self.child, L["Reset Colors to Default"], 200, 24, function() db.targetedListInterruptibleColor = {r = 1, g = 0.494, b = 0.137, a = 1} db.targetedListUninterruptibleColor = {r = 0.8, g = 0.302, b = 0.302, a = 1} db.targetedListSelfTargetColor = {r = 0.02, g = 0.776, b = 0.4, a = 0.2} - db.targetedListHighlightColor = {r = 1, g = 0.8, b = 0} + db.targetedListHighlightColor = {r = 1, g = 0.8, b = 0, a = 1} db.targetedListBorderColor = {r = 0.18, g = 0.18, b = 0.18, a = 1} -- Refresh color swatches if tlInterColor.UpdateSwatch then tlInterColor:UpdateSwatch() end @@ -7492,6 +7505,8 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) ptsSize.disableOn = HidePersonalOptions local ptsScale = sizeGroup:AddWidget(GUI:CreateSlider(self.child, L["Scale"], 0.5, 2.0, 0.05, db, "personalTargetedSpellScale", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) ptsScale.disableOn = HidePersonalOptions + local ptsAlpha = sizeGroup:AddWidget(GUI:CreateSlider(self.child, L["Alpha"], 0.0, 1.0, 0.05, db, "personalTargetedSpellAlpha", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) + ptsAlpha.disableOn = HidePersonalOptions local ptsSpacing = sizeGroup:AddWidget(GUI:CreateSlider(self.child, L["Spacing"], 0, 20, 1, db, "personalTargetedSpellSpacing", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) ptsSpacing.disableOn = HidePersonalOptions local ptsMaxIcons = sizeGroup:AddWidget(GUI:CreateSlider(self.child, L["Max Icons"], 1, 10, 1, db, "personalTargetedSpellMaxIcons", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) @@ -7518,14 +7533,8 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) -- Targeted = spells targeting you). Skipped: offset (icon has its -- own positioning), classColor / roleColor (spell alert, not unit -- identity), colorByTime / colorByType (no aura-state context). - -- The Icon Alpha (personalTargetedSpellAlpha) and Cooldown Swipe - -- toggles stay on this group — they're not border-related. local borderGroup = GUI:CreateSettingsGroup(self.child, 260) borderGroup:AddWidget(GUI:CreateHeader(self.child, L["Border"]), 40) - local ptsAlpha = borderGroup:AddWidget(GUI:CreateSlider(self.child, L["Alpha"], 0.0, 1.0, 0.05, db, "personalTargetedSpellAlpha", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) - ptsAlpha.disableOn = HidePersonalOptions - local ptsSwipe = borderGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Show Cooldown Swipe"], db, "personalTargetedSpellShowSwipe", PersonalTargetedUpdate), 30) - ptsSwipe.disableOn = HidePersonalOptions GUI:CreateBorderControls(borderGroup, db, "personalTargetedSpell", { parent = self.child, include = { alpha = true, inset = true, blendMode = true, @@ -7535,7 +7544,7 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) lightColors = PersonalTargetedUpdate, refreshStates = function() self:RefreshStates() end, hideWhen = function(d) return not d.personalTargetedSpellEnabled end, - sizeMin = 1, sizeMax = 5, sizeStep = 1, + sizeMin = 0, sizeMax = 5, sizeStep = 1, -- 0 = animation-only (no solid edge) }) AddToSection(borderGroup, nil, 1) @@ -7547,6 +7556,10 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) PersonalTargetedUpdate() end), 30) ptsDuration.disableOn = HidePersonalOptions + -- The cooldown swipe is the radial cooldown sweep on the icon (independent + -- of the numeric duration text), so it's gated only on the feature itself. + local ptsSwipe = durationGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Show Cooldown Swipe"], db, "personalTargetedSpellShowSwipe", PersonalTargetedUpdate), 30) + ptsSwipe.disableOn = HidePersonalOptions local ptsDurFont = durationGroup:AddWidget(GUI:CreateFontDropdown(self.child, L["Font"], db, "personalTargetedSpellDurationFont", PersonalTargetedUpdate), 55) ptsDurFont.disableOn = HidePersonalDurationOptions local ptsDurScale = durationGroup:AddWidget(GUI:CreateSlider(self.child, L["Scale"], 0.5, 2.0, 0.1, db, "personalTargetedSpellDurationScale", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) @@ -7570,20 +7583,29 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) local highlightSection = Add(GUI:CreateCollapsibleSection(self.child, L["Important Spells"], true), 36, "both") currentSection = highlightSection - local personalHighlightStyleOptions = { glow = L["Glow"], marchingAnts = L["Marching Ants"], solidBorder = L["Solid Border"], pulse = L["Pulse"], none = L["None"] } - + local function HidePersonalHighlightOptions(d) return not d.personalTargetedSpellEnabled or not d.personalTargetedSpellHighlightImportant end + local highlightGroup = GUI:CreateSettingsGroup(self.child, 260) highlightGroup:AddWidget(GUI:CreateHeader(self.child, L["Highlight Settings"]), 40) - local ptsHighlight = highlightGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Highlight Important Spells"], db, "personalTargetedSpellHighlightImportant", PersonalTargetedUpdate), 30) + local ptsHighlight = highlightGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Highlight Important Spells"], db, "personalTargetedSpellHighlightImportant", function() + self:RefreshStates() + PersonalTargetedUpdate() + end), 30) ptsHighlight.disableOn = HidePersonalOptions - local ptsHighlightStyle = highlightGroup:AddWidget(GUI:CreateDropdown(self.child, L["Highlight Style"], personalHighlightStyleOptions, db, "personalTargetedSpellHighlightStyle", PersonalTargetedUpdate), 55) - ptsHighlightStyle.disableOn = function(d) return not d.personalTargetedSpellEnabled or not d.personalTargetedSpellHighlightImportant end - local ptsHighlightColor = highlightGroup:AddWidget(GUI:CreateColorPicker(self.child, L["Highlight Color"], db, "personalTargetedSpellHighlightColor", false, PersonalTargetedUpdate), 35) - ptsHighlightColor.disableOn = function(d) return not d.personalTargetedSpellEnabled or not d.personalTargetedSpellHighlightImportant end - local ptsHighlightSize = highlightGroup:AddWidget(GUI:CreateSlider(self.child, L["Border Thickness"], 1, 6, 1, db, "personalTargetedSpellHighlightSize", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) - ptsHighlightSize.disableOn = function(d) return not d.personalTargetedSpellEnabled or not d.personalTargetedSpellHighlightImportant end - local ptsHighlightInset = highlightGroup:AddWidget(GUI:CreateSlider(self.child, L["Border Inset"], -4, 8, 1, db, "personalTargetedSpellHighlightInset", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) - ptsHighlightInset.disableOn = function(d) return not d.personalTargetedSpellEnabled or not d.personalTargetedSpellHighlightImportant end + -- Important Spell Border: the highlight on its own DF.Border (full toolkit), + -- gated by the Highlight Important Spells toggle above. + GUI:CreateBorderControls(highlightGroup, db, "personalTargetedSpellImportant", { + parent = self.child, + noShowToggle = true, -- the Highlight Important Spells checkbox is the gate + include = { alpha = true, inset = true, blendMode = true, + gradient = true, shadow = true, animate = true }, + fullUpdate = PersonalTargetedUpdate, + lightUpdate = PersonalTargetedUpdate, + lightColors = PersonalTargetedUpdate, + refreshStates = function() self:RefreshStates() end, + hideWhen = HidePersonalHighlightOptions, + sizeMin = 0, sizeMax = 8, sizeStep = 1, + }) AddToSection(highlightGroup, nil, 1) currentSection = nil diff --git a/Profile.lua b/Profile.lua index 634532d2..32726cf5 100644 --- a/Profile.lua +++ b/Profile.lua @@ -292,6 +292,10 @@ function DF:SetProfile(name) DF:MigrateDesignerPresets() end + if DF.MigrateTargetedSpellImportantBorder then + DF:MigrateTargetedSpellImportantBorder() + end + if DF.MigrateTextDesignerFromLegacy then DF:MigrateTextDesignerFromLegacy() end diff --git a/TestMode/TestMode.lua b/TestMode/TestMode.lua index 5e452284..945d53bd 100644 --- a/TestMode/TestMode.lua +++ b/TestMode/TestMode.lua @@ -4832,6 +4832,8 @@ function DF:ApplyTestPreset(preset) db.testShowMissingBuff = false db.testShowExternalDef = false db.testShowTargetedList = false + db.testShowTargetedSpell = false + db.testShowPersonalTargeted = false db.testShowBossDebuffs = false db.testShowStatusIcons = true elseif preset == "COMBAT" then @@ -4843,6 +4845,8 @@ function DF:ApplyTestPreset(preset) db.testShowMissingBuff = false db.testShowExternalDef = false db.testShowTargetedList = false + db.testShowTargetedSpell = false + db.testShowPersonalTargeted = false db.testShowBossDebuffs = true db.testShowStatusIcons = true elseif preset == "HEALER" then @@ -4854,6 +4858,8 @@ function DF:ApplyTestPreset(preset) db.testShowMissingBuff = true db.testShowExternalDef = true db.testShowTargetedList = true + db.testShowTargetedSpell = true + db.testShowPersonalTargeted = true db.testShowBossDebuffs = true db.testShowStatusIcons = true elseif preset == "FULL" then @@ -4865,6 +4871,8 @@ function DF:ApplyTestPreset(preset) db.testShowMissingBuff = true db.testShowExternalDef = true db.testShowTargetedList = true + db.testShowTargetedSpell = true + db.testShowPersonalTargeted = true db.testShowStatusIcons = true end @@ -5497,11 +5505,7 @@ function DF:UpdateTestTargetedSpell(frame, testData) local spacing = db.targetedSpellSpacing or 2 local frameLevel = db.targetedSpellFrameLevel or 0 local highlightImportant = db.targetedSpellHighlightImportant ~= false - local highlightStyle = db.targetedSpellHighlightStyle or "glow" - local highlightColor = db.targetedSpellHighlightColor or {r = 1, g = 0.8, b = 0} - local highlightSize = db.targetedSpellHighlightSize or 3 - local highlightInset = db.targetedSpellHighlightInset or 0 - + if durationOutline == "NONE" then durationOutline = "" end -- Apply pixel perfect @@ -5616,139 +5620,59 @@ function DF:UpdateTestTargetedSpell(frame, testData) end end - -- Apply important spell highlight (show on important spells, including interrupted ones) + -- Apply important spell highlight (show on important spells, including + -- interrupted ones). Mirrors live ApplyIconSettings: the highlight is + -- its own DF.Border (full toolkit via BuildSpec on the + -- targetedSpellImportant* keys), gated by the Highlight Important + -- Spells master toggle. Overlay lazily allocated. if icon.highlightFrame then - -- Calculate position with inset - local offset = borderSize + highlightSize - highlightInset - - -- Hide all styles first - always do this if icon.highlight then icon.highlight:Hide() end - if DF.HideSolidBorder then DF.HideSolidBorder(icon.highlightFrame) end - if DF.HideGlowBorder then DF.HideGlowBorder(icon.highlightFrame) end - if DF.HideAnimatedBorder then DF.HideAnimatedBorder(icon.highlightFrame) end - if icon.highlightFrame.pulseAnim then icon.highlightFrame.pulseAnim:Stop() end - -- Remove from animator - if DF.TargetedSpellAnimator then - DF.TargetedSpellAnimator.frames[icon.highlightFrame] = nil - end - - -- Only show if highlighting is enabled, spell is important, and style is not "none" - local shouldShowHighlight = highlightImportant and spell.isImportant and highlightStyle and highlightStyle ~= "none" - - if shouldShowHighlight then + icon.highlightBorder = icon.highlightBorder or DF.Border:New(icon.highlightFrame) + if highlightImportant and spell.isImportant then + -- Inset owned by the engine (BuildSpec spec.inset); frame + -- offset is inset-independent so the band stays centred. + local hlSize = db.targetedSpellImportantBorderSize or 3 + local offset = borderSize + hlSize icon.highlightFrame:ClearAllPoints() icon.highlightFrame:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", -offset, offset) icon.highlightFrame:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", offset, -offset) icon.highlightFrame:SetAlpha(1) icon.highlightFrame:Show() - - -- Apply style using edge-based borders - if highlightStyle == "glow" then - -- Glow border with ADD blend mode - if DF.InitGlowBorder and DF.UpdateGlowBorder then - DF.InitGlowBorder(icon.highlightFrame) - DF.UpdateGlowBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 0.8) - end - elseif highlightStyle == "pulse" then - -- Pulsing glow with animation - if DF.InitGlowBorder and DF.UpdateGlowBorder and DF.InitPulseAnimation then - DF.InitGlowBorder(icon.highlightFrame) - DF.UpdateGlowBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 0.8) - DF.InitPulseAnimation(icon.highlightFrame) - -- Store color for pulse animation to use - icon.highlightFrame.pulseR = highlightColor.r - icon.highlightFrame.pulseG = highlightColor.g - icon.highlightFrame.pulseB = highlightColor.b - if icon.highlightFrame.pulseAnim then - icon.highlightFrame.pulseAnim:Play() - end - end - elseif highlightStyle == "marchingAnts" then - -- Animated marching ants border - if DF.InitAnimatedBorder and DF.TargetedSpellAnimator then - DF.InitAnimatedBorder(icon.highlightFrame) - icon.highlightFrame.animThickness = math.max(1, highlightSize) - icon.highlightFrame.animR = highlightColor.r - icon.highlightFrame.animG = highlightColor.g - icon.highlightFrame.animB = highlightColor.b - icon.highlightFrame.animA = 1 - DF.TargetedSpellAnimator.frames[icon.highlightFrame] = true - end - elseif highlightStyle == "solidBorder" then - -- Solid border - if DF.InitSolidBorder and DF.UpdateSolidBorder then - DF.InitSolidBorder(icon.highlightFrame) - DF.UpdateSolidBorder(icon.highlightFrame, highlightSize, highlightColor.r, highlightColor.g, highlightColor.b, 1) - end - end + local spec = DF.Border:BuildSpec(db, "targetedSpellImportant", { iconMode = true }) + spec.enabled = true + DF.Border:Apply(icon.highlightBorder, spec) else + DF.Border:Apply(icon.highlightBorder, { enabled = false }) icon.highlightFrame:Hide() end end - -- Apply border settings - 4 edge textures (consistent with live code) - if showBorder then - if icon.borderLeft then - icon.borderLeft:SetColorTexture(borderColor.r, borderColor.g, borderColor.b, 1) - icon.borderLeft:SetWidth(borderSize) - icon.borderLeft:Show() - end - if icon.borderRight then - icon.borderRight:SetColorTexture(borderColor.r, borderColor.g, borderColor.b, 1) - icon.borderRight:SetWidth(borderSize) - icon.borderRight:Show() - end - if icon.borderTop then - icon.borderTop:SetColorTexture(borderColor.r, borderColor.g, borderColor.b, 1) - icon.borderTop:SetHeight(borderSize) - icon.borderTop:ClearAllPoints() - icon.borderTop:SetPoint("TOPLEFT", borderSize, 0) - icon.borderTop:SetPoint("TOPRIGHT", -borderSize, 0) - icon.borderTop:Show() - end - if icon.borderBottom then - icon.borderBottom:SetColorTexture(borderColor.r, borderColor.g, borderColor.b, 1) - icon.borderBottom:SetHeight(borderSize) - icon.borderBottom:ClearAllPoints() - icon.borderBottom:SetPoint("BOTTOMLEFT", borderSize, 0) - icon.borderBottom:SetPoint("BOTTOMRIGHT", -borderSize, 0) - icon.borderBottom:Show() - end - - -- Adjust icon texture position for border - if icon.icon then - icon.icon:ClearAllPoints() - icon.icon:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", borderSize, -borderSize) - icon.icon:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -borderSize, borderSize) - end - - -- Adjust cooldown to match - if icon.cooldown then - icon.cooldown:ClearAllPoints() - icon.cooldown:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", borderSize, -borderSize) - icon.cooldown:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -borderSize, borderSize) - end - else - -- Hide all border edges - if icon.borderLeft then icon.borderLeft:Hide() end - if icon.borderRight then icon.borderRight:Hide() end - if icon.borderTop then icon.borderTop:Hide() end - if icon.borderBottom then icon.borderBottom:Hide() end - - -- Full size icon when no border + -- Border via the unified DF.Border backend (iconMode), mirroring live + -- ApplyIconSettings. The live create path replaced the old 4 edge + -- textures with a DF.Border, so test mode must render the same way. + icon.border = icon.border or DF.Border:New(icon.iconFrame) + local bspec = DF.Border:BuildSpec(db, "targetedSpell", { iconMode = true }) + bspec.enabled = showBorder + bspec.size = borderSize + DF.Border:Apply(icon.border, bspec) + do + local ai = showBorder and borderSize or 0 if icon.icon then icon.icon:ClearAllPoints() - icon.icon:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", 0, 0) - icon.icon:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", 0, 0) + icon.icon:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", ai, -ai) + icon.icon:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -ai, ai) end - - -- Adjust cooldown to match if icon.cooldown then icon.cooldown:ClearAllPoints() - icon.cooldown:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", 0, 0) - icon.cooldown:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", 0, 0) + icon.cooldown:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", ai, -ai) + icon.cooldown:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -ai, ai) end end + -- Hide any legacy edge textures left on a pooled icon. + if icon.borderLeft then icon.borderLeft:Hide() end + if icon.borderRight then icon.borderRight:Hide() end + if icon.borderTop then icon.borderTop:Hide() end + if icon.borderBottom then icon.borderBottom:Hide() end icon:SetAlpha(alpha) icon:Show() @@ -5846,9 +5770,9 @@ function DF:UpdateAllTestTargetedSpell() if not frame then return end local db = DF:GetFrameDB(frame) - -- Auto-show the preview whenever the feature is enabled (matches the - -- personal-display test behaviour); raid frames keep it force-disabled. - if db.targetedSpellEnabled then + -- Show the preview when the feature is enabled AND the test-panel + -- Targeted Spells toggle is on; raid frames keep it force-disabled. + if db.targetedSpellEnabled and db.testShowTargetedSpell ~= false then DF:UpdateTestTargetedSpell(frame, testData) else -- Hide all icons and their highlights (new multi-icon system) @@ -5858,14 +5782,7 @@ function DF:UpdateAllTestTargetedSpell() -- Also hide pinned frame if it exists if icon.highlightFrame then icon.highlightFrame:Hide() - -- Stop any animations - if icon.highlightFrame.pulseAnim and icon.highlightFrame.pulseAnim:IsPlaying() then - icon.highlightFrame.pulseAnim:Stop() - end - -- Unregister from animator - if DF.TargetedSpellAnimator then - DF.TargetedSpellAnimator.frames[icon.highlightFrame] = nil - end + if icon.highlightBorder then DF.Border:Apply(icon.highlightBorder, { enabled = false }) end end end end @@ -5887,7 +5804,7 @@ function DF:UpdateAllTestTargetedSpell() -- Update personal targeted spells display in test mode local db = DF:GetDB() - if db.personalTargetedSpellEnabled and DF.ShowTestPersonalTargetedSpells then + if db.personalTargetedSpellEnabled and db.testShowPersonalTargeted ~= false and DF.ShowTestPersonalTargetedSpells then DF:ShowTestPersonalTargetedSpells() elseif DF.HideTestPersonalTargetedSpells then DF:HideTestPersonalTargetedSpells() @@ -5913,7 +5830,7 @@ function DF:UpdateAllTestTargetedSpell() -- Also show personal targeted spells in raid test mode local db = DF:GetDB() - if db.personalTargetedSpellEnabled and DF.ShowTestPersonalTargetedSpells then + if db.personalTargetedSpellEnabled and db.testShowPersonalTargeted ~= false and DF.ShowTestPersonalTargetedSpells then DF:ShowTestPersonalTargetedSpells() elseif DF.HideTestPersonalTargetedSpells then DF:HideTestPersonalTargetedSpells() @@ -6760,6 +6677,14 @@ function DF:CreateTestPanel() panel.animTargetedListCheck = secIndicators:AddCheckbox("Animate Targeted List", "testAnimateTargetedList", function() if DF.testMode or DF.raidTestMode then DF:UpdateAllTestTargetedList() end end) + -- The (new fingerprint) Targeted Spells icons + the Personal Targeted display. + -- UpdateAllTestTargetedSpell drives BOTH previews, so both share it. + panel.showTargetedSpellCheck = secIndicators:AddCheckbox("Targeted Spells", "testShowTargetedSpell", function() + if DF.testMode or DF.raidTestMode then DF:UpdateAllTestTargetedSpell() end + end) + panel.showPersonalTargetedCheck = secIndicators:AddCheckbox("Personal Targeted", "testShowPersonalTargeted", function() + if DF.testMode or DF.raidTestMode then DF:UpdateAllTestTargetedSpell() end + end) -- One unified "Icons" toggle for the whole status/role/leader icon set in test -- mode (was split into "Status / Ready" + "Role / Leader"). Keyed on -- testShowStatusIcons; the role/leader render gate reads the same key. @@ -6919,6 +6844,8 @@ function DF:CreateTestPanel() self.showExternalDefCheck:SetChecked(db.testShowExternalDef) self.showTargetedListCheck:SetChecked(db.testShowTargetedList) self.animTargetedListCheck:SetChecked(db.testAnimateTargetedList) + self.showTargetedSpellCheck:SetChecked(db.testShowTargetedSpell ~= false) + self.showPersonalTargetedCheck:SetChecked(db.testShowPersonalTargeted ~= false) self.showStatusIconsCheck:SetChecked(db.testShowStatusIcons ~= false) self.showSelectionCheck:SetChecked(db.testShowSelection) self.showAggroCheck:SetChecked(db.testShowAggro)