feat(payload-handler): wire MoveTooltip (E6 completes PayloadHandler)

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.
This commit is contained in:
2026-05-27 14:31:23 +02:00
parent 121c96f79e
commit d6012a9459
+102
View File
@@ -1,7 +1,10 @@
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Config;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
@@ -12,6 +15,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using HellionChat.Code; using HellionChat.Code;
using HellionChat.Resources; using HellionChat.Resources;
using HellionChat.Themes; 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 ?? "<null>"
);
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 const float MaxInlineIconSize = 32f;
private static void InlineIcon(IDalamudTextureWrap icon) private static void InlineIcon(IDalamudTextureWrap icon)