local FILL_ATLAS = "UI-HUD-ExperienceBar-Fill-Experience" local FRAME_ATLAS = "UI-HUD-ExperienceBar-Frame" local BACKGROUND_ATLAS = "UI-HUD-ExperienceBar-Background" local ACTION_BARS = { "MainActionBar", "MainMenuBar", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBar5", "MultiBar6", "MultiBar7", "StanceBar", "PetActionBar", "PossessActionBar", } local bar local watch local lastSignature local elapsed = 0 local hooked = {} local pushed = {} local EnsureWatch local function CopyTexture(dest, src) if not dest or not src then return false end if src.GetAtlas then local atlas = src:GetAtlas() if atlas and atlas ~= "" and dest.SetAtlas then dest:SetAtlas(atlas, false) return true end end if src.GetTexture then local file = src:GetTexture() if file then dest:SetTexture(file) if src.GetTexCoord and dest.SetTexCoord then dest:SetTexCoord(src:GetTexCoord()) end return true end end return false end local function TrySetAtlas(texture, atlas) if not texture or not atlas or atlas == "" or not texture.SetAtlas then return false end return pcall(texture.SetAtlas, texture, atlas, false) end local function TrySetAtlases(texture, ...) for i = 1, select("#", ...) do if TrySetAtlas(texture, select(i, ...)) then return true end end return false end local FRAME_PIXEL_WIDTH = 571 local GUTTER_WIDTH_FRACTION = 2 / FRAME_PIXEL_WIDTH local GUTTER_CENTERS = { 56.5 / FRAME_PIXEL_WIDTH, 113.5 / FRAME_PIXEL_WIDTH, 170.5 / FRAME_PIXEL_WIDTH, 227.5 / FRAME_PIXEL_WIDTH, 284.5 / FRAME_PIXEL_WIDTH, 341.5 / FRAME_PIXEL_WIDTH, 398.5 / FRAME_PIXEL_WIDTH, 455.5 / FRAME_PIXEL_WIDTH, 512.5 / FRAME_PIXEL_WIDTH, } local function DisableAtlasSlice(texture) if texture.SetHorizTile then texture:SetHorizTile(false) end if texture.SetVertTile then texture:SetVertTile(false) end if texture.SetTextureSliceMargins then pcall(texture.SetTextureSliceMargins, texture, 0, 0, 0, 0) end if texture.SetTextureSliceMode and Enum and Enum.TextureSliceMode and Enum.TextureSliceMode.Stretched then pcall(texture.SetTextureSliceMode, texture, Enum.TextureSliceMode.Stretched) end if texture.SetSnapToPixelGrid then pcall(texture.SetSnapToPixelGrid, texture, true) end if texture.SetTexelSnappingBias then pcall(texture.SetTexelSnappingBias, texture, 0) end end local function SetRawAtlas(texture, atlas) if not texture or not atlas or atlas == "" then return false end local info = C_Texture and C_Texture.GetAtlasInfo and C_Texture.GetAtlasInfo(atlas) local file = info and (info.file or info.filename) if not info or not file or not info.leftTexCoord or not info.width or info.width == 0 then local applied = TrySetAtlas(texture, atlas) if applied then DisableAtlasSlice(texture) end return applied end texture:SetTexture(file) texture:SetTexCoord(info.leftTexCoord, info.rightTexCoord, info.topTexCoord, info.bottomTexCoord) DisableAtlasSlice(texture) return true end local function ApplyGutterSlice(texture, center) local info = C_Texture and C_Texture.GetAtlasInfo and C_Texture.GetAtlasInfo(FRAME_ATLAS) local file = info and (info.file or info.filename) if not info or not file then texture:SetColorTexture(0.55, 0.54, 0.5, 0.95) return end texture:SetTexture(file) local left = info.leftTexCoord or 0 local span = (info.rightTexCoord or 1) - left local half = GUTTER_WIDTH_FRACTION / 2 texture:SetTexCoord( left + (center - half) * span, left + (center + half) * span, info.topTexCoord or 0, info.bottomTexCoord or 1) DisableAtlasSlice(texture) end local function LayoutGutters(frame) local gutters = frame.Gutters local border = frame.BarFrameTexture if not gutters or not border then return end local width = border:GetWidth() if not width or width < 1 then width = frame.StatusBar and frame.StatusBar:GetWidth() end if not width or width < 1 then return end local sliceWidth = math.max(1, width * GUTTER_WIDTH_FRACTION) for i, texture in ipairs(gutters) do local x = width * GUTTER_CENTERS[i] texture:ClearAllPoints() texture:SetWidth(sliceWidth) texture:SetPoint("TOP", border, "TOPLEFT", x, 0) texture:SetPoint("BOTTOM", border, "BOTTOMLEFT", x, 0) texture:Show() end end local function SyncBarLevels(frame) local base = frame:GetFrameLevel() local status = frame.StatusBar if status then status:SetFrameStrata("LOW") status:SetFrameLevel(base + 1) end if frame.Overlay then frame.Overlay:SetFrameStrata(frame:GetFrameStrata() or "MEDIUM") frame.Overlay:SetFrameLevel(base + 20) end end local function FormatBarText(prof) if not prof.maxRank or prof.maxRank <= 0 then return prof.name end return string.format("%s %d/%d", prof.name, prof.rank or 0, prof.maxRank) end local function IsExperienceFill(status) local tex = status and status.GetStatusBarTexture and status:GetStatusBarTexture() local atlas = tex and tex.GetAtlas and tex:GetAtlas() return atlas and atlas:find("Experience", 1, true) end local function FindLiveBar(container) if not container or not container.bars then return end local fallback for _, candidate in pairs(container.bars) do if candidate and candidate.StatusBar then local visible = (not candidate.ShouldBeVisible or candidate:ShouldBeVisible()) and candidate:IsShown() if visible then if IsExperienceFill(candidate.StatusBar) then return candidate end fallback = fallback or candidate end end end return fallback or container.bars[1] end local function ContainerActive(container) if not container or not container:IsShown() then return false end if (container:GetAlpha() or 1) < 0.05 then return false end if container.bars then for _, candidate in pairs(container.bars) do if candidate and candidate.StatusBar and candidate:IsShown() then if not candidate.ShouldBeVisible or candidate:ShouldBeVisible() then return true end end end return false end return true end local function TopContainer() local top, topY local containers = { MainStatusTrackingBarContainer, SecondaryStatusTrackingBarContainer, } for _, container in ipairs(containers) do if ContainerActive(container) then local y = container:GetTop() if y and (not topY or y > topY) then top, topY = container, y end end end return top or MainStatusTrackingBarContainer end local function TextureAtlas(texture) if texture and texture.GetAtlas then local atlas = texture:GetAtlas() if atlas and atlas ~= "" then return atlas end end end local function FindBackgroundTexture(status, container) if status and status.Background then return status.Background end if container and container.Background then return container.Background end if not status then return end local regions = { status:GetRegions() } for _, region in ipairs(regions) do if region.GetDrawLayer and region.GetAtlas then local layer = region:GetDrawLayer() if layer == "BACKGROUND" and region ~= status:GetStatusBarTexture() then return region end end end end local function VisibleBar(container) if not container or not container.bars then return end for _, candidate in pairs(container.bars) do if candidate and candidate.StatusBar and candidate:IsShown() then if not candidate.ShouldBeVisible or candidate:ShouldBeVisible() then return candidate end end end end local function GetLiveArt() local main = MainStatusTrackingBarContainer local second = SecondaryStatusTrackingBarContainer local fillBar = FindLiveBar(main) if not fillBar or not IsExperienceFill(fillBar.StatusBar) then fillBar = FindLiveBar(second) or fillBar end local gutterContainer = TopContainer() or main local gutterBar = VisibleBar(gutterContainer) or fillBar local fillStatus = fillBar and fillBar.StatusBar local gutterStatus = gutterBar and gutterBar.StatusBar return { gutterContainer = gutterContainer, gutterStatus = gutterStatus, fillStatus = fillStatus, frameTexture = gutterContainer and gutterContainer.BarFrameTexture, frameAtlas = TextureAtlas(gutterContainer and gutterContainer.BarFrameTexture) or FRAME_ATLAS, fillAtlas = TextureAtlas(fillStatus and fillStatus.GetStatusBarTexture and fillStatus:GetStatusBarTexture()) or FILL_ATLAS, backgroundAtlas = TextureAtlas(FindBackgroundTexture(gutterStatus or fillStatus, gutterContainer)) or BACKGROUND_ATLAS, } end local function GetInsets(parent, child) if not parent or not child then return 0, 0, 0, 0 end local parentLeft, parentRight = parent:GetLeft(), parent:GetRight() local parentTop, parentBottom = parent:GetTop(), parent:GetBottom() local childLeft, childRight = child:GetLeft(), child:GetRight() local childTop, childBottom = child:GetTop(), child:GetBottom() if not parentLeft or not childLeft then return 0, 0, 0, 0 end return childLeft - parentLeft, parentRight - childRight, parentTop - childTop, childBottom - parentBottom end local function PlaceLike(dest, src, srcParent, destParent) dest:ClearAllPoints() if not src or not srcParent then dest:SetAllPoints(destParent) return end local left, right, top, bottom = GetInsets(srcParent, src) dest:SetPoint("TOPLEFT", destParent, "TOPLEFT", left, -top) dest:SetPoint("BOTTOMRIGHT", destParent, "BOTTOMRIGHT", -right, bottom) end local function ApplyFill(status, atlas) local applied = false local current = status:GetStatusBarTexture() if current and current.SetAtlas then applied = TrySetAtlases(current, atlas, FILL_ATLAS) end if not applied then status:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8") current = status:GetStatusBarTexture() applied = current and TrySetAtlases(current, atlas, FILL_ATLAS) end current = status:GetStatusBarTexture() if current and current.SetDesaturated then current:SetDesaturated(true) end local r, g, b = Professor:GetBarColor() status:SetStatusBarColor(r, g, b, 1) current = status:GetStatusBarTexture() if current and current.SetDesaturated then current:SetDesaturated(true) end if current and current.SetDrawLayer then current:SetDrawLayer("ARTWORK", 0) end end local function ApplyXPLook(frame) local live = GetLiveArt() local gutter = live.gutterContainer local height = 17 if gutter then height = gutter:GetHeight() or height frame:SetFrameStrata(gutter:GetFrameStrata() or "MEDIUM") frame:SetFrameLevel((gutter:GetFrameLevel() or 0) + 4) end frame:SetHeight(height) local status = frame.StatusBar PlaceLike(status, live.gutterStatus, gutter, frame) if not TrySetAtlases(frame.Background, live.backgroundAtlas, BACKGROUND_ATLAS) then frame.Background:SetColorTexture(0, 0, 0, 0.55) end frame.Background:SetAllPoints(status) ApplyFill(status, live.fillAtlas) local border = frame.BarFrameTexture border:SetAlpha(1) border:SetDrawLayer("OVERLAY", 4) local frameAtlas = live.frameAtlas or FRAME_ATLAS if not SetRawAtlas(border, frameAtlas) and live.frameTexture then CopyTexture(border, live.frameTexture) end DisableAtlasSlice(border) if live.frameTexture and live.frameTexture.GetBlendMode and border.SetBlendMode then border:SetBlendMode(live.frameTexture:GetBlendMode()) end if live.frameTexture then PlaceLike(border, live.frameTexture, gutter, frame) else border:ClearAllPoints() border:SetAllPoints(frame) end SyncBarLevels(frame) LayoutGutters(frame) end local function CreateBar() local frame = CreateFrame("Frame", "ProfessorBar", UIParent) frame.StatusBar = CreateFrame("StatusBar", nil, frame) frame.Background = frame.StatusBar:CreateTexture(nil, "BACKGROUND") frame.Overlay = CreateFrame("Frame", nil, frame) frame.Overlay:SetAllPoints() frame.Overlay:EnableMouse(false) frame.BarFrameTexture = frame.Overlay:CreateTexture(nil, "OVERLAY", nil, 4) frame.Gutters = {} for i, center in ipairs(GUTTER_CENTERS) do local texture = frame.Overlay:CreateTexture(nil, "OVERLAY", nil, 6) ApplyGutterSlice(texture, center) frame.Gutters[i] = texture end frame.Text = frame.Overlay:CreateFontString(nil, "OVERLAY", "TextStatusBarText") frame.Text:SetDrawLayer("OVERLAY", 7) frame.Text:SetPoint("CENTER", frame.StatusBar, "CENTER", 0, 0) frame:EnableMouse(true) frame:HookScript("OnShow", function(self) SyncBarLevels(self) LayoutGutters(self) end) frame:HookScript("OnSizeChanged", function(self) LayoutGutters(self) end) frame:SetScript("OnEnter", function(self) self.Text:Show() end) frame:SetScript("OnLeave", function(self) if not Professor:ShouldShowText() then self.Text:Hide() end end) ApplyXPLook(frame) frame:Hide() return frame end local function Acquire() if not bar then bar = CreateBar() end return bar end local function Signature() local function bits(frame) if not frame then return "x" end return string.format("%s:%.1f:%.1f:%.1f:%s", frame:GetName() or "?", frame:GetLeft() or -1, frame:GetBottom() or -1, frame:GetWidth() or -1, frame:IsShown() and "1" or "0") end return bits(MainStatusTrackingBarContainer) .. "|" .. bits(SecondaryStatusTrackingBarContainer) end local function EditModeLive() local manager = EditModeManagerFrame if not manager then return false end if manager.IsEditModeActive then return manager:IsEditModeActive() end return manager.editModeActive end local function ClearPushes(stackTop) for frame in pairs(pushed) do if stackTop and frame.GetBottom and frame.AdjustPointsOffset then local bottom = frame:GetBottom() if bottom then local delta = stackTop - bottom if math.abs(delta) >= 0.5 then frame:AdjustPointsOffset(0, delta) end end end pushed[frame] = nil end end local function IsDependent(frame, other) if not frame or not other or frame == other then return false end local parent = frame:GetParent() while parent and parent ~= UIParent do if parent == other then return true end parent = parent:GetParent() end local points = frame:GetNumPoints() or 0 for i = 1, points do local _, rel = frame:GetPoint(i) if rel == other then return true end local relParent = rel while relParent and relParent ~= UIParent do if relParent == other then return true end relParent = relParent:GetParent() end end return false end local function HorizontallyOverlaps(frame, left, right) local frameLeft, frameRight = frame:GetLeft(), frame:GetRight() if not frameLeft or not frameRight then return false end return frameRight > left + 20 and frameLeft < right - 20 end local function NudgeActionBars(ourBar) if not ourBar or not ourBar:IsShown() or EditModeLive() then return end local stackTop = ourBar:GetBottom() local desiredBottom = ourBar:GetTop() if not stackTop or not desiredBottom then return end local left = ourBar:GetLeft() or 0 local right = ourBar:GetRight() or 0 local group, lowest = {}, nil for _, name in ipairs(ACTION_BARS) do local frame = _G[name] if frame and frame ~= ourBar and frame:IsVisible() and frame.AdjustPointsOffset then local bottom = frame:GetBottom() if bottom and HorizontallyOverlaps(frame, left, right) then local onStack = bottom >= stackTop - 8 and bottom <= stackTop + 80 local already = math.abs(bottom - desiredBottom) < 8 if onStack or already then if not lowest or bottom < lowest - 2 then lowest = bottom group = { frame } elseif math.abs(bottom - lowest) <= 2 then group[#group + 1] = frame end end end end end for _, frame in ipairs(group) do local dependent = false for _, other in ipairs(group) do if IsDependent(frame, other) then dependent = true break end end if not dependent then local bottom = frame:GetBottom() local delta = desiredBottom - bottom if math.abs(delta) >= 0.5 then frame:AdjustPointsOffset(0, delta) end pushed[frame] = true end end end function Professor:LayoutBars() local professions = self:GetVisibleProfessions() local anchor = TopContainer() local frame = Acquire() if #professions == 0 then local stackTop = frame:IsShown() and frame:GetBottom() frame:Hide() ClearPushes(stackTop) lastSignature = Signature() return end ApplyXPLook(frame) frame:ClearAllPoints() if anchor then frame:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, 0) frame:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", 0, 0) else frame:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 80) frame:SetWidth(804) end frame:Show() LayoutGutters(frame) NudgeActionBars(frame) lastSignature = Signature() end function Professor:UpdateBars() EnsureWatch() local professions = self:GetVisibleProfessions() local frame = Acquire() if #professions == 0 then self:LayoutBars() return end local prof = professions[1] local status = frame.StatusBar status:SetMinMaxValues(0, prof.maximum) status:SetValue(prof.progress) frame.Text:SetText(FormatBarText(prof)) if self:ShouldShowText() then frame.Text:Show() else frame.Text:Hide() end self:LayoutBars() end local function HookContainer(container) if not container or hooked[container] then return end hooked[container] = true container:HookScript("OnShow", function() Professor:LayoutBars() end) container:HookScript("OnHide", function() Professor:LayoutBars() end) container:HookScript("OnSizeChanged", function() Professor:LayoutBars() end) end EnsureWatch = function() HookContainer(MainStatusTrackingBarContainer) HookContainer(SecondaryStatusTrackingBarContainer) if watch then return end if EventRegistry and EventRegistry.RegisterCallback then pcall(EventRegistry.RegisterCallback, EventRegistry, "EditMode.Exit", function() Professor:LayoutBars() end, Professor) end if EditModeManagerFrame and EditModeManagerFrame.ExitEditMode then hooksecurefunc(EditModeManagerFrame, "ExitEditMode", function() Professor:LayoutBars() end) end if StatusTrackingBarManager and StatusTrackingBarManager.UpdateBarsShown then hooksecurefunc(StatusTrackingBarManager, "UpdateBarsShown", function() if bar and bar:IsShown() then NudgeActionBars(bar) end end) end watch = CreateFrame("Frame") watch:SetScript("OnUpdate", function(_, dt) elapsed = elapsed + dt if elapsed < 0.25 then return end elapsed = 0 HookContainer(MainStatusTrackingBarContainer) HookContainer(SecondaryStatusTrackingBarContainer) if Signature() ~= lastSignature then Professor:LayoutBars() elseif bar and bar:IsShown() then NudgeActionBars(bar) end end) end