From 5841ba0afbd91523496ee24cc7c25b0ac741a01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20B=C3=BCchner?= Date: Sat, 8 Aug 2026 00:35:09 +0200 Subject: [PATCH] fix(blizzskin): anchor stand-in tooltips like the global GameTooltip The three tooltip anchor features (cursor anchor, fixed position, growth direction) all open their GameTooltip_SetDefaultAnchor post-hook with if tooltip ~= GameTooltip then return end That refuses tooltips which are not literally _G.GameTooltip, and some addons cannot use the global one. A caller driving secure frames from insecure code taints the global tooltip by touching it, so it builds its own GameTooltipTemplate frame instead and flags it LIKE_GLOBAL_GAMETOOLTIP, which is the established way of asking to be treated as the real thing. OPie does exactly this for its ring slices (Libs/NotGameTooltip.lua): tip.LIKE_GLOBAL_GAMETOOLTIP = true -- External addons: please treat this as you would treat _G.GameTooltip Those stand-ins are not going off on their own. They still anchor through GameTooltip_SetDefaultAnchor, so our hook does fire for them; it just declines on the first line. The tooltip therefore keeps whatever Blizzard's default anchoring gave it and sits at the bottom-right container while every other tooltip in the game obeys the user's anchor box. TooltipIsGlobalLike accepts the global tooltip or any frame carrying the flag, and is used for the anchor decision in those three places. The armed-state bookkeeping deliberately stays on the global tooltip only. _fixedArmed and _growthDefaultAnchored gate SetPoint enforcement that is hooked onto GameTooltip's own setters, so they can only ever describe one tooltip; arming them for a stand-in would let a later GameTooltip:SetPoint be enforced against state belonging to a different frame. A stand-in gets the one-shot anchor at default-anchor time, which is all it needs, since that enforcement exists for Blizzard's container re-anchoring and Blizzard does not manage third-party frames. Left on strict identity: _repointTooltipAtCursor (only ever called by our own frames with the global tooltip), the FadeOut and SetPoint/SetOwner hooks (bound to the GameTooltip object itself), and the Show Tooltips suppression block, which reparents into a hidden host and is a visibility feature rather than an anchor one. No cost when no stand-in exists: one extra table read on a path that already ran, and nothing new is hooked, registered or created. --- .../EllesmereUIBlizzardSkin.lua | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/EllesmereUIBlizzardSkin/EllesmereUIBlizzardSkin.lua b/EllesmereUIBlizzardSkin/EllesmereUIBlizzardSkin.lua index ac589dcb..2c7ced7b 100644 --- a/EllesmereUIBlizzardSkin/EllesmereUIBlizzardSkin.lua +++ b/EllesmereUIBlizzardSkin/EllesmereUIBlizzardSkin.lua @@ -2167,6 +2167,25 @@ local function TooltipOwnerUsable(parent) return true end +-- Addons that cannot risk touching the global GameTooltip -- taint-sensitive +-- callers driving secure frames, such as OPie's ring view -- build their own +-- GameTooltipTemplate frame and flag it LIKE_GLOBAL_GAMETOOLTIP, which is their +-- documented request to be treated exactly as _G.GameTooltip. Those stand-ins +-- still route through GameTooltip_SetDefaultAnchor like everyone else, so a +-- strict identity check was the only thing keeping our anchors off them: their +-- tooltips ignored the fixed/cursor position and landed on Blizzard's default +-- container instead. Honour the flag for the anchor decision. +-- +-- Strict identity is still correct everywhere else in this file. The armed-state +-- bookkeeping below hooks GameTooltip's OWN SetOwner/SetPoint methods, so it can +-- only ever track one tooltip; a stand-in gets the one-shot anchor at default +-- time and nothing re-anchors it afterwards, which is all it needs (Blizzard's +-- container logic, the reason that enforcement exists, does not manage it). +local function TooltipIsGlobalLike(tooltip) + if tooltip == GameTooltip then return true end + return type(tooltip) == "table" and tooltip.LIKE_GLOBAL_GAMETOOLTIP == true +end + do -- Selected position = where the tooltip sits relative to the cursor, so the -- tooltip corner that touches the cursor is the opposite one. @@ -2220,7 +2239,7 @@ do end local function ApplyCursorAnchor(tooltip, parent) - if tooltip ~= GameTooltip then return end + if not TooltipIsGlobalLike(tooltip) then return end -- Gated by the "Reskin Tooltip" master (matches the grayed-out option), so -- disabling the reskin restores the default tooltip position. if EllesmereUIDB and EllesmereUIDB.customTooltips == false then return end @@ -2453,7 +2472,7 @@ do end local function ApplyFixedAnchor(tooltip, parent) - if tooltip ~= GameTooltip then return end + if not TooltipIsGlobalLike(tooltip) then return end if not WantFixed() then return end if tooltip:IsForbidden() then return end -- A forbidden owner (a nameplate aura button) cannot be anchored to, so @@ -2479,8 +2498,10 @@ do end hooksecurefunc("GameTooltip_SetDefaultAnchor", function(tooltip, parent) - if tooltip ~= GameTooltip then return end - _fixedArmed = true + if not TooltipIsGlobalLike(tooltip) then return end + -- Only the global tooltip can be armed: the SetPoint enforcement below + -- is hooked onto GameTooltip's own setters. See TooltipIsGlobalLike. + if tooltip == GameTooltip then _fixedArmed = true end ApplyFixedAnchor(tooltip, parent) end) -- Every explicit tooltip build starts with SetOwner (it runs BEFORE the @@ -2638,8 +2659,10 @@ do end hooksecurefunc("GameTooltip_SetDefaultAnchor", function(tooltip) - if tooltip ~= GameTooltip then return end - _growthDefaultAnchored = true + if not TooltipIsGlobalLike(tooltip) then return end + -- Only the global tooltip can be armed: the SetPoint enforcement below + -- is hooked onto GameTooltip's own setters. See TooltipIsGlobalLike. + if tooltip == GameTooltip then _growthDefaultAnchored = true end Enforce(tooltip) end) -- Every tooltip build starts with SetOwner (it runs BEFORE the