Skip to content

Feature Request: Proper fading #231

@Vortikai

Description

@Vortikai

As of now, Actionbars -> Visibility has the options to Show on Hover Only and Show in Combat Only. The feature hide the bars chosen, and reveals immediately with a pop-in effect, like so:

20260505-1331-45.1594432.mp4

Current DragonUI visibility options

The DragonUI visibility options also are limited to Hover, Combat, or Hover AND in Combat, but not Hover OR in Combat. This is a bit limited.

My requests are:

  1. To make these frames properly fade smoothly, rather than immedaitely go from opacity 0 to 1 (fast reveal), then 1 to 0 (immedaitely hide)
  2. Add in the option to show if in combat, OR if I hover over the action bar (Hover OR in combat)
  3. Fade as Group option that when selected for an action bar, fades and shows all frames with that checkbox as a group. An example: Bottom and Right Action Bars, if selected to fade as a group, both show and hide together based on other conditions.
  4. Immediately add visibility options to the bag bar and micro bar.

I currently have an addon written with AI that fades frames on certain conditions. I'm struggling with it and think it'll just be easier if it were implemented in DragonUI.

20260505-1329-41.4014563.mp4

Proper fade goal with my addon the AI made

I'm willing to help with this if needed. Here's the Lua I've been using:

-- FadeFrameUI (WoW 3.3.5a)

local mouseInterval = 0.05
local fadeInTime = 0.2
local fadeOutTime = 1.0
local fadeIntermediate = 0.4
local fadeIdleDelay = 5.0
local fadeOutDelay = .25

local ENABLED_FADE_GROUPS = {
    actionBars = true,
    microBar = true,
    bagBar = true,
    mainMenuBar = true,
}

local FrameRegistry = {
    frames = {},
    names = {},
    groups = {},
}

local opacity = {
    actionBars = 0,
    mainMenuBar = 0.2,
    bagBar = 0,
    microBar = 0,
    minimap = 0,
    player = 0,
    buffs = 0,
    buffButtons = 0,
    debuffs = 1,
    combat = 1,
    target = 1,
    lowHealth = 1,
    casting = 1,
    watch = 0,
    party = 0,
    raid = 0,
    chatButtons = 0,
}

local GroupVisibilityRules = {
    player = {
        combat = 1,
        target = 1,
        lowHP = 1,
        casting = 1,
        idle = 0,
    },

    party = {
        combat = 1,
        target = 1,
        lowHP = 1,
        casting = 1,
        idle = 0,
    },

    raid = {
        combat = 1,
        target = 1,
        lowHP = 1,
        casting = 1,
        idle = 0,
    },

    actionBars = {
        combat = 1,
        target = 1,
        lowHP = 0,
        casting = 0,
        idle = 0,
    },

    microBar = {
        combat = 1,
        target = 1,
        lowHP = 0,
        casting = 0,
        idle = 0,
    },

    minimap = {
        combat = 1,
        target = 1,
        lowHP = 0,
        casting = 0,
        idle = 0,
    },
}

local frameTargetAlpha = {}
local FrameTreeScanned = {}
local frameLastShown = {}
local MinimapBorderTop = _G["MinimapBorderTop"]
local watchRescanQueued = false

local HOVER_EXPAND = {
    player = { "player", "party", "raid" },
    party = { "player", "party", "raid" },

    raid = { "player", "party", "raid" },

    mainMenuBar = { "mainMenuBar" },
    actionBars = { "actionBars", "mainMenuBar" },

    microBar = { "microBar", "bagBar" },
    bagBar = { "microBar", "bagBar" },

    watch = { "watch", "minimap" },
    minimap = { "watch", "minimap" },

    buffs = { "buffs" },
}

function FrameRegistry:Register(frame, group)

    local name = frame:GetName()

    if not frame then return end

    if not ENABLED_FADE_GROUPS[group] then
        return
    end

    if group == "microBar" and self.frames[frame] then
        return
    end

    self.frames[frame] = group

    if group then

        self.groups[group] = self.groups[group] or {
            set = {},
            list = {}
        }

        local g = self.groups[group]

        if g.set[frame] then return end

        g.set[frame] = true
        table.insert(g.list, frame)
    end

    if name then
        self.names[name] = frame
    end
end

local function GetPriorityState(state)
    -- highest priority first

    if state.inCombat then
        return "combat"
    end

    if state.targetExists then
        return "target"
    end

    if state.lowHP then
        return "lowHP"
    end

    if state.casting then
        return "casting"
    end

    return "idle"
end

local function IsUnitLowHP(unit)
    if not UnitExists(unit) then return false end

    local hp = UnitHealth(unit)
    local max = UnitHealthMax(unit)
    if not max or max == 0 then return false end

    return (hp / max) < 0.8
end

local function AreRunesOnCooldown()
    if not RuneFrame or not RuneButtonIndividual1 then return false end

    for i = 1, 6 do
        local start, duration = GetRuneCooldown(i)
        if start and duration and duration > 0 then
            if (start + duration - GetTime()) > 0.1 then
                return true
            end
        end
    end

    return false
end

local function IsDescendantOf(frame, parent)
    if not frame or not parent then return false end

    while frame do
        if frame == parent then
            return true
        end
        frame = frame:GetParent()
    end

    return false
end

local function SafeWatchScan(frame, group)
    if not frame then return end
    if FrameTreeScanned[frame] then return end

    FrameTreeScanned[frame] = true
    local objType = frame:GetObjectType()
    FrameRegistry:Register(frame, group)

    if objType == "FontString" and frame.GetParent then
        FrameRegistry.frames[frame] = group
    end

    if frame.GetChildren then
        local children = { frame:GetChildren() }
        for _, child in ipairs(children) do
            SafeWatchScan(child, group)
        end
    end
end

local function RegisterWatchFrameTree()
    if not WatchFrame then return end

    SafeWatchScan(WatchFrame, "watch")
end

local function GetGroupOpacity(group)
    if not group then return 0 end

    local p = opacity[group]
    if p ~= nil then return p end

    if group:sub(1,5) == "party" then
        return opacity.party
    end

    if group:sub(1,11) == "raid_member"
    or group:sub(1,10) == "raid_group" then
        return opacity.raid
    end

    return 0
end

local function HasExpiringBuff()
    for i=1,40 do
        local buff = _G["BuffButton"..i]

        if buff and buff:IsVisible() then

            if buff.flash and buff.flash:IsShown() then
                return true
            end

    if buff.flashing then
        return true
    end

            local _,_,_,_,_,duration,expirationTime = UnitBuff("player", i)

            if expirationTime and duration and duration > 0 then
                local remain = expirationTime - GetTime()

                if remain > 0 and remain <= 50 then
                    return true
                end
            end
        end
    end

    return false
end

local function SafeRegisterRaid(frame)
    if not frame then return end

    FrameRegistry:Register(frame, "raid")

    if frame.GetChildren then
        local children = { frame:GetChildren() }
        for _, child in ipairs(children) do
            if child then
                SafeRegisterRaid(child)
            end
        end
    end
end

local function IsHovered(group, hoveredGroup)
    if not hoveredGroup then return false end

    local set = HOVER_EXPAND[hoveredGroup]
    if set then
        for _, g in ipairs(set) do
            if group == g then return true end
            if g == "party" and group:sub(1,5) == "party" then return true end
            if g == "raid" and group:sub(1,4) == "raid" then return true end
        end
    end

    return false
end

local function NormalizeGroup(group)
    if not group then return nil end
    if group:match("^raid") then return "raid" end
    if group:match("^party") then return "party" end
    return group
end

local function ResetFrameAlphaCache()
    frameTargetAlpha = {}
end

local function GetTargetAlpha(frame, group, state)

    -- =========================
    -- 1. PRIORITY STATE ENGINE
    -- =========================
    local priority = GetPriorityState(state)

--    if group == "buffButtons" then
--        group = "buffs"
--    end

    local rules = GroupVisibilityRules[group]
    local stateValue = nil

    if rules then
        stateValue = rules[priority]
    end

    -- default fallback state
    if stateValue == nil then
        stateValue = 0
    end


    -- =========================
    -- 2. HARD OVERRIDES (absolute visibility)
    -- =========================
    if state.inCombat
    or state.targetExists then
        stateValue = math.max(stateValue, 1)
    end

    if group == "player" then
        if IsUnitLowHP("player") or AreRunesOnCooldown() then
            stateValue = 1
        end
    end

    if group == "debuffs" then
        return 1
    end

    if group == "minimapCore" then
        return 1
    end

    if group == "buffs" then
        if HasExpiringBuff() then
            return 1
        end
    end


    -- =========================
    -- 3. HOVER OVERRIDE
    -- =========================
    if state.hoveredGroup then
        if IsHovered(group, state.hoveredGroup) then
            stateValue = 1
        end
    end


    -- =========================
    -- 4. FADE ENGINE (CRITICAL FIX)
    -- =========================
    if stateValue >= 1 then
        return 1
    end

    -- THIS restores your original behavior:
    -- idle -> fadeIntermediate -> hidden
    local base = GetGroupOpacity(group)

    -- restore idle visibility scaling properly
    if stateValue > 0 then
        return fadeIntermediate
    end

    return base
end

local function RegisterFrameTree(root, group)
    if not root then return end
    if FrameTreeScanned[root] then return end

    local normGroup = NormalizeGroup(group)

    local stack = { root }

    while #stack > 0 do
        local frame = table.remove(stack)

        if frame and not FrameTreeScanned[frame] then
            FrameTreeScanned[frame] = true

            FrameRegistry:Register(frame, normGroup)

            local children = { frame:GetChildren() }
            for _, child in ipairs(children) do
                table.insert(stack, child)
            end
        end
    end
end

local function FadeFrameTo(frame, targetAlpha)
    if not frame then return end

    frame:SetScript("OnUpdate", nil)

    local current = frame:GetAlpha()
    if math.abs(current - targetAlpha) < 0.01 then return end

    if InCombatLockdown() then
        frame:SetAlpha(targetAlpha)
        return
    end

    local fadeTime = (targetAlpha > current) and fadeInTime or fadeOutTime
    local startTime = GetTime()

    frame:SetScript("OnUpdate", function(self)
        local elapsed = GetTime() - startTime
        local progress = elapsed / fadeTime

        if progress >= 1 then
            self:SetAlpha(targetAlpha)
            self:SetScript("OnUpdate", nil)
        else
            self:SetAlpha(current + (targetAlpha - current) * progress)
        end
    end)
end

local function InitializeFadeFrameUI()
    -- =========================
    -- MAIN ACTION BAR SYSTEM
    -- =========================
    FrameRegistry:Register(MainMenuBar, "mainMenuBar")

    for i = 1, 12 do
        local btn = _G["ActionButton" .. i]
        if btn then
            FrameRegistry:Register(btn, "mainMenuBar")
        end
    end

    if MultiBarBottomLeft then
        FrameRegistry:Register(MultiBarBottomLeft, "actionBars")
    end
    if MultiBarBottomRight then
        FrameRegistry:Register(MultiBarBottomRight, "actionBars")
    end
    if MultiBarLeft then
        FrameRegistry:Register(MultiBarLeft, "actionBars")
    end
    if MultiBarRight then
        FrameRegistry:Register(MultiBarRight, "actionBars")
    end
    if StanceBarFrame then
        FrameRegistry:Register(StanceBarFrame, "mainMenuBar")
    end

    if MainMenuBarArtFrame then
       FrameRegistry:Register(MainMenuBarArtFrame,"mainMenuBar")
    end

    if MainMenuExpBar then
       FrameRegistry:Register(MainMenuExpBar,"mainMenuBar")
    end


    -- =========================
    -- BAG SYSTEM
    -- =========================
    if MainMenuBarBackpackButton then
        FrameRegistry:Register(MainMenuBarBackpackButton, "bagBar")
    end

    for i = 0, 3 do
        local bag = _G["CharacterBag" .. i .. "Slot"]
        if bag then
            FrameRegistry:Register(bag, "bagBar")
        end
    end

    if KeyRingButton then
        FrameRegistry:Register(KeyRingButton, "bagBar")
    end


    -- =========================
    -- MICRO MENU
    -- =========================

    if MICRO_BUTTONS then
        for _, btn in ipairs(MICRO_BUTTONS) do
            if btn then
                FrameRegistry:Register(btn, "microBar")
            end
        end
    end

    local function SafeRegisterMicro(frame)
        if not frame then return end

        if frame:IsObjectType("Button") then
            FrameRegistry:Register(frame, "microBar")
        end

        for i = 1, frame:GetNumChildren() do
            local child = select(i, frame:GetChildren())
            if child and child:IsObjectType("Button") then
                FrameRegistry:Register(child, "microBar")
            end
        end
    end

--    SafeRegisterMicro(pUiMicroMenu)
--    SafeRegisterMicro(DragonUI_MicroMenu)
--    SafeRegisterMicro(DragonUI_MicroMenu_FULLSCREEN)


    -- =========================
    -- PLAYER SYSTEM
    -- =========================
    if PlayerFrame then
        FrameRegistry:Register(PlayerFrame, "player")
    end

    if PetFrame then
        FrameRegistry:Register(PetFrame, "player")
    end

    if RuneFrame then
        FrameRegistry:Register(RuneFrame, "player")
    end


    -- =========================
    -- BUFFS / WATCH
    -- =========================
-- BUFFS / WATCH (SAFE MODE)
-- Do NOT fade BuffFrame; Blizzard controls buff animation

    if BuffFrame and BuffFrame.toggleButton then
        FrameRegistry:Register(BuffFrame.toggleButton, "buffs")
    end

    -- Register individual buff buttons again so they can fade independently
    for i = 1,40 do
        local buff = _G["BuffButton"..i]
        if buff then
            FrameRegistry:Register(buff, "buffButtons")
        end
    end

    if RUI_BuffCollapseButton then
        FrameRegistry:Register(RUI_BuffCollapseButton, "buffs")
    end

    if ObjectiveTrackerBlocksFrame then
        FrameRegistry:Register(ObjectiveTrackerBlocksFrame, "watch")
    end
 --   if DragonUI_QuestTrackerFrame then
 --       FrameRegistry:Register(DragonUI_QuestTrackerFrame, "watch")
 --   end

    RegisterWatchFrameTree()
    
    -- =========================
    -- PARTY FRAMES
    -- =========================
    for i = 1, 4 do
        local f = _G["PartyMemberFrame" .. i]
        if f then
            FrameRegistry:Register(f, "party" .. i)
        end
    end


    -- =========================
    -- RAID / PULL OUT FRAMES
    -- =========================
    for i = 1, 10 do
        local pullout = _G["RaidPullout" .. i]
        if pullout then
            SafeRegisterRaid(pullout)
        end
    end


    -- =========================
    -- MINIMAP CORE (NON-FADING) The minimap has a bug where if it is affected by alpha, it may disappear completely. I disabled the minimap from the minimap group and added it to the core.
    -- =========================
    if Minimap then
        FrameRegistry:Register(Minimap, "minimapCore")
    end

    local minimapFrames = {
        MinimapZoomIn,
        MinimapZoomOut,
        MinimapZoneTextButton,
        MiniMapTracking,
        MiniMapMailFrame,
        MiniMapBattlefieldFrame,
        GameTimeFrame,
        TimeManagerClockButton,
    }

    if MinimapBorderTop then
        FrameRegistry:Register(MinimapBorderTop, "watch")
    end

    for _, f in ipairs(minimapFrames) do
        if f then
            FrameRegistry:Register(f, "watch")
        end
    end

    if RUI_MinimapFrame then
        FrameRegistry:Register(RUI_MinimapFrame, "minimap")
    end

    if MinimapZoneTextButton and MinimapZoneTextButton.GetRegions then
        local regions = { MinimapZoneTextButton:GetRegions() }
        for _, r in ipairs(regions) do
            if r then
                FrameRegistry:Register(r, "watch")
            end
        end
    end


    -- =========================
    -- DRAGON UI / CUSTOM UI
    -- =========================
    if pUiMainBar then
        FrameRegistry:Register(pUiMainBar, "mainMenuBar")
    end

    if DragonUI_MainBar then
        FrameRegistry:Register(DragonUI_MainBar, "mainMenuBar")
    end

    if pUiMainBarArt then
        FrameRegistry:Register(pUiMainBarArt, "mainMenuBar")
    end

    if DragonUI_XPBar then
        FrameRegistry:Register(DragonUI_XPBar, "mainMenuBar")
    end

    if pUiStanceBar then
        FrameRegistry:Register(pUiStanceBar, "mainMenuBar")
    end


    -- =========================
    -- CHAT
    -- =========================
   -- FrameRegistry:Register(ChatFrameMenuButton, "chatButtons")
   -- FrameRegistry:Register(FriendsMicroButton, "chatButtons")
   -- FrameRegistry:Register(ChatFrame1ButtonFrameUpButton, "chatButtons")
   -- FrameRegistry:Register(ChatFrame1ButtonFrameDownButton, "chatButtons")
   -- FrameRegistry:Register(ChatFrame1ButtonFrameBottomButton, "chatButtons")

    -- =========================
    -- RETAIL UI COMPAT (RUI)
    -- =========================
    if RUI_BagsBar then
        for i = 1, RUI_BagsBar:GetNumChildren() do
            local child = select(i, RUI_BagsBar:GetChildren())
            if child and child:IsObjectType("Button") then
                FrameRegistry:Register(child, "bagBar")
            end
        end
    end

    if RUI_ActionBar1 then
        FrameRegistry:Register(RUI_ActionBar1, "mainMenuBar")
    end

    if RUI_ActionBar6 then
        FrameRegistry:Register(RUI_ActionBar6, "mainMenuBar")
    end
end

    -- =========================
    -- ONUPDATE LOOP
    -- =========================

local function OnUpdate(self, elapsed)

    local now = GetTime()

    -- =========================
    -- HOVER DETECTION
    -- =========================
    local function GetHoveredGroup()
        local f = GetMouseFocus()
        if not f or f == WorldFrame then
            return nil
        end

        while f do
            if WatchFrame and (
                f == WatchFrame
                or f == WatchFrameLines
                or f == WatchFrameScrollChildFrame
                or IsDescendantOf(f, WatchFrame)
            ) then
                return "watch"
            end

            local group = FrameRegistry.frames[f]

            if not group then
                local p = f:GetParent()
                while p and not group do
                    group = FrameRegistry.frames[p]
                    p = p:GetParent()
                end
            end

            if group then
                return NormalizeGroup(group)
            end

            f = f:GetParent()
        end

        return nil
    end

    local hoveredGroup = GetHoveredGroup()


    -- =========================
    -- STATE
    -- =========================

    local hp = UnitHealth("player") or 0
    local maxhp = UnitHealthMax("player") or 1

    local state = {
        now = now,
        inCombat = UnitAffectingCombat("player") and true or false,
        casting = (UnitCastingInfo("player") or UnitChannelInfo("player")) and true or false,
        targetExists = UnitExists("target") and true or false,
        lowHP = (hp / maxhp) < 0.8,
        runes = AreRunesOnCooldown(),
        hoveredGroup = hoveredGroup,
    }

    -- =========================
    -- APPLY FADES
    -- =========================

    if UnitInRaid("player") then
        for i=1,10 do
            local pullout = _G["RaidPullout"..i]
            if pullout and not FrameRegistry.frames[pullout] then
                SafeRegisterRaid(pullout)
            end
        end
    end

    if RUI_BuffCollapseButton and not FrameRegistry.frames[RUI_BuffCollapseButton] then
        FrameRegistry:Register(RUI_BuffCollapseButton, "buffs")
    end

    for group, data in pairs(FrameRegistry.groups) do
    for _, frame in ipairs(data.list) do
        local target = GetTargetAlpha(frame, group, state)

        if frameTargetAlpha[frame] ~= target then
            frameTargetAlpha[frame] = target
            FadeFrameTo(frame, target)
        end
    end
end
--    SyncDragonUIGlows()
end

    -- =========================
    -- Addon Protection
    -- =========================

local function OnAddonLoaded(self, event, addon)
    if addon == "Blizzard_TimeManager" and TimeManagerClockButton then
        FrameRegistry:Register(TimeManagerClockButton, "minimap")
    end
end


local clockWatcher = CreateFrame("Frame")
clockWatcher:RegisterEvent("ADDON_LOADED")
clockWatcher:SetScript("OnEvent", OnAddonLoaded)

local watchDirty = false

hooksecurefunc("WatchFrame_Update", function()
    watchDirty = true
    watchRescanQueued = true
end)

-- local function OnUpdateWatch()
--     if watchDirty then
--         watchDirty = false
--         SafeWatchRescan()
--     end
-- end

local function ReconcileMicroButtons()
    local names = {
        "CharacterMicroButton",
        "SpellbookMicroButton",
        "TalentMicroButton",
        "AchievementMicroButton",
        "QuestLogMicroButton",
        "SocialsMicroButton",
        "LFDMicroButton",
        "MainMenuMicroButton",
        "HelpMicroButton",
        "CollectionsMicroButton",
        "PVPMicroButton",
    }

    for _, name in ipairs(names) do
        local f = _G[name]
        if f then
            FrameRegistry:Register(f, "microBar")
        end
    end
end

local loginFrame = CreateFrame("Frame")
loginFrame:RegisterEvent("PLAYER_LOGIN")
loginFrame:SetScript("OnEvent", function()
    ZoneTextFrame:ClearAllPoints()
    ZoneTextFrame:SetPoint("TOP", UIParent, "TOP", 0, -90)

    InitializeFadeFrameUI()
    ReconcileMicroButtons()

    WatchFrame:SetUserPlaced(true)
    WatchFrame:SetMovable(true)
    WatchFrame:ClearAllPoints()
    WatchFrame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", -40, -200)
    WatchFrame:SetHeight(600)

WatchFrame:SetAlpha(1)
WatchFrameLines:SetAlpha(1)
    ResetFrameAlphaCache()
end)

local updater = CreateFrame("Frame")
updater:SetScript("OnUpdate", OnUpdate)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions