-- Main action bar: the client's secure action buttons, kept and reskinned (camelot -- ActionButtonTemplate, MainActionBar), with the bar border, page arrows and end caps (gryphons, -- wyverns for the Horde). -- Recreating the buttons would mean rewriting spells, macros and drag and drop, and risk taint. -- Nothing is resized or moved in combat, where the client forbids it. -- camelot ActionButtonTemplate: 45 x 45; shared/ActionBar.lua minButtonPadding = 2 local BUTTON_SIZE = 45 local BUTTON_PADDING = 2 local BUTTON_PITCH = BUTTON_SIZE + BUTTON_PADDING local FRAME_WIDTH, FRAME_HEIGHT = 46, 45 -- size of the four state textures local BUTTON_COUNT = 12 local END_CAP_WIDTH, END_CAP_HEIGHT = 154, 95 local END_CAP_DROP = -2 -- measured on screen: the art bottom drops 2 px below the bar local L = ForeverUI.L local ATLAS = { normal = "ui-hud-actionbar-iconframe", pushed = "ui-hud-actionbar-iconframe-down", highlight = "ui-hud-actionbar-iconframe-mouseover", border = "ui-hud-actionbar-iconframe-border", slot = "ui-hud-actionbar-iconframe-slot", background = "ui-hud-actionbar-iconframe-background", flash = "ui-hud-actionbar-iconframe-flash", } local BARS = { "ActionButton", "MultiBarBottomLeftButton", "MultiBarBottomRightButton", "MultiBarRightButton", "MultiBarLeftButton", "BonusActionButton", } -- The frame is not the button's NormalTexture: ActionButton_Update sets UI-Quickslot2 on -- every refresh and ActionButton_ShowGrid recolors it, erasing any skin. So the client's -- texture is silenced and we draw our own frame. add: use the ADD blend mode. local function setStateTexture(texture, atlas, add) if not texture then return end ForeverUI.SetAtlas(texture, atlas, true) texture:SetWidth(FRAME_WIDTH) texture:SetHeight(FRAME_HEIGHT) texture:ClearAllPoints() texture:SetPoint("TOPLEFT", 0, 0) texture:SetBlendMode(add and "ADD" or "BLEND") end -- Hide again what the client just rewrote. local function silenceNormalTexture(button) local normal = button:GetNormalTexture() if normal then normal:SetAlpha(0) normal:SetVertexColor(1, 1, 1, 0) end end -- 3.3.5 trap: ActionButton_Update and ActionButton_HideGrid hide the WHOLE empty button while -- its "showgrid" ATTRIBUTE is 0, and it only rises during a drag, so empty slots vanish, in -- combat too (a stance change updates every button). Keeping the attribute at 1 makes the -- client keep empty buttons shown. A secure frame changes only out of combat; -- PLAYER_REGEN_ENABLED brings back anything hidden meanwhile. local function keepGrid(button) if not button or InCombatLockdown() or button:GetAttribute("statehidden") then return end if (button:GetAttribute("showgrid") or 0) < 1 then button:SetAttribute("showgrid", 1) end if not button:IsShown() then button:Show() end end local function skinButton(button) if not button or button.foreverSkinned then return end local name = button:GetName() if not name then return end local icon = _G[name .. "Icon"] local border = _G[name .. "Border"] local hotkey = _G[name .. "HotKey"] local count = _G[name .. "Count"] local macro = _G[name .. "Name"] local cooldown = _G[name .. "Cooldown"] local flash = _G[name .. "Flash"] local floatingBG = _G[name .. "FloatingBG"] button:SetWidth(BUTTON_SIZE) button:SetHeight(BUTTON_SIZE) -- Empty slot, under the icon. local background = button:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(background, ATLAS.background, true) background:SetAllPoints(button) local slot = button:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(slot, ATLAS.slot, true) slot:SetAllPoints(button) button.foreverBackground = background button.foreverSlot = slot if icon then -- The icon fills the button, as in camelot; 3.3.5 has no MaskTexture, so the frame's -- solid corners cover the icon's corners. icon:ClearAllPoints() icon:SetAllPoints(button) icon:SetTexCoord(0, 1, 0, 1) icon:SetDrawLayer("BORDER") end -- Our frame, above the icon and out of the client's reach. local frame = button:CreateTexture(nil, "ARTWORK") setStateTexture(frame, ATLAS.normal) button.foreverFrame = frame silenceNormalTexture(button) setStateTexture(button:GetPushedTexture(), ATLAS.pushed) setStateTexture(button:GetHighlightTexture(), ATLAS.highlight) setStateTexture(button:GetCheckedTexture(), ATLAS.highlight, true) if flash then ForeverUI.SetAtlas(flash, ATLAS.flash) flash:ClearAllPoints() flash:SetPoint("TOPLEFT", 0, 0) end if border then -- The client's equipped border is green and square; camelot uses its own, hidden by default. ForeverUI.SetAtlas(border, ATLAS.border) border:ClearAllPoints() border:SetPoint("TOPLEFT", 0, 0) end if floatingBG then floatingBG:SetAlpha(0) end -- SetFontObject RESETS the justification, so set it AFTER. The number fonts have no -- justifyH (client FontStyles.xml), so they are centered by default. if hotkey then hotkey:SetWidth(32) hotkey:SetHeight(10) hotkey:ClearAllPoints() hotkey:SetPoint("TOPRIGHT", -5, -5) hotkey:SetFontObject(NumberFontNormalSmallGray) hotkey:SetJustifyH("RIGHT") end if count then count:ClearAllPoints() count:SetPoint("BOTTOMRIGHT", -5, 5) count:SetFontObject(NumberFontNormal) count:SetJustifyH("RIGHT") end if macro then macro:SetWidth(36) macro:SetHeight(10) macro:ClearAllPoints() macro:SetPoint("BOTTOM", 0, 2) macro:SetFontObject(GameFontHighlightSmallOutline) end if cooldown and icon then cooldown:ClearAllPoints() cooldown:SetPoint("TOPLEFT", icon, "TOPLEFT", 3, -3) cooldown:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -3, 3) end keepGrid(button) button.foreverSkinned = true end -- ---------- Bar local holder = CreateFrame("Frame", "ForeverUIActionBarHolder", UIParent) holder:SetWidth(BUTTON_COUNT * BUTTON_SIZE + (BUTTON_COUNT - 1) * BUTTON_PADDING) holder:SetHeight(BUTTON_SIZE) -- Bar border: camelot's UI-HUD-ActionBar-Frame, one octagonal image, from TOPLEFT (-6, 6) -- to BOTTOMRIGHT (4, -5). Nine-sliced so the corner bevels keep their size. local border = CreateFrame("Frame", nil, holder) border:SetPoint("TOPLEFT", -6, 6) border:SetPoint("BOTTOMRIGHT", 4, -5) border:SetFrameLevel(holder:GetFrameLevel()) ForeverUI.SetBarFrameArt(border) -- Page block (mainline/MainActionBar.xml ActionBarPageNumber): BOTTOMRIGHT on the bar's -- BOTTOMLEFT at (-4, 9); number 17 x 10 centered at -1, two 17 x 14 arrows at +10 and -10; -- sized by its children: 17 x 34. local page = CreateFrame("Frame", "ForeverUIActionBarPage", holder) page:SetWidth(17) page:SetHeight(34) page:SetPoint("BOTTOMRIGHT", holder, "BOTTOMLEFT", -4, 9) page:SetFrameLevel(holder:GetFrameLevel() + 11) -- Dividers between buttons: RIGHT on the button's LEFT, offset 5. local dividers = {} -- End caps (gryphons): two 154 x 95 frames filled by their texture, movable on their own -- (Customize UI); BottomBar.lua gives their default positions. local END_CAP_INSET = 30 -- each cap overlaps its bar by 30 px local function createEndCap(name, atlas) local cap = CreateFrame("Frame", name, UIParent) cap:SetWidth(END_CAP_WIDTH) cap:SetHeight(END_CAP_HEIGHT) -- Behind every element of the playing screen (levels 1 and up of the same strata): a cap -- never covers anything. cap:SetFrameStrata(holder:GetFrameStrata()) cap:SetFrameLevel(0) local texture = cap:CreateTexture(nil, "OVERLAY") texture:SetAllPoints(cap) if not ForeverUI.SetAtlas(texture, atlas, true) then cap:Hide() end cap.texture = texture return cap end -- mainline/EditModePresetLayouts.lua: the left cap sits on the bar's LEFT edge, the right -- cap on the bag bar's RIGHT edge, each 30 px inward (BottomBar.lua re-anchors it to the -- bags). Camelot drops the cap 20 px below the bar, off screen for a bar 2 px above the -- screen bottom, so the art bottom is aligned on the bar bottom, then 2 px lower. local leftCap = createEndCap("ForeverUIActionBarLeftCap", "ui-hud-actionbar-gryphon-left") local rightCap = createEndCap("ForeverUIActionBarRightCap", "ui-hud-actionbar-gryphon-right") -- Faction art: camelot always draws the gryphons (camelot/MainMenuBarEndCaps.xml); on the user's -- request (2026-10-18) the Horde gets the wyverns of the same sheet (ui-hud-actionbar-wyvern-*, -- same 240 x 140 size). The faction is read again on PLAYER_ENTERING_WORLD. local function applyFactionCaps() local kind = (UnitFactionGroup and UnitFactionGroup("player") == "Horde") and "wyvern" or "gryphon" if leftCap.kind == kind then return end leftCap.kind = kind ForeverUI.SetAtlas(leftCap.texture, "ui-hud-actionbar-" .. kind .. "-left", true) ForeverUI.SetAtlas(rightCap.texture, "ui-hud-actionbar-" .. kind .. "-right", true) end applyFactionCaps() -- The bonus bar takes the same place. On a stance change 3.3.5 shows BonusActionBarFrame -- over the main bar at its own position, so its buttons are anchored like the main ones and -- follow the holder. Nothing is reparented: buttons follow the client bar's visibility. -- 3.3.5 slides this bar in, but secure buttons cannot move every frame in combat, where -- stances change: they are anchored ONCE to a track (our frame), and the track slides, out -- of combat only (see below). local PLACED_BARS = { "ActionButton", "BonusActionButton" } -- The track covers the holder and shifts vertically while sliding. The duration comes from -- the client when it declares it. local track = CreateFrame("Frame", "ForeverUIBonusSlide", holder) local SLIDE_DURATION = BONUSACTIONBAR_SLIDETIME or 0.2 local SLIDE_DISTANCE = BUTTON_SIZE local function placeTrack(progress) local offset = -SLIDE_DISTANCE * (1 - progress) track:ClearAllPoints() track:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, offset) track:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", 0, offset) end placeTrack(1) -- The slide goes both ways. When hiding, the client sets mode = "hide" and keeps the bar -- shown until the move ends, so the mode is read rather than the visibility alone. Without -- that field the hide is instant: hidden buttons cannot be animated. -- The bonus buttons are anchored to the track, which makes it protected (as the crafting page, -- ProfessionsBook.lua): it never moves in combat. It waits at rest while the bar is hidden and -- goes back to rest when combat starts, so a bar shown in combat appears in place, unslid. track.progress = 1 track.target = 1 track:SetScript("OnUpdate", function(self, elapse) local bar = BonusActionBarFrame local visible = bar and bar:IsShown() if InCombatLockdown() then self.visible = visible return end if visible and not self.visible then self.progress, self.target = 0, 1 -- shown: slides up elseif visible then -- the client is hiding it, or shows it again before the end of its own slide self.target = (bar.mode == "hide") and 0 or 1 else if self.progress ~= 1 then placeTrack(1) -- hidden: back at rest end self.progress, self.target = 1, 1 end self.visible = visible if self.progress ~= self.target then local step = (elapse or 0) / SLIDE_DURATION if self.target > self.progress then self.progress = math.min(self.target, self.progress + step) else self.progress = math.max(self.target, self.progress - step) end placeTrack(self.progress) end end) -- PLAYER_REGEN_DISABLED fires before the lockdown: last moment to put the track at rest track:RegisterEvent("PLAYER_REGEN_DISABLED") track:SetScript("OnEvent", function(self) self.progress, self.target = 1, 1 placeTrack(1) end) ForeverUI.ActionBarSlide = track local function layoutButtons() if InCombatLockdown() then return end for _, prefix in ipairs(PLACED_BARS) do local anchor = (prefix == "BonusActionButton") and track or holder for index = 1, BUTTON_COUNT do local button = _G[prefix .. index] if button then button:ClearAllPoints() button:SetPoint("LEFT", anchor, "LEFT", (index - 1) * BUTTON_PITCH, 0) -- Dividers belong to the holder: one set, placed on the main bar. if prefix == "ActionButton" and index > 1 and not dividers[index] then local divider = ForeverUI.CreateDivider(holder, holder:GetFrameLevel() + 1) divider:SetPoint("TOP", button, "TOP", 0, 0) divider:SetPoint("BOTTOM", button, "BOTTOM", 0, 0) divider:SetPoint("RIGHT", button, "LEFT", 5, 0) dividers[index] = divider end end end end end -- Hide the client's bar art. 3.3.5 MainMenuBar.xml has FOUR sets of pieces, and missing one -- leaves the old dwarf bar behind ours: MainMenuBarTexture0..3 (bar body), -- MainMenuXPBarTexture0..3 (XP bar frame), MainMenuMaxLevelBar0..3 (shown at 80), the -- MainMenuBarLeftEndCap / RightEndCap end caps, and MainMenuBarExpText. local OLD_ART = { "MainMenuBarTexture%d", "MainMenuXPBarTexture%d", "MainMenuMaxLevelBar%d", } -- Bonus bar art: all the frame's own texture regions are hidden (buttons are child frames, -- not regions). The frame also stops taking the mouse: BonusActionBarFrame is 505 x 43, HIGH -- strata, toplevel, enableMouse; invisible, it would still swallow clicks on the micro-menu -- below it. Its buttons keep their own mouse, being child frames. local function clearBonusArt() local bar = BonusActionBarFrame if not bar or not bar.GetRegions then return end if bar.EnableMouse then bar:EnableMouse(false) end local regions = { bar:GetRegions() } for _, region in ipairs(regions) do if region and region.GetObjectType and region:GetObjectType() == "Texture" then region:SetAlpha(0) end end end local function hideOldBarArt() clearBonusArt() for _, model in ipairs(OLD_ART) do for index = 0, 3 do local texture = _G[string.format(model, index)] if texture then texture:SetAlpha(0) end end end for _, name in ipairs({ "MainMenuBarLeftEndCap", "MainMenuBarRightEndCap", "MainMenuBarExpText" }) do local region = _G[name] if region then region:SetAlpha(0) end end if MainMenuBarPageNumber then MainMenuBarPageNumber:SetAlpha(1) MainMenuBarPageNumber:SetWidth(17) MainMenuBarPageNumber:SetHeight(10) MainMenuBarPageNumber:SetJustifyH("CENTER") MainMenuBarPageNumber:ClearAllPoints() MainMenuBarPageNumber:SetPoint("CENTER", page, "CENTER", -1, 0) end -- Page arrows: camelot has its own in the bar atlas. local arrows = { { button = ActionBarUpButton, prefix = "ui-hud-actionbar-pageuparrow", y = 10 }, { button = ActionBarDownButton, prefix = "ui-hud-actionbar-pagedownarrow", y = -10 }, } for _, entry in ipairs(arrows) do local button = entry.button if button and not button.foreverSkinned then ForeverUI.SetAtlas(button:GetNormalTexture(), entry.prefix .. "-up") ForeverUI.SetAtlas(button:GetPushedTexture(), entry.prefix .. "-down") ForeverUI.SetAtlas(button:GetHighlightTexture(), entry.prefix .. "-mouseover") if button.GetDisabledTexture and button:GetDisabledTexture() then ForeverUI.SetAtlas(button:GetDisabledTexture(), entry.prefix .. "-disabled") end button:SetWidth(17) button:SetHeight(14) -- 3.3.5 insets the 32 x 32 arrows by 6 and 7 px: kept on 17 x 14, they leave no -- clickable area. Camelot's arrows have none. button:SetHitRectInsets(0, 0, 0, 0) button:ClearAllPoints() button:SetPoint("CENTER", page, "CENTER", 0, entry.y) button.foreverSkinned = true end end end local function skinAll() for _, prefix in ipairs(BARS) do for index = 1, BUTTON_COUNT do local button = _G[prefix .. index] skinButton(button) keepGrid(button) end end hideOldBarArt() end -- The client rewrites the normal texture on every button update: silence it again after. if hooksecurefunc then hooksecurefunc("ActionButton_Update", function(self) -- a window tab on the bar shows the tab's icon (TabMacros.lua) if ForeverUI.TabMacros then ForeverUI.TabMacros.ShowIcon(self) end if self and self.foreverSkinned then silenceNormalTexture(self) keepGrid(self) end end) hooksecurefunc("ActionButton_ShowGrid", function(self) if self and self.foreverSkinned then silenceNormalTexture(self) end end) -- Here the client hides empty buttons. hooksecurefunc("ActionButton_HideGrid", function(self) if self and self.foreverSkinned then silenceNormalTexture(self) keepGrid(self) end end) end local watcher = CreateFrame("Frame") watcher:RegisterEvent("PLAYER_ENTERING_WORLD") watcher:RegisterEvent("PLAYER_REGEN_ENABLED") watcher:RegisterEvent("ACTIONBAR_PAGE_CHANGED") watcher:RegisterEvent("UPDATE_BONUS_ACTIONBAR") watcher:SetScript("OnEvent", function(_self, event) if event == "PLAYER_ENTERING_WORLD" then applyFactionCaps() end skinAll() layoutButtons() end) skinAll() layoutButtons() ForeverUI.ActionBarHolder = holder ForeverUI.ActionBarBorder = border ForeverUI.ActionBarEndCaps = { left = leftCap, right = rightCap, inset = END_CAP_INSET, drop = END_CAP_DROP, faction = applyFactionCaps } -- camelot/EditModePresetLayoutConstants.lua: BOTTOMRIGHT on the micro-menu's BOTTOMLEFT at -- (-4.5, -4); the micro-menu at BOTTOM (116.5, 6), 275 wide, puts the bar's right edge at -- -25.5 from the screen center, 2 from the bottom. BottomBar.lua redoes this with the real -- micro-menu width. ForeverUI.Layout.Register(holder, "actionbar", L.ACTIONBAR_EDIT_LABEL, "BOTTOMRIGHT", "BOTTOM", -25.5, 2) ForeverUI.Layout.Register(leftCap, "leftgryphon", L.ACTIONBAR_EDIT_LABEL_LEFT_GRYPHON, "BOTTOMRIGHT", "BOTTOM", -25.5 - holder:GetWidth() + END_CAP_INSET, 2 + END_CAP_DROP) ForeverUI.Layout.Register(rightCap, "rightgryphon", L.ACTIONBAR_EDIT_LABEL_RIGHT_GRYPHON, "BOTTOMLEFT", "BOTTOM", 0, 2 + END_CAP_DROP)