From d6012a945978d640ed9fd84915b4b66d9eb46b96 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Wed, 27 May 2026 14:31:23 +0200 Subject: [PATCH] feat(payload-handler): wire MoveTooltip (E6 completes PayloadHandler) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E6 closes out the PayloadHandler resurrection. MoveTooltip handles the cross-viewport AddonLifecycle tooltip-repositioning logic — reads MainWindow.LastViewport/LastWindowPos/LastWindowSize (from A1) to filter events and reposition the native item tooltip away from the chat window. Whole method marked `public unsafe void` per spec §4.2 (matches v1.5.6 exactly — avoids per-read unsafe-block scoping). LogWarning added on the unexpected-AddonArgs early-out branch (wires _logger into real use, prevents CS0414 unused-field warning). PayloadHandler is now feature-complete. F registers it in DI; G wires the AddonLifecycle.RegisterListener for MoveTooltip from a HostedService; H adds MessageList.AttachPayloadHandler setter-injection. --- HellionChat/PayloadHandler.cs | 102 ++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/HellionChat/PayloadHandler.cs b/HellionChat/PayloadHandler.cs index 2aa27c5..1b127b4 100644 --- a/HellionChat/PayloadHandler.cs +++ b/HellionChat/PayloadHandler.cs @@ -1,7 +1,10 @@ using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.Config; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; @@ -12,6 +15,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; using HellionChat.Code; using HellionChat.Resources; using HellionChat.Themes; @@ -564,6 +568,104 @@ internal sealed class PayloadHandler } } + public unsafe void MoveTooltip(AddonEvent type, AddonArgs args) + { + if (args == null) + { + _logger.LogWarning( + "MoveTooltip received unexpected AddonArgs type: {ArgsType}", + args?.GetType().Name ?? "" + ); + return; + } + + // Only move if the user has the "Next to Cursor" option selected + if ( + !Plugin.GameConfig.TryGet(UiControlOption.DetailTrackingType, out uint selected) + || selected != 0 + ) + return; + + if (_mainWindow.LastViewport != ImGuiHelpers.MainViewport.Handle) + return; + + var atk = args.Addon; + if (atk.IsNull) + return; + + var atkBase = (AtkUnitBase*)atk.Address; + if (atkBase->WindowNode == null) + return; + + if (!atkBase->IsVisible) + return; + + var component = atkBase->WindowNode->AtkResNode; + var atkPos = new Vector2(component.ScreenX, component.ScreenY); + var atkSize = new Vector2( + component.GetWidth() * component.ScaleX, + component.GetHeight() * component.GetScaleY() + ); + + var chatRect = new MathUtil.Rectangle( + _mainWindow.LastWindowPos, + _mainWindow.LastWindowSize + ); + var addonRect = new MathUtil.Rectangle(atkPos, atkSize); + + if (!chatRect.HasOverlap(addonRect)) + return; + + var viewportSize = ImGuiHelpers.MainViewport.Size; + var isLeft = chatRect.SizeX < viewportSize.X / 2; + var isTop = chatRect.SizeY < viewportSize.Y / 2; + + var mousePos = ImGui.GetMousePos(); + + // addon spawned left of mouse cursor + if (addonRect.X < mousePos.X) + { + if (isLeft) + addonRect.X = (short)mousePos.X + 5; + } + else + { + if (!isLeft) + addonRect.X = Math.Max(0, (short)mousePos.X - 5 - addonRect.Width); + } + + if (!chatRect.HasOverlap(addonRect)) + { + atkBase->SetPosition((short)addonRect.X, (short)addonRect.Y); + return; + } + + // addon spawned above mouse cursor + if (addonRect.Y < mousePos.Y) + { + if (isTop) + addonRect.Y = (short)mousePos.Y + 5; + } + else + { + if (!isTop) + addonRect.Y = Math.Max(0, (short)mousePos.Y - 5 - addonRect.Height); // prevent it going below 0 + } + + if (!chatRect.HasOverlap(addonRect)) + { + atkBase->SetPosition((short)addonRect.X, (short)addonRect.Y); + return; + } + + // Spawning right/bottom of mouse cursor didn't solve the overlap, so we spawn it next to the chat + var x = isLeft ? chatRect.SizeX : _mainWindow.LastWindowPos.X - atkSize.X; + var y = Math.Clamp(chatRect.SizeY - atkSize.Y, 0, float.MaxValue); + y -= isTop ? 0 : Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom + + atkBase->SetPosition((short)x, (short)y); + } + private const float MaxInlineIconSize = 32f; private static void InlineIcon(IDalamudTextureWrap icon)