From ac293f1c09c5740c7b3c5953ea3a0f88755086ed Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 20:47:15 +0100 Subject: [PATCH 01/34] Targeted Spells: base border -> DF.Border (Stage 1) Migrate the group per-icon base border off the 4 hand-rolled edge textures (icon.borderLeft/Right/Top/Bottom) onto the unified DF.Border backend, matching Personal Targeted Spell / Targeted List and the rest of the Stage 4.x borders. Create path now allocates iconFrame.border = DF.Border:New(iconFrame); the per-update ApplyIconSettings drives BuildSpec(db, "targetedSpell", {iconMode}) + Apply (spec.enabled = showBorder, spec.size = the pixel-perfect thickness), and insets the icon art + cooldown swipe by that thickness when shown. Reads the existing targetedSpell* keys, so the current GUI works unchanged; the full border toolkit (style/alpha/gradient/inset/animation) is now available for a later CreateBorderControls GUI pass. The "important spell" highlight is untouched in this stage (Stage 2). --- Features/TargetedSpells.lua | 109 +++++++----------------------------- 1 file changed, 20 insertions(+), 89 deletions(-) diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index 90a65f65..c0128e63 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -631,39 +631,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 @@ -1090,68 +1062,27 @@ local function ApplyIconSettings(icon, db, spellID) 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 From 4a5e98e5bc74ac1c0da507868c08c41bee75c589 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 20:56:40 +0100 Subject: [PATCH 02/34] Targeted Spells: highlight border -> DF.Border overlay (Stage 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retire the bespoke important-spell highlight (InitGlow/Solid/AnimatedBorder + the marching-ants animator + pulse AnimationGroup) in the live render path, replacing it with a second DF.Border overlay on the existing highlightFrame. The highlightFrame stays as the secret-safe alpha gate (SetAlphaFromBoolean on the possibly-secret IsSpellImportant result); only its border content moves to DF.Border. The highlightStyle enum is preserved (no saved migration) and mapped at render time onto a DF.Border animation: glow→PROC, marchingAnts→DF_DASH, pulse→ DF_PULSATE, solidBorder→static SOLID. Both render sites (group + personal) and both create paths now allocate/drive iconFrame.highlightBorder / icon.highlightBorder. The bespoke Init/Update/Hide helpers stay defined for now — TestMode's preview still calls them; they migrate + get deleted in Stage 3. Teardown calls on them are safe no-ops (the new overlay hides with its container). --- Features/TargetedSpells.lua | 150 +++++++++++++----------------------- 1 file changed, 52 insertions(+), 98 deletions(-) diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index c0128e63..34504e47 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -649,7 +649,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) @@ -1006,58 +1010,32 @@ local function ApplyIconSettings(icon, db, spellID) 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 + -- Highlight border via the unified DF.Border backend. The style maps onto + -- a DF.Border animation (glow→PROC, marchingAnts→DF_DASH, pulse→DF_PULSATE, + -- solidBorder→static). The highlightFrame stays as the secret-safe alpha + -- gate (SetAlphaFromBoolean), positioned just outside the base border. + if highlightImportant and spellID and highlightStyle ~= "none" 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) - + local hSize = math.max(1, highlightSize) + local spec = { + enabled = true, + size = hSize, + inset = 0, + style = "SOLID", + color = { r = highlightColor.r, g = highlightColor.g, b = highlightColor.b, a = 1 }, + } + if highlightStyle == "marchingAnts" then + spec.animation = { type = "DF_DASH", thickness = hSize, color = spec.color } 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() + spec.animation = { type = "DF_PULSATE", color = spec.color } + elseif highlightStyle == "glow" then + spec.animation = { type = "PROC", color = spec.color } end + 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 @@ -2366,7 +2344,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 @@ -2543,58 +2523,32 @@ local function ApplyPersonalIconSettings(icon, db, spellID) 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 + -- Highlight border via the unified DF.Border backend. The style maps onto + -- a DF.Border animation (glow→PROC, marchingAnts→DF_DASH, pulse→DF_PULSATE, + -- solidBorder→static). The highlightFrame stays as the secret-safe alpha + -- gate (SetAlphaFromBoolean), positioned just outside the base border. + if highlightImportant and spellID and highlightStyle ~= "none" 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) - + local hSize = math.max(1, highlightSize) + local spec = { + enabled = true, + size = hSize, + inset = 0, + style = "SOLID", + color = { r = highlightColor.r, g = highlightColor.g, b = highlightColor.b, a = 1 }, + } + if highlightStyle == "marchingAnts" then + spec.animation = { type = "DF_DASH", thickness = hSize, color = spec.color } 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() + spec.animation = { type = "DF_PULSATE", color = spec.color } + elseif highlightStyle == "glow" then + spec.animation = { type = "PROC", color = spec.color } end + 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 From 723c3f7e34729c616f9d50bfb377091ecabe27b2 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 21:02:18 +0100 Subject: [PATCH 03/34] Targeted Spells: migrate TestMode preview + teardown off bespoke helpers (Stage 3a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestMode's targeted-spell preview highlight now uses the same DF.Border overlay + style→animation mapping as the live path (overlay lazily allocated on the preview highlightFrame). The active-icon teardown drops its bespoke HideAnimated/SolidBorder + TargetedSpellAnimator calls for a single DF.Border:Apply{enabled=false}. Nothing now references the bespoke Init/Update/Hide Glow/Solid/Animated helpers, the marching-ants TargetedSpellAnimator driver, or InitPulseAnimation — they are dead code, deleted in Stage 3b. (Highlights.lua / Dispel.lua have their own unrelated InitAnimatedBorder / pulseAnim and are untouched.) --- Features/TargetedSpells.lua | 9 +---- TestMode/TestMode.lua | 66 +++++++++---------------------------- 2 files changed, 17 insertions(+), 58 deletions(-) diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index 34504e47..8211be5c 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -1245,14 +1245,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) diff --git a/TestMode/TestMode.lua b/TestMode/TestMode.lua index 5e452284..5287a021 100644 --- a/TestMode/TestMode.lua +++ b/TestMode/TestMode.lua @@ -5621,67 +5621,33 @@ function DF:UpdateTestTargetedSpell(frame, testData) -- Calculate position with inset local offset = borderSize + highlightSize - highlightInset - -- Hide all styles first - always do this + -- Highlight border via the unified DF.Border backend, mirroring the + -- live ApplyIconSettings mapping (glow→PROC, marchingAnts→DF_DASH, + -- pulse→DF_PULSATE, solidBorder→static). Overlay lazily allocated. 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" + icon.highlightBorder = icon.highlightBorder or DF.Border:New(icon.highlightFrame) local shouldShowHighlight = highlightImportant and spell.isImportant and highlightStyle and highlightStyle ~= "none" - if shouldShowHighlight then 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 + local hSize = math.max(1, highlightSize) + local spec = { + enabled = true, size = hSize, inset = 0, style = "SOLID", + color = { r = highlightColor.r, g = highlightColor.g, b = highlightColor.b, a = 1 }, + } + if highlightStyle == "marchingAnts" then + spec.animation = { type = "DF_DASH", thickness = hSize, color = spec.color } 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 + spec.animation = { type = "DF_PULSATE", color = spec.color } + elseif highlightStyle == "glow" then + spec.animation = { type = "PROC", color = spec.color } end + DF.Border:Apply(icon.highlightBorder, spec) else + DF.Border:Apply(icon.highlightBorder, { enabled = false }) icon.highlightFrame:Hide() end end From 99a351599b4ea097a7f696202b2daed571227004 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 21:06:52 +0100 Subject: [PATCH 04/34] Targeted Spells: full DF.Border GUI toolkit (Stage 1.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the bare Show Border / Size / Colour controls for GUI:CreateBorderControls, matching the Personal Targeted Spell sibling: adds Style (Solid/Gradient), Border Alpha, Inset, Blend Mode, Shadow and Animate. (Class/Role colour is deliberately not included — consistent with the siblings and not meaningful for "an enemy casting at this unit".) The render path already reads every targetedSpell* border key via BuildSpec, so the controls light up the engine with no render change. The now-unused base-border keys keep working as the Show Border + Size + Colour the control still exposes. --- Options/Options.lua | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Options/Options.lua b/Options/Options.lua index af85f6cb..27bd150e 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -6894,15 +6894,21 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) 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) From b19eef977d80f1d964ea8e794e076b11458a960b Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 21:11:01 +0100 Subject: [PATCH 05/34] Targeted Spells: migrate Targeted List bar highlight glow to DF.Border (Stage 3b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bar highlight (bar.highlightFrame glow via DF.UpdateGlowBorder) now uses a DF.Border overlay with the PROC animation, matching the icon highlight. Both live sites (render + the light colour-update path) and the TestMode teardown are migrated. With this, NOTHING references the bespoke Init/Update/Hide helpers or the TargetedSpellAnimator driver — they are fully dead and ready to delete. --- Features/TargetedSpells.lua | 20 ++++++++++++-------- TestMode/TestMode.lua | 9 +-------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index 8211be5c..87078aee 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -5037,10 +5037,12 @@ 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) + DF.Border:Apply(bar.highlightBorder, { + enabled = true, size = 2, inset = 0, style = "SOLID", + color = { r = hc.r, g = hc.g, b = hc.b, a = 1 }, + animation = { type = "PROC", color = { r = hc.r, g = hc.g, b = hc.b, a = 1 } }, + }) bar.highlightFrame:Show() if isTest and activeRec.testIsImportant ~= nil then -- Clean bool — use SetShown directly @@ -6093,10 +6095,12 @@ 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 + DF.Border:Apply(bar.highlightBorder, { + enabled = true, size = 2, inset = 0, style = "SOLID", + color = { r = hc.r, g = hc.g, b = hc.b, a = 1 }, + animation = { type = "PROC", color = { r = hc.r, g = hc.g, b = hc.b, a = 1 } }, + }) end end end diff --git a/TestMode/TestMode.lua b/TestMode/TestMode.lua index 5287a021..7bbeae03 100644 --- a/TestMode/TestMode.lua +++ b/TestMode/TestMode.lua @@ -5824,14 +5824,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 From 6ceb1f775789e267b77831e9555f4116ee8a72ac Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 21:14:44 +0100 Subject: [PATCH 06/34] Targeted Spells: delete the dead bespoke border helpers (Stage 3c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the entire "HIGHLIGHT STYLE ANIMATIONS" block (~346 lines): the TargetedSpellAnimator marching-ants/pulse OnUpdate driver, InitAnimatedBorder/ InitSolidBorder/InitGlowBorder (+ Update/Hide) and InitPulseAnimation, plus their DF.* exports and the DASH/ANIM tunables — all superseded by DF.Border. Also converts three remaining icon-teardown sites (which called the helpers by their bare local names — missed by the earlier DF.*-only grep) to a single DF.Border:Apply{enabled=false}. Highlights.lua / Dispel.lua keep their own unrelated InitAnimatedBorder / pulseAnim and are untouched. Targeted Spells borders (base + important highlight, on icons, personal frame, the Targeted List bar, and test mode) now run entirely through DF.Border. --- Features/TargetedSpells.lua | 373 +----------------------------------- 1 file changed, 3 insertions(+), 370 deletions(-) diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index 87078aee..13669066 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 @@ -1288,14 +942,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) @@ -2798,14 +2445,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 @@ -2837,14 +2477,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 From 2809a70266f861d1310a483833ba13a879d67e61 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 21:29:54 +0100 Subject: [PATCH 07/34] Targeted Spells: seed the new border defaults (fix nil dropdowns) Stage 1.5 exposed the full DF.Border toolkit for the targetedSpell border but the new keys were never defaulted, so Border Blend Mode / Animation (and Style / Gradient / Inset / Shadow / Texture) read nil and the dropdowns showed the literal "nil". Add the full targetedSpellBorder* default set to both party and raid, mirroring the personalTargetedSpell sibling (AnimationType="NONE", BlendMode="BLEND", Style="SOLID", etc.). --- Config.lua | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/Config.lua b/Config.lua index daf8d3cb..b53be4cf 100644 --- a/Config.lua +++ b/Config.lua @@ -2069,8 +2069,33 @@ 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", targetedSpellDisableMouse = false, targetedSpellDurationColor = {r = 1, g = 1, b = 1}, targetedSpellDurationColorByTime = false, @@ -3669,8 +3694,33 @@ 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", targetedSpellDisableMouse = false, targetedSpellDurationColor = {r = 1, g = 1, b = 1}, targetedSpellDurationColorByTime = false, From 5f2e579442d0b3dfdd7209f9d5d5faaaf7cd4fa9 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 22:12:36 +0100 Subject: [PATCH 08/34] Targeted Spells: add Important Spell Border defaults (foundation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seed the full targetedSpellImportantBorder* key set (party + raid) for the upcoming 'Important Spell Border' subsection — a second DF.Border gated by the Highlight Important Spells toggle, replacing the bespoke highlightStyle enum. Seeded from the old highlight values (Colour {1,0.8,0}, Size 3, Inset 2) with AnimationType=PROC (old default style 'glow'). Keys are inert until the render + GUI + migration land. --- Config.lua | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Config.lua b/Config.lua index b53be4cf..d3c56fa9 100644 --- a/Config.lua +++ b/Config.lua @@ -2096,6 +2096,33 @@ DF.PartyDefaults = { 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, @@ -3721,6 +3748,33 @@ DF.RaidDefaults = { 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, From b2a2988f3c0399cb103d71a8d89103d2f4ca2824 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 22:22:12 +0100 Subject: [PATCH 09/34] Targeted Spells: migrate old highlight settings -> Important Spell Border keys DF:MigrateTargetedSpellImportantBorder (Core.lua, per-profile guard _tsImportantBorderV1; called at login + profile switch) copies any customised targetedSpellHighlightColor/Size/Inset/Style into the new targetedSpellImportantBorder* keys, mapping style->animation (glow=PROC, marchingAnts=DF_DASH, pulse=DF_PULSATE, solidBorder/none=NONE). Additive + no-op for default profiles (defaults already match); does not change rendering yet. --- Core.lua | 43 +++++++++++++++++++++++++++++++++++++++++++ Profile.lua | 3 +++ 2 files changed, 46 insertions(+) diff --git a/Core.lua b/Core.lua index a4b49abc..b93a7cf6 100644 --- a/Core.lua +++ b/Core.lua @@ -3509,6 +3509,43 @@ local function ZeroBuffDebuffBorderInset(profile) 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" } + for _, profile in pairs(DandersFramesDB_v2.profiles) do + if type(profile) == "table" and not profile._tsImportantBorderV1 then + for _, modeKey in ipairs({ "party", "raid" }) do + local m = profile[modeKey] + if type(m) == "table" then + if m.targetedSpellHighlightColor ~= nil and m.targetedSpellImportantBorderColor == nil then + m.targetedSpellImportantBorderColor = m.targetedSpellHighlightColor + if m.targetedSpellImportantBorderAnimationColor == nil then + m.targetedSpellImportantBorderAnimationColor = m.targetedSpellHighlightColor + end + end + if m.targetedSpellHighlightSize ~= nil and m.targetedSpellImportantBorderSize == nil then + m.targetedSpellImportantBorderSize = m.targetedSpellHighlightSize + end + if m.targetedSpellHighlightInset ~= nil and m.targetedSpellImportantBorderInset == nil then + m.targetedSpellImportantBorderInset = m.targetedSpellHighlightInset + end + if m.targetedSpellHighlightStyle ~= nil and m.targetedSpellImportantBorderAnimationType == nil then + m.targetedSpellImportantBorderAnimationType = styleToAnim[m.targetedSpellHighlightStyle] or "PROC" + end + end + end + profile._tsImportantBorderV1 = true + end + end +end + -- One-shot per-profile, two independently-guarded steps so a profile already -- through step 1 still receives step 2. Both steps are value-idempotent. function DF:MigrateBorderInsetFold() @@ -5304,6 +5341,12 @@ DF._MainEventDispatcher = function(self, event, arg1) DF:MigrateBorderInsetFold() 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 + -- CRITICAL: Update power bars now that unit data is available -- At ADDON_LOADED, UnitPower() etc may return 0 before player is loaded -- Power bar updates don't require combat protection diff --git a/Profile.lua b/Profile.lua index 634532d2..5819721b 100644 --- a/Profile.lua +++ b/Profile.lua @@ -306,6 +306,9 @@ function DF:SetProfile(name) if DF.MigrateBorderInsetFold then DF:MigrateBorderInsetFold() end + if DF.MigrateTargetedSpellImportantBorder then + DF:MigrateTargetedSpellImportantBorder() + end -- Apply the profile — runtime state is already clear so the proxy reads -- the new profile directly with no stale overlay From 1a324fd7ac6891409ab928a62f4c76a2be836f2f Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 22:43:45 +0100 Subject: [PATCH 10/34] Targeted Spells: group important-spell highlight onto its own DF.Border subsection Switch the group per-icon important-spell highlight render to a full DF.Border via BuildSpec(db, "targetedSpellImportant", {iconMode}) and swap the GUI from the bare Style/Colour/Size/Inset controls to CreateBorderControls. The highlight border now exposes the whole border toolkit (style, alpha, inset, blend mode, gradient, shadow, animation), gated by the existing Highlight Important Spells toggle + the secret-safe SetAlphaFromBoolean(isImportant). Frame offset now follows the targetedSpellImportant border size/inset. Personal Targeted Spell still uses the prior style->animation mapping (its render block is shared and reads a different key prefix). --- Features/TargetedSpells.lua | 44 ++++++++++++------------------------- Options/Options.lua | 24 +++++++++++--------- 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index 13669066..ceeb7d0b 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -626,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 @@ -656,35 +655,20 @@ 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. + local hlSize = db.targetedSpellImportantBorderSize or 3 + local hlInset = db.targetedSpellImportantBorderInset or 2 + local offset = borderSize + hlSize - hlInset icon.highlightFrame:ClearAllPoints() icon.highlightFrame:SetPoint("TOPLEFT", icon.iconFrame, "TOPLEFT", -offset, offset) icon.highlightFrame:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", offset, -offset) - - -- Highlight border via the unified DF.Border backend. The style maps onto - -- a DF.Border animation (glow→PROC, marchingAnts→DF_DASH, pulse→DF_PULSATE, - -- solidBorder→static). The highlightFrame stays as the secret-safe alpha - -- gate (SetAlphaFromBoolean), positioned just outside the base border. - if highlightImportant and spellID and highlightStyle ~= "none" and icon.highlightBorder then + if highlightImportant and spellID and icon.highlightBorder then local isImportant = C_Spell.IsSpellImportant(spellID) - local hSize = math.max(1, highlightSize) - local spec = { - enabled = true, - size = hSize, - inset = 0, - style = "SOLID", - color = { r = highlightColor.r, g = highlightColor.g, b = highlightColor.b, a = 1 }, - } - if highlightStyle == "marchingAnts" then - spec.animation = { type = "DF_DASH", thickness = hSize, color = spec.color } - elseif highlightStyle == "pulse" then - spec.animation = { type = "DF_PULSATE", color = spec.color } - elseif highlightStyle == "glow" then - spec.animation = { type = "PROC", color = spec.color } - 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) @@ -693,7 +677,7 @@ local function ApplyIconSettings(icon, db, spellID) icon.highlightFrame:Hide() end end - + -- 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 + diff --git a/Options/Options.lua b/Options/Options.lua index 27bd150e..07a0e542 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -6943,8 +6943,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() @@ -6952,14 +6951,19 @@ 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, + 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 = 1, sizeMax = 8, sizeStep = 1, + }) AddToSection(highlightGroup, nil, 1) currentSection = nil From 10cdde63b4bef821775be7b0bd892207d4c51f32 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 23:09:12 +0100 Subject: [PATCH 11/34] Targeted Spells: fix test-mode border render + redundant highlight toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test mode (Bug 1): the base border still drew the old 4 edge textures (icon.borderLeft/Right/Top/Bottom), which the live create path retired when it moved to DF.Border, so no border rendered in test mode. Render it through DF.Border (lazy-allocated icon.border + BuildSpec) mirroring live ApplyIconSettings, and migrate the test-mode important highlight onto the targetedSpellImportant* keys so test mode is WYSIWYG with the new GUI. Highlight toggle (Bug 2): CreateBorderControls always added its own Show Border checkbox, so the Important Spells section had two competing gates (Highlight Important Spells + Show Border) — the animation, gated only by the master toggle, kept showing when Show Border was unchecked while the toolkit hid. Add opts.noShowToggle to suppress the built-in checkbox and let the Highlight Important Spells master be the single gate. --- GUI/GUI.lua | 17 +++++-- Options/Options.lua | 1 + TestMode/TestMode.lua | 113 ++++++++++++------------------------------ 3 files changed, 44 insertions(+), 87 deletions(-) diff --git a/GUI/GUI.lua b/GUI/GUI.lua index 94585b64..995b67f6 100644 --- a/GUI/GUI.lua +++ b/GUI/GUI.lua @@ -3924,11 +3924,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/Options/Options.lua b/Options/Options.lua index 07a0e542..8949cc3d 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -6955,6 +6955,7 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) -- 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, diff --git a/TestMode/TestMode.lua b/TestMode/TestMode.lua index 7bbeae03..d8f5862e 100644 --- a/TestMode/TestMode.lua +++ b/TestMode/TestMode.lua @@ -5497,11 +5497,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,35 +5612,25 @@ 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 - - -- Highlight border via the unified DF.Border backend, mirroring the - -- live ApplyIconSettings mapping (glow→PROC, marchingAnts→DF_DASH, - -- pulse→DF_PULSATE, solidBorder→static). Overlay lazily allocated. if icon.highlight then icon.highlight:Hide() end icon.highlightBorder = icon.highlightBorder or DF.Border:New(icon.highlightFrame) - local shouldShowHighlight = highlightImportant and spell.isImportant and highlightStyle and highlightStyle ~= "none" - if shouldShowHighlight then + if highlightImportant and spell.isImportant then + local hlSize = db.targetedSpellImportantBorderSize or 3 + local hlInset = db.targetedSpellImportantBorderInset or 2 + local offset = borderSize + hlSize - hlInset 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() - local hSize = math.max(1, highlightSize) - local spec = { - enabled = true, size = hSize, inset = 0, style = "SOLID", - color = { r = highlightColor.r, g = highlightColor.g, b = highlightColor.b, a = 1 }, - } - if highlightStyle == "marchingAnts" then - spec.animation = { type = "DF_DASH", thickness = hSize, color = spec.color } - elseif highlightStyle == "pulse" then - spec.animation = { type = "DF_PULSATE", color = spec.color } - elseif highlightStyle == "glow" then - spec.animation = { type = "PROC", color = spec.color } - 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 }) @@ -5652,69 +5638,32 @@ function DF:UpdateTestTargetedSpell(frame, testData) 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 + -- 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", borderSize, -borderSize) - icon.icon:SetPoint("BOTTOMRIGHT", icon.iconFrame, "BOTTOMRIGHT", -borderSize, borderSize) + 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", 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 - 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) - 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() From 383a263e3b444183f3000cf7211438e00bef7d6d Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 23:14:33 +0100 Subject: [PATCH 12/34] Targeted List: make the important highlight a static border, not a PROC glow The bar highlight migration hardcoded a PROC (LibCustomGlow ProcGlow) animation on top of the solid highlight border, so the Targeted List important highlight flashed/animated where it used to be a calm glow. The Targeted List highlight exposes only an enable toggle + colour (no animation control), so drop the animation and render a plain static solid border in the highlight colour at both apply sites (render + lightweight colour update). --- Features/TargetedSpells.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index ceeb7d0b..1f27966b 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -4655,10 +4655,12 @@ local function TargetedList_ApplyBarContent(bar, activeRec) if party and party.targetedListHighlightImportant then local hc = party.targetedListHighlightColor or {r=1, g=0.8, b=0} 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 = 1 }, - animation = { type = "PROC", color = { r = hc.r, g = hc.g, b = hc.b, a = 1 } }, }) bar.highlightFrame:Show() if isTest and activeRec.testIsImportant ~= nil then @@ -5713,10 +5715,12 @@ function DF:LightweightUpdateTargetedListHighlightColor() 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() 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 = 1 }, - animation = { type = "PROC", color = { r = hc.r, g = hc.g, b = hc.b, a = 1 } }, }) end end From ae4d6e03751df3bb7f3ab47c7e245b7fcf4a8ff2 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 23:20:57 +0100 Subject: [PATCH 13/34] Targeted List: add alpha to the highlight colour Enable alpha on the Highlight Color picker and honour color.a in the highlight border render (was hardcoded a=1) so the static highlight can be made translucent. Default + reset colour now carry a=1. --- Config.lua | 2 +- Features/TargetedSpells.lua | 4 ++-- Options/Options.lua | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Config.lua b/Config.lua index d3c56fa9..29044989 100644 --- a/Config.lua +++ b/Config.lua @@ -2200,7 +2200,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, diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index 1f27966b..b7902c13 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -4660,7 +4660,7 @@ local function TargetedList_ApplyBarContent(bar, activeRec) -- 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 = 1 }, + 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 @@ -5720,7 +5720,7 @@ function DF:LightweightUpdateTargetedListHighlightColor() -- 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 = 1 }, + color = { r = hc.r, g = hc.g, b = hc.b, a = hc.a or 1 }, }) end end diff --git a/Options/Options.lua b/Options/Options.lua index 8949cc3d..19e90a8a 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -7256,13 +7256,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 From 7741b319ae4ef1a16bc5b5d425c03022074d1cd4 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 23:28:00 +0100 Subject: [PATCH 14/34] Targeted Spells: fix important-highlight inset not staying centred The highlight frame offset subtracted the inset (offset = borderSize + hlSize - hlInset) while BuildSpec ALSO applies the inset via spec.inset in iconMode, so the Border Inset slider was applied twice and resized the frame, throwing off centring. Make the frame offset inset-independent (borderSize + hlSize) and let the engine's symmetric spec.inset own the inset, at both the live and test-mode render sites. --- Features/TargetedSpells.lua | 7 +++++-- TestMode/TestMode.lua | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index b7902c13..15fddbbb 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -659,9 +659,12 @@ local function ApplyIconSettings(icon, db, spellID) -- 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 hlInset = db.targetedSpellImportantBorderInset or 2 - local offset = borderSize + hlSize - hlInset + 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) diff --git a/TestMode/TestMode.lua b/TestMode/TestMode.lua index d8f5862e..3178639d 100644 --- a/TestMode/TestMode.lua +++ b/TestMode/TestMode.lua @@ -5621,9 +5621,10 @@ function DF:UpdateTestTargetedSpell(frame, testData) if icon.highlight then icon.highlight:Hide() end 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 hlInset = db.targetedSpellImportantBorderInset or 2 - local offset = borderSize + hlSize - hlInset + 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) From 579efe98143a085245b5c18fab6afdd4e614f728 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 23:37:29 +0100 Subject: [PATCH 15/34] Targeted Spells: allow important highlight thickness 0 (matches base border) The Important Spell Border thickness slider had sizeMin=1, so it couldn't be set to 0 like the base border (sizeMin=0). Lower to 0; at 0 the solid edges vanish while any configured animation keeps running, consistent with every other DF.Border (thickness and animation are independent; animation is gated by the border being shown, not by thickness). --- Options/Options.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options/Options.lua b/Options/Options.lua index 19e90a8a..eff20786 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -6963,7 +6963,7 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) lightColors = FullUpdate, refreshStates = function() self:RefreshStates() end, hideWhen = HideHighlightOptions, - sizeMin = 1, sizeMax = 8, sizeStep = 1, + sizeMin = 0, sizeMax = 8, sizeStep = 1, }) AddToSection(highlightGroup, nil, 1) From e2da95b98635b1909c978af76e7748ed3f4d6976 Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 23:41:39 +0100 Subject: [PATCH 16/34] Targeted/Personal Targeted Spells: move Alpha + Cooldown Swipe out of the Border group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pages parked an icon Alpha slider and a cooldown-swipe toggle under the Border header, ahead of CreateBorderControls — so the Border group mixed feature display knobs with the shared engine, and the icon Alpha sat confusingly next to the engine's own Border Alpha. Move both into the top-level Settings group (matches Defensive Icon, where Hide Cooldown Swipe already lives in Settings). The Border header is now purely the shared DF.Border toolkit on every migrated page. --- Options/Options.lua | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Options/Options.lua b/Options/Options.lua index eff20786..dd0ef283 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -6814,6 +6814,12 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) FullUpdate() end), 30) tsNameplateOffscreen.disableOn = HideTargetedSpellOptions + -- Icon display knobs (NOT border settings — kept out of the Border group so + -- that group is purely the shared DF.Border toolkit). + local tsAlpha = settingsGroup:AddWidget(GUI:CreateSlider(self.child, L["Alpha"], 0.0, 1.0, 0.05, db, "targetedSpellAlpha", FullUpdate, TargetedSpellLightweightUpdate, true), 55) + tsAlpha.disableOn = HideTargetedSpellOptions + local hideSwipe = settingsGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Hide Cooldown Swipe"], db, "targetedSpellHideSwipe", FullUpdate), 30) + hideSwipe.disableOn = HideTargetedSpellOptions settingsGroup:AddWidget(GUI:CreateButton(self.child, L["Run Setup Wizard"], 160, 24, function() if DF.ShowTargetedSpellSetupWizard then DF:ShowTargetedSpellSetupWizard() end end), 34) @@ -6890,10 +6896,6 @@ 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 -- 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 @@ -7465,6 +7467,12 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) if DF.TogglePersonalTargetedSpells then DF:TogglePersonalTargetedSpells(db.personalTargetedSpellEnabled) end end), 30) settingsGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Important Spells Only"], db, "personalTargetedSpellImportantOnly", PersonalTargetedUpdate), 30) + -- Icon display knobs (NOT border settings — kept out of the Border group so + -- that group is purely the shared DF.Border toolkit). + local ptsAlpha = settingsGroup:AddWidget(GUI:CreateSlider(self.child, L["Alpha"], 0.0, 1.0, 0.05, db, "personalTargetedSpellAlpha", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) + ptsAlpha.disableOn = HidePersonalOptions + local ptsSwipe = settingsGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Show Cooldown Swipe"], db, "personalTargetedSpellShowSwipe", PersonalTargetedUpdate), 30) + ptsSwipe.disableOn = HidePersonalOptions Add(settingsGroup, nil, 1) -- ===== CONTENT TYPES GROUP (Column 2) ===== @@ -7529,14 +7537,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, From 65a7a43341206b10e0d10f7a01b99a1d2a14144c Mon Sep 17 00:00:00 2001 From: Krathe Date: Mon, 15 Jun 2026 23:57:24 +0100 Subject: [PATCH 17/34] Border: hide solid/gradient edges at thickness 0 Apply() always Show()'d the four edge textures in SOLID/GRADIENT mode; at size 0 they collapse to zero width/height but a degenerate texture could still leave a hairline. Other borders never exposed this (their sliders min at 1) but the Targeted Spells base + important borders now allow 0. Hide the edges when size <= 0 so 0 means no visible border, engine-wide. Animation overlays are separate frames and keep running, consistent with thickness being independent of animation. --- Frames/Border.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Frames/Border.lua b/Frames/Border.lua index fd95c649..d3ccb5ff 100644 --- a/Frames/Border.lua +++ b/Frames/Border.lua @@ -1414,6 +1414,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. From 8031065f5fa4f9635274d8fc8f3ae7d813b8b01a Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 00:04:15 +0100 Subject: [PATCH 18/34] Border: hide textured backdrop at thickness 0 (parity with solid) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Texture-style borders clamped edgeSize to 1px at size 0 instead of hiding, so thickness 0 still showed a 1px textured border — inconsistent with the solid/gradient path which now hides at 0. Hide the backdrop when size <= 0 so any border style means no border at 0. Animation overlays are applied later in Apply gated only on spec.animation, so they keep running at thickness 0 regardless of style. --- Frames/Border.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Frames/Border.lua b/Frames/Border.lua index d3ccb5ff..597a2782 100644 --- a/Frames/Border.lua +++ b/Frames/Border.lua @@ -1430,10 +1430,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 From 786fd751fde57c702d1d997ebe443b38652cdeea Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 00:39:44 +0100 Subject: [PATCH 19/34] Personal Targeted Spell: important highlight onto its own DF.Border subsection Brings the Personal Targeted Spell important-spell highlight to parity with the group/party side: render switches to BuildSpec(db, "personalTargetedSpellImportant", {iconMode}) with an inset-independent frame offset, the GUI swaps the old Style/Colour/Size/Inset controls for CreateBorderControls (noShowToggle; the Highlight Important Spells toggle is the single gate), full personalTargetedSpellImportantBorder* Config defaults seeded in both tables, and the migration maps the old personalTargetedSpellHighlight* keys across under an independent guard so profiles already through the group step still migrate personal. --- Config.lua | 54 +++++++++++++++++++++++++++++++++++ Core.lua | 56 ++++++++++++++++++++++++------------- Features/TargetedSpells.lua | 42 ++++++++-------------------- Options/Options.lua | 31 ++++++++++++-------- 4 files changed, 122 insertions(+), 61 deletions(-) diff --git a/Config.lua b/Config.lua index 29044989..94239a06 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, @@ -3358,6 +3385,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, diff --git a/Core.lua b/Core.lua index b93a7cf6..e8753980 100644 --- a/Core.lua +++ b/Core.lua @@ -3519,29 +3519,45 @@ 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 when the new key is still nil. Value-idempotent; + -- shared by the group (targetedSpell) and personal (personalTargetedSpell) sets. + local function mapHighlight(m, p) + if m[p.."HighlightColor"] ~= nil and m[p.."ImportantBorderColor"] == nil then + m[p.."ImportantBorderColor"] = m[p.."HighlightColor"] + if m[p.."ImportantBorderAnimationColor"] == nil then + m[p.."ImportantBorderAnimationColor"] = m[p.."HighlightColor"] + end + end + if m[p.."HighlightSize"] ~= nil and m[p.."ImportantBorderSize"] == nil then + m[p.."ImportantBorderSize"] = m[p.."HighlightSize"] + end + if m[p.."HighlightInset"] ~= nil and m[p.."ImportantBorderInset"] == nil then + m[p.."ImportantBorderInset"] = m[p.."HighlightInset"] + end + if m[p.."HighlightStyle"] ~= nil and m[p.."ImportantBorderAnimationType"] == 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" and not profile._tsImportantBorderV1 then - for _, modeKey in ipairs({ "party", "raid" }) do - local m = profile[modeKey] - if type(m) == "table" then - if m.targetedSpellHighlightColor ~= nil and m.targetedSpellImportantBorderColor == nil then - m.targetedSpellImportantBorderColor = m.targetedSpellHighlightColor - if m.targetedSpellImportantBorderAnimationColor == nil then - m.targetedSpellImportantBorderAnimationColor = m.targetedSpellHighlightColor - end - end - if m.targetedSpellHighlightSize ~= nil and m.targetedSpellImportantBorderSize == nil then - m.targetedSpellImportantBorderSize = m.targetedSpellHighlightSize - end - if m.targetedSpellHighlightInset ~= nil and m.targetedSpellImportantBorderInset == nil then - m.targetedSpellImportantBorderInset = m.targetedSpellHighlightInset - end - if m.targetedSpellHighlightStyle ~= nil and m.targetedSpellImportantBorderAnimationType == nil then - m.targetedSpellImportantBorderAnimationType = styleToAnim[m.targetedSpellHighlightStyle] or "PROC" - end + 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 - profile._tsImportantBorderV1 = true end end end diff --git a/Features/TargetedSpells.lua b/Features/TargetedSpells.lua index 15fddbbb..cbe5bc05 100644 --- a/Features/TargetedSpells.lua +++ b/Features/TargetedSpells.lua @@ -2115,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 @@ -2140,37 +2139,20 @@ 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) - - -- Highlight border via the unified DF.Border backend. The style maps onto - -- a DF.Border animation (glow→PROC, marchingAnts→DF_DASH, pulse→DF_PULSATE, - -- solidBorder→static). The highlightFrame stays as the secret-safe alpha - -- gate (SetAlphaFromBoolean), positioned just outside the base border. - if highlightImportant and spellID and highlightStyle ~= "none" and icon.highlightBorder then + if highlightImportant and spellID and icon.highlightBorder then local isImportant = C_Spell.IsSpellImportant(spellID) - local hSize = math.max(1, highlightSize) - local spec = { - enabled = true, - size = hSize, - inset = 0, - style = "SOLID", - color = { r = highlightColor.r, g = highlightColor.g, b = highlightColor.b, a = 1 }, - } - if highlightStyle == "marchingAnts" then - spec.animation = { type = "DF_DASH", thickness = hSize, color = spec.color } - elseif highlightStyle == "pulse" then - spec.animation = { type = "DF_PULSATE", color = spec.color } - elseif highlightStyle == "glow" then - spec.animation = { type = "PROC", color = spec.color } - 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) diff --git a/Options/Options.lua b/Options/Options.lua index dd0ef283..d8e3b268 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -7583,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 From e65b4c1963eeae436738198738c6507062513830 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 00:52:58 +0100 Subject: [PATCH 20/34] Borders: allow thickness 0 on the two animation-capable outliers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Missing Buff and Personal Targeted Spell borders are animation-capable but clamped sizeMin to 1, so you couldn't run an animation with no solid edge (every other animation-capable border already allows 0: buff, debuff, defensive, targeted spell + important). Lower both to sizeMin 0. Safe now that Apply hides the edges at 0 while the animation keeps running. Non- animation borders (pet, resource bar, targeted list) stay at 1 — 0 there would just duplicate the Show Border toggle. --- Options/Options.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Options/Options.lua b/Options/Options.lua index d8e3b268..6760c305 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) @@ -7548,7 +7548,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) From 90ec37ff7fce752bc216faa72968171ddc19bc8c Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 01:23:18 +0100 Subject: [PATCH 21/34] Border: always defer LCG glow start so it never sizes to a stale frame StartAnimation started the LCG glow (PULSATE/CHASE/FLASH/PROC) immediately when animRect:GetWidth() was non-zero, only deferring on an exact 0. But a frame resized during the same layout pass (e.g. a test-mode enable toggle re-render) reports a stale/unresolved non-zero width, so the glow sized to the wrong (too-large) rect on alternate re-renders. Always defer the start to the next frame so it reads the settled size; the start token still guards against a superseded or stopped start, and one frame of latency is imperceptible. --- Frames/Border.lua | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Frames/Border.lua b/Frames/Border.lua index 597a2782..85e5f602 100644 --- a/Frames/Border.lua +++ b/Frames/Border.lua @@ -1060,11 +1060,18 @@ function Border:StartAnimation(border, spec) end end - if (border.animRect:GetWidth() or 0) > 0 then - startGlow() - else - C_Timer.After(0, startGlow) - end + -- ALWAYS defer the glow start to the next frame so it reads the + -- animRect's size AFTER the layout pass settles. Starting immediately + -- on a non-zero width is unsafe: when the frame is resized this same + -- frame (e.g. a test-mode toggle re-render, or any re-layout), the + -- width read is stale/unresolved and LCG sizes the glow to it — the + -- "renders huge" case the deferral was added for. The earlier + -- `width > 0 → start now` fast-path only caught a width of exactly 0, + -- so on alternate re-renders it intermittently fired against a stale + -- size and the glow rendered too large every other toggle. One frame + -- of latency is imperceptible and the start token guards against a + -- superseded/stopped start. + C_Timer.After(0, startGlow) border.activeAnimation = anim.type return end From 95a8c8f3a06592043d630d45019b04c4f172aa57 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 01:40:54 +0100 Subject: [PATCH 22/34] Border: fix doubled PROC glow on re-apply; start in loop state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROC was started with startAnim=true, playing LCG's proc start animation (begins large, shrinks to the border). DF.Border uses PROC as a CONTINUOUS border animation, so that start flash re-fired on every re-Apply (test-mode toggles, relayouts); when the prior start animation hadn't fully torn down, two glows rendered at two sizes (one inset, one further out) — visible as the highlight flashing/doubling on alternate toggles. Start straight in the loop state (startAnim=false) for a clean, stable glow with no flash-in. Also reverts the always-defer glow-start change from the prior commit: it did not address this and is unnecessary; restore the original defer-only-when-unsized behaviour. --- Frames/Border.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Frames/Border.lua b/Frames/Border.lua index 85e5f602..1f1aa8a4 100644 --- a/Frames/Border.lua +++ b/Frames/Border.lua @@ -1051,27 +1051,27 @@ 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 + -- startAnim = false: DF.Border uses PROC as a CONTINUOUS border + -- animation, not a one-shot proc trigger. The start animation + -- begins large and shrinks to the border, and it re-fires on + -- every re-Apply (e.g. test-mode toggles, relayouts) — when the + -- prior start animation hasn't fully torn down you get two glows + -- at two sizes (one inset, one further out). Starting straight in + -- the loop state gives a clean, stable glow with no flash-in. LCG.ProcGlow_Start(target, { color = color, duration = duration, - startAnim = true, + startAnim = false, key = key, }) end end - -- ALWAYS defer the glow start to the next frame so it reads the - -- animRect's size AFTER the layout pass settles. Starting immediately - -- on a non-zero width is unsafe: when the frame is resized this same - -- frame (e.g. a test-mode toggle re-render, or any re-layout), the - -- width read is stale/unresolved and LCG sizes the glow to it — the - -- "renders huge" case the deferral was added for. The earlier - -- `width > 0 → start now` fast-path only caught a width of exactly 0, - -- so on alternate re-renders it intermittently fired against a stale - -- size and the glow rendered too large every other toggle. One frame - -- of latency is imperceptible and the start token guards against a - -- superseded/stopped start. - C_Timer.After(0, startGlow) + if (border.animRect:GetWidth() or 0) > 0 then + startGlow() + else + C_Timer.After(0, startGlow) + end border.activeAnimation = anim.type return end From 895eb10a511488eb270bde0f1affe5245a8dc384 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 01:50:38 +0100 Subject: [PATCH 23/34] Border: make PROC start flash an opt-in option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the hardcoded startAnim with a per-border setting (BorderAnimationProcStart, default off). BuildSpec reads it into anim.procStart, StartAnimation passes it to ProcGlow_Start, and animSpecHash includes it so toggling re-applies cleanly. CreateBorderControls gains a 'Proc Start Flash' checkbox shown only when Animation = PROC. Default off keeps the clean continuous glow (no flash-in, no doubling on re-apply). Users who want the proc flash can opt in per border; it plays cleanly on a normal single appearance — only rapid re-toggling (a test-mode stress, not real play) can still double it, which is inherent to using a one-shot proc effect as a continuous animation. --- Frames/Border.lua | 22 ++++++++++++++-------- GUI/GUI.lua | 7 +++++++ Locales/enUS.lua | 1 + 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Frames/Border.lua b/Frames/Border.lua index 1f1aa8a4..5d8e15bc 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 @@ -955,6 +960,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,17 +1057,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 - -- startAnim = false: DF.Border uses PROC as a CONTINUOUS border - -- animation, not a one-shot proc trigger. The start animation - -- begins large and shrinks to the border, and it re-fires on - -- every re-Apply (e.g. test-mode toggles, relayouts) — when the - -- prior start animation hasn't fully torn down you get two glows - -- at two sizes (one inset, one further out). Starting straight in - -- the loop state gives a clean, stable glow with no flash-in. + -- 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 = false, + startAnim = anim.procStart and true or false, key = key, }) end diff --git a/GUI/GUI.lua b/GUI/GUI.lua index 995b67f6..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) 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 From f736a2baa3aa4dd0d293ab590641af20168afee9 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 02:09:08 +0100 Subject: [PATCH 24/34] Border: temporary PROC glow diagnostics (gated on /df debug) Logs each ProcGlow start (target, pre-existing glow + its ProcStart/ProcLoop shown state, procStart flag) and each stop (which frame still carries a glow) to chat when debug mode is on. Lets us see, on one slow toggle cycle, whether the double is two starts without a stop, a second glow on a new target, or one frame with both textures shown. To be reverted once the root cause is identified. --- Frames/Border.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Frames/Border.lua b/Frames/Border.lua index 5d8e15bc..d3e3fb7c 100644 --- a/Frames/Border.lua +++ b/Frames/Border.lua @@ -887,6 +887,14 @@ function Border:StopAnimation(border) -- field from the pre-rename revision and is checked for users -- mid-upgrade who might still have a glow running on the old frame.) local anchor = border.anchorTo or border + if DF.debugEnabled then + local ag = anchor and anchor["_ProcGlowDFBorder"] + local rg = border.animRect and border.animRect["_ProcGlowDFBorder"] + if ag or rg then + print(("|cff00aaff[PROC STOP]|r t=%.2f border=%s anchorGlow=%s animRect=%s animRectGlow=%s") + :format(GetTime(), tostring(border), tostring(ag), tostring(border.animRect), tostring(rg))) + end + end local function stopAll(t) if LCG.PixelGlow_Stop then LCG.PixelGlow_Stop(t, key) end if LCG.AutoCastGlow_Stop then LCG.AutoCastGlow_Stop(t, key) end @@ -1040,6 +1048,16 @@ function Border:StartAnimation(border, spec) if border._lcgStartToken ~= token then return end local target = border.animRect if not target then return end + if DF.debugEnabled and anim.type == "PROC" then + local existing = target["_ProcGlowDFBorder"] + print(("|cffff8800[PROC START]|r t=%.2f border=%s target=%s existing=%s procStart=%s") + :format(GetTime(), tostring(border), tostring(target), tostring(existing), tostring(anim.procStart))) + if existing then + print((" |cffff8800reusing glow|r ProcStart.shown=%s ProcLoop.shown=%s") + :format(tostring(existing.ProcStart and existing.ProcStart:IsShown()), + tostring(existing.ProcLoop and existing.ProcLoop:IsShown()))) + end + end if anim.type == "PULSATE" then -- PixelGlow `border` arg: false → no outer mask. anim.mask = true -- restores the backing card for users who want that look. From d37caa025c840b6c910e47bfbf283476290befe9 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 02:15:56 +0100 Subject: [PATCH 25/34] Border: reset ProcGlow textures before pool release (fix double glow) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnosis from runtime logs: DF's start/stop reference lifecycle is clean (every start sees existing=nil, every stop releases the glow), so the double was NOT an orphaned frame. The glow frame carries two textures — ProcStart (the big ~3.5x start-flash) and ProcLoop (the small loop). LCG's pool resetter hides the frame and clears the reference but leaves those textures' shown/alpha state, so a frame re-acquired from the shared pool could return with BOTH visible: the inset + further-out double seen on alternate re-applies. Fix: in stopAll, before LCG releases the frame, stop its ProcStart/ProcLoop animation groups and hide+zero both textures, so the next Acquire starts clean. Keeps the proc start flash working (no need to disable it). --- Frames/Border.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Frames/Border.lua b/Frames/Border.lua index d3e3fb7c..9fe7cc45 100644 --- a/Frames/Border.lua +++ b/Frames/Border.lua @@ -896,6 +896,21 @@ function Border:StopAnimation(border) end end 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 From a73532e8e3bc6bf084e721000e81ed374e7a8677 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 02:20:19 +0100 Subject: [PATCH 26/34] Border: remove temporary PROC glow diagnostics Root cause confirmed and fixed (texture reset on pool release). Strip the debug prints added for diagnosis. --- Frames/Border.lua | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Frames/Border.lua b/Frames/Border.lua index 9fe7cc45..ddc6004d 100644 --- a/Frames/Border.lua +++ b/Frames/Border.lua @@ -887,14 +887,6 @@ function Border:StopAnimation(border) -- field from the pre-rename revision and is checked for users -- mid-upgrade who might still have a glow running on the old frame.) local anchor = border.anchorTo or border - if DF.debugEnabled then - local ag = anchor and anchor["_ProcGlowDFBorder"] - local rg = border.animRect and border.animRect["_ProcGlowDFBorder"] - if ag or rg then - print(("|cff00aaff[PROC STOP]|r t=%.2f border=%s anchorGlow=%s animRect=%s animRectGlow=%s") - :format(GetTime(), tostring(border), tostring(ag), tostring(border.animRect), tostring(rg))) - end - end 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; @@ -1063,16 +1055,6 @@ function Border:StartAnimation(border, spec) if border._lcgStartToken ~= token then return end local target = border.animRect if not target then return end - if DF.debugEnabled and anim.type == "PROC" then - local existing = target["_ProcGlowDFBorder"] - print(("|cffff8800[PROC START]|r t=%.2f border=%s target=%s existing=%s procStart=%s") - :format(GetTime(), tostring(border), tostring(target), tostring(existing), tostring(anim.procStart))) - if existing then - print((" |cffff8800reusing glow|r ProcStart.shown=%s ProcLoop.shown=%s") - :format(tostring(existing.ProcStart and existing.ProcStart:IsShown()), - tostring(existing.ProcLoop and existing.ProcLoop:IsShown()))) - end - end if anim.type == "PULSATE" then -- PixelGlow `border` arg: false → no outer mask. anim.mask = true -- restores the backing card for users who want that look. From cad0e56c97c8a14048fc5763748726eb6359f2b4 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 02:37:06 +0100 Subject: [PATCH 27/34] Test mode: add Targeted Spells + Personal Targeted toggles The new fingerprint Targeted Spells icons and the Personal Targeted display auto-showed in test mode whenever their feature was enabled, with no test- panel switch (the old 'Targeted Spell' checkbox slot was repurposed for Targeted List after Blizzard killed the old display). Add proper toggles: - testShowTargetedSpell (repurposes the pre-existing dead key, now default true) + new testShowPersonalTargeted, in both party + raid config tables. - Two checkboxes in the test panel's Indicators section, both refreshing via UpdateAllTestTargetedSpell (which drives both previews). - Render gates in UpdateAllTestTargetedSpell: icons + personal now respect their toggle (off => hidden, via the existing else-branch hide path). - Wired into all four presets (Static/Combat/Healer/Full), mirroring Targeted List (off in Static/Combat, on in Healer/Full). Default true preserves the prior auto-show behaviour until a preset/toggle changes it. --- Config.lua | 6 ++++-- TestMode/TestMode.lua | 28 +++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Config.lua b/Config.lua index 94239a06..c8f6c311 100644 --- a/Config.lua +++ b/Config.lua @@ -2310,7 +2310,8 @@ DF.PartyDefaults = { testShowReducedMaxHealth = true, testShowSelection = false, testShowStatusIcons = true, - testShowTargetedSpell = false, + testShowTargetedSpell = true, + testShowPersonalTargeted = true, testShowClassPower = true, testShowAuraDesigner = false, testShowTextDesigner = true, @@ -3894,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/TestMode/TestMode.lua b/TestMode/TestMode.lua index 3178639d..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 @@ -5762,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) @@ -5796,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() @@ -5822,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() @@ -6669,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. @@ -6828,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) From d38fab7c2281cfed5afb97dc44d03b6a6d1058d1 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 13:49:14 +0100 Subject: [PATCH 28/34] Targeted/Personal Targeted Spells: group Alpha with Size/Scale, Swipe with Duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the buff/debuff convention — Alpha belongs with Icon Size + Scale, and the cooldown swipe sits with the duration settings — rather than parked in the functional Settings group. Both the Targeted Spells and Personal Targeted pages. The swipe stays enabled independent of the duration text (it's the radial cooldown sweep, not the numeric text). --- Options/Options.lua | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Options/Options.lua b/Options/Options.lua index 6760c305..63d1696a 100644 --- a/Options/Options.lua +++ b/Options/Options.lua @@ -6814,12 +6814,6 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) FullUpdate() end), 30) tsNameplateOffscreen.disableOn = HideTargetedSpellOptions - -- Icon display knobs (NOT border settings — kept out of the Border group so - -- that group is purely the shared DF.Border toolkit). - local tsAlpha = settingsGroup:AddWidget(GUI:CreateSlider(self.child, L["Alpha"], 0.0, 1.0, 0.05, db, "targetedSpellAlpha", FullUpdate, TargetedSpellLightweightUpdate, true), 55) - tsAlpha.disableOn = HideTargetedSpellOptions - local hideSwipe = settingsGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Hide Cooldown Swipe"], db, "targetedSpellHideSwipe", FullUpdate), 30) - hideSwipe.disableOn = HideTargetedSpellOptions settingsGroup:AddWidget(GUI:CreateButton(self.child, L["Run Setup Wizard"], 160, 24, function() if DF.ShowTargetedSpellSetupWizard then DF:ShowTargetedSpellSetupWizard() end end), 34) @@ -6878,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) @@ -6921,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) @@ -7467,12 +7467,6 @@ function DF:SetupGUIPages(GUI, CreateCategory, CreateSubTab, BuildPage) if DF.TogglePersonalTargetedSpells then DF:TogglePersonalTargetedSpells(db.personalTargetedSpellEnabled) end end), 30) settingsGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Important Spells Only"], db, "personalTargetedSpellImportantOnly", PersonalTargetedUpdate), 30) - -- Icon display knobs (NOT border settings — kept out of the Border group so - -- that group is purely the shared DF.Border toolkit). - local ptsAlpha = settingsGroup:AddWidget(GUI:CreateSlider(self.child, L["Alpha"], 0.0, 1.0, 0.05, db, "personalTargetedSpellAlpha", PersonalTargetedUpdate, PersonalTargetedUpdate, true), 55) - ptsAlpha.disableOn = HidePersonalOptions - local ptsSwipe = settingsGroup:AddWidget(GUI:CreateCheckbox(self.child, L["Show Cooldown Swipe"], db, "personalTargetedSpellShowSwipe", PersonalTargetedUpdate), 30) - ptsSwipe.disableOn = HidePersonalOptions Add(settingsGroup, nil, 1) -- ===== CONTENT TYPES GROUP (Column 2) ===== @@ -7511,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) @@ -7560,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) From 38bf01eb9cea653583c22da8b678786e27c9ffd5 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 13:58:18 +0100 Subject: [PATCH 29/34] Changelog: Targeted Spells border rework + border-engine fixes --- Changelog.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.lua b/Changelog.lua index acf1f271..9af2b03e 100644 --- a/Changelog.lua +++ b/Changelog.lua @@ -20,6 +20,7 @@ DF.CHANGELOG_TEXT = [===[ ### 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 @@ -33,6 +34,8 @@ DF.CHANGELOG_TEXT = [===[ * (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] From c6e2baae09aa9309a8f55bddfb188abda4c0d4f5 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 21:14:04 +0100 Subject: [PATCH 30/34] Targeted Spells: fix Important Spell Border migration dropping old settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MigrateTargetedSpellImportantBorder's mapHighlight guarded each copy on the new …ImportantBorder* key being nil, but the ADDON_LOADED default-merge populates those keys before the migration runs, so the guard never fired and existing Highlight settings never carried into the new keys. Gate solely on the per-profile _tsImportantBorderV1 / _personalTsImportantBorderV1 flags (one run) and copy the old values unconditionally, mirroring MigrateBorderInsetFold. --- Core.lua | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Core.lua b/Core.lua index e8753980..822b3947 100644 --- a/Core.lua +++ b/Core.lua @@ -3520,22 +3520,26 @@ function DF:MigrateTargetedSpellImportantBorder() 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 when the new key is still nil. Value-idempotent; - -- shared by the group (targetedSpell) and personal (personalTargetedSpell) sets. + -- 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 and m[p.."ImportantBorderColor"] == nil then + if m[p.."HighlightColor"] ~= nil then m[p.."ImportantBorderColor"] = m[p.."HighlightColor"] - if m[p.."ImportantBorderAnimationColor"] == nil then - m[p.."ImportantBorderAnimationColor"] = m[p.."HighlightColor"] - end + m[p.."ImportantBorderAnimationColor"] = m[p.."HighlightColor"] end - if m[p.."HighlightSize"] ~= nil and m[p.."ImportantBorderSize"] == nil then + if m[p.."HighlightSize"] ~= nil then m[p.."ImportantBorderSize"] = m[p.."HighlightSize"] end - if m[p.."HighlightInset"] ~= nil and m[p.."ImportantBorderInset"] == nil then + if m[p.."HighlightInset"] ~= nil then m[p.."ImportantBorderInset"] = m[p.."HighlightInset"] end - if m[p.."HighlightStyle"] ~= nil and m[p.."ImportantBorderAnimationType"] == nil then + if m[p.."HighlightStyle"] ~= nil then m[p.."ImportantBorderAnimationType"] = styleToAnim[m[p.."HighlightStyle"]] or "PROC" end end From d351bb390b9bbf77f13dae647742fbd4a4020179 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 21:16:01 +0100 Subject: [PATCH 31/34] Changelog: add #164 entries to CHANGELOG.md (the build's source) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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] From 959b00794a773bd752d8e0aaa3bf621e8435d459 Mon Sep 17 00:00:00 2001 From: Krathe Date: Tue, 16 Jun 2026 23:59:36 +0100 Subject: [PATCH 32/34] Export: include targeted-spell border + Important Spell Border keys --- ExportCategories.lua | 79 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) 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", From 24a92a1cd00b2204b107898f689df8ada1540b62 Mon Sep 17 00:00:00 2001 From: Krathe Date: Wed, 17 Jun 2026 00:16:36 +0100 Subject: [PATCH 33/34] Changelog: drop Changelog.lua edit (generated from CHANGELOG.md at build) --- Changelog.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/Changelog.lua b/Changelog.lua index 9af2b03e..acf1f271 100644 --- a/Changelog.lua +++ b/Changelog.lua @@ -20,7 +20,6 @@ DF.CHANGELOG_TEXT = [===[ ### 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 @@ -34,8 +33,6 @@ DF.CHANGELOG_TEXT = [===[ * (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] From 89c8d7f2a850ce442324a4e67c8cf9366b5febb1 Mon Sep 17 00:00:00 2001 From: Krathe Date: Wed, 17 Jun 2026 00:35:18 +0100 Subject: [PATCH 34/34] Targeted Spells: re-anchor Important-border migration to merge cleanly with the OOR migration --- Core.lua | 80 ++++++++++++++++++++++++++--------------------------- Profile.lua | 7 +++-- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/Core.lua b/Core.lua index 822b3947..1a351b16 100644 --- a/Core.lua +++ b/Core.lua @@ -3509,6 +3509,40 @@ local function ZeroBuffDebuffBorderInset(profile) end end +-- One-shot per-profile, two independently-guarded steps so a profile already +-- through step 1 still receives step 2. Both steps are value-idempotent. +function DF:MigrateBorderInsetFold() + if not DandersFramesDB_v2 or not DandersFramesDB_v2.profiles then return end + for _, profile in pairs(DandersFramesDB_v2.profiles) do + if type(profile) == "table" then + -- Step 1: AD icon/square fold (preset libraries + inline configs). + if not profile._borderInsetFoldV1 then + local seen = {} + -- Canonical store when Designer Presets exist (nil-guarded so this + -- stays correct where they don't). + local lib = profile.auraDesignerPresets + if type(lib) == "table" then + for _, presetCfg in pairs(lib) do + FoldAuraDesignerConfig(presetCfg, seen) + end + end + if type(profile.party) == "table" then + FoldAuraDesignerConfig(profile.party.auraDesigner, seen) + end + if type(profile.raid) == "table" then + FoldAuraDesignerConfig(profile.raid.auraDesigner, seen) + end + profile._borderInsetFoldV1 = true + end + -- Step 2: zero buff/debuff border inset (mode-level + raid overrides). + if not profile._buffDebuffInsetZeroV1 then + ZeroBuffDebuffBorderInset(profile) + profile._buffDebuffInsetZeroV1 = true + end + end + 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 @@ -3566,40 +3600,6 @@ function DF:MigrateTargetedSpellImportantBorder() end end --- One-shot per-profile, two independently-guarded steps so a profile already --- through step 1 still receives step 2. Both steps are value-idempotent. -function DF:MigrateBorderInsetFold() - if not DandersFramesDB_v2 or not DandersFramesDB_v2.profiles then return end - for _, profile in pairs(DandersFramesDB_v2.profiles) do - if type(profile) == "table" then - -- Step 1: AD icon/square fold (preset libraries + inline configs). - if not profile._borderInsetFoldV1 then - local seen = {} - -- Canonical store when Designer Presets exist (nil-guarded so this - -- stays correct where they don't). - local lib = profile.auraDesignerPresets - if type(lib) == "table" then - for _, presetCfg in pairs(lib) do - FoldAuraDesignerConfig(presetCfg, seen) - end - end - if type(profile.party) == "table" then - FoldAuraDesignerConfig(profile.party.auraDesigner, seen) - end - if type(profile.raid) == "table" then - FoldAuraDesignerConfig(profile.raid.auraDesigner, seen) - end - profile._borderInsetFoldV1 = true - end - -- Step 2: zero buff/debuff border inset (mode-level + raid overrides). - if not profile._buffDebuffInsetZeroV1 then - ZeroBuffDebuffBorderInset(profile) - profile._buffDebuffInsetZeroV1 = 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 @@ -5343,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 @@ -5361,12 +5367,6 @@ DF._MainEventDispatcher = function(self, event, arg1) DF:MigrateBorderInsetFold() 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 - -- CRITICAL: Update power bars now that unit data is available -- At ADDON_LOADED, UnitPower() etc may return 0 before player is loaded -- Power bar updates don't require combat protection diff --git a/Profile.lua b/Profile.lua index 5819721b..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 @@ -306,9 +310,6 @@ function DF:SetProfile(name) if DF.MigrateBorderInsetFold then DF:MigrateBorderInsetFold() end - if DF.MigrateTargetedSpellImportantBorder then - DF:MigrateTargetedSpellImportantBorder() - end -- Apply the profile — runtime state is already clear so the proxy reads -- the new profile directly with no stale overlay