feat(payload-handler): wire Hover/Click + LeftClick/RightClick/LinkClick paths

E5 fills the hover/click handler layer (10 methods):

- Hover/Click: replaces E1's empty stubs with real 1:1 bodies; Click uses
  unsafe for FFXIVClientStructs pointer access (UIGlobals.PlaySoundEffect
  via PopupSfx const from E2)
- DoHover: §4.2 swap LogWindow.DefaultText → _themes.Active.Colors.TextPrimary
- HoverStatus, HoverItem, HoverEventItem: 2x _chunkRenderer.DrawChunks
  swaps each (name + description rendering)
- HoverUri: pure 1:1, no LogWindow deps
- LeftClickPayload: unsafe-preserved, Plugin.GameGui static stays
- ClickLinkPayload: pure 1:1, Plugin.ChatGui / Plugin.Framework statics stay
- RightClickPayload: sets _popup field (per E1/E2 field-syntax convention)

Adds FFXIVClientStructs.FFXIV.Client.UI using for UIGlobals; adds
Action alias and DalamudPartyFinderPayload/ChatTwoPartyFinderPayload
aliases required by LeftClickPayload switch arms.

E6 will wire MoveTooltip. After E5, PayloadHandler is functionally complete
except for the MoveTooltip AddonLifecycle wiring.
This commit is contained in:
2026-05-27 13:51:18 +02:00
parent 6da8ac93fe
commit 121c96f79e
+250 -2
View File
@@ -11,6 +11,7 @@ using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; 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 HellionChat.Code; using HellionChat.Code;
using HellionChat.Resources; using HellionChat.Resources;
using HellionChat.Themes; using HellionChat.Themes;
@@ -19,6 +20,9 @@ using HellionChat.Ui.Windows;
using HellionChat.Util; using HellionChat.Util;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Action = System.Action;
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
namespace HellionChat; namespace HellionChat;
@@ -580,7 +584,251 @@ internal sealed class PayloadHandler
); );
} }
internal void Hover(Payload payload) { } internal void Hover(Payload payload)
{
var hoverSize = 350f * ImGuiHelpers.GlobalScale;
internal unsafe void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button) { } switch (payload)
{
case StatusPayload status:
DoHover(() => HoverStatus(status), hoverSize);
break;
case ItemPayload item:
if (Plugin.Config.NativeItemTooltips)
{
if (!HandleTooltips || HoveredItem != item.RawItemId)
{
HandleTooltips = true;
HoveredItem = item.RawItemId;
HoverCounter = LastHoverCounter = 0;
GameFunctions.GameFunctions.OpenItemTooltip(item.RawItemId, item.Kind);
}
else
{
LastHoverCounter = HoverCounter;
}
return;
}
DoHover(() => HoverItem(item), hoverSize);
break;
case UriPayload uri:
DoHover(() => HoverUri(uri), hoverSize);
break;
}
}
private void DoHover(Action drawAction, float spacingHorizontal)
{
ImGui.SetNextWindowSize(new Vector2(spacingHorizontal, -1f));
using (ImRaii.Tooltip())
using (ImRaii.TextWrapPos(0.0f))
using (
ImRaii.PushColor(
ImGuiCol.Text,
ColourUtil.RgbaToVector4(_themes.Active.Colors.TextPrimary)
)
)
drawAction();
}
private void HoverStatus(StatusPayload status)
{
if (
Plugin.TextureProvider.GetFromGameIcon(status.Status.Value.Icon).GetWrapOrDefault() is
{ } icon
)
InlineIcon(icon);
var builder = new SeStringBuilder();
var nameValue = status.Status.Value.Name.ToString();
switch (status.Status.Value.StatusCategory)
{
case 1:
builder.AddUiForeground($"{SeIconChar.Buff.ToIconString()}{nameValue}", 517);
break;
case 2:
builder.AddUiForeground($"{SeIconChar.Debuff.ToIconString()}{nameValue}", 518);
break;
default:
builder.AddUiForeground(nameValue, 1);
break;
}
var name = ChunkUtil.ToChunks(builder.BuiltString, ChunkSource.None, null);
_chunkRenderer.DrawChunks(name.ToList());
ImGui.Separator();
var desc = ChunkUtil.ToChunks(
status.Status.Value.Description.ToDalamudString(),
ChunkSource.None,
null
);
_chunkRenderer.DrawChunks(desc.ToList());
}
private void HoverItem(ItemPayload item)
{
if (item.Kind == ItemKind.EventItem)
{
HoverEventItem(item);
return;
}
if (!Sheets.ItemSheet.TryGetRow(item.ItemId, out var resolvedItem))
return;
if (
Plugin
.TextureProvider.GetFromGameIcon(
new GameIconLookup(resolvedItem.Icon, item.Kind == ItemKind.Hq)
)
.GetWrapOrDefault() is
{ } icon
)
InlineIcon(icon);
var name = ChunkUtil.ToChunks(resolvedItem.Name.ToDalamudString(), ChunkSource.None, null);
_chunkRenderer.DrawChunks(name.ToList());
ImGui.Separator();
var desc = ChunkUtil.ToChunks(
resolvedItem.Description.ToDalamudString(),
ChunkSource.None,
null
);
_chunkRenderer.DrawChunks(desc.ToList());
}
private void HoverEventItem(ItemPayload item)
{
if (!Sheets.EventItemSheet.TryGetRow(item.RawItemId, out var itemRow))
return;
if (
Plugin
.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon))
.GetWrapOrDefault() is
{ } icon
)
InlineIcon(icon);
var name = ChunkUtil.ToChunks(itemRow.Name.ToDalamudString(), ChunkSource.None, null);
_chunkRenderer.DrawChunks(name.ToList());
ImGui.Separator();
if (!Sheets.EventItemHelpSheet.TryGetRow(item.RawItemId, out var itemHelpRow))
return;
_chunkRenderer.DrawChunks(
ChunkUtil
.ToChunks(itemHelpRow.Description.ToDalamudString(), ChunkSource.None, null)
.ToList()
);
}
private void HoverUri(UriPayload uri)
{
ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority));
ImGuiUtil.WarningText(Language.Context_URLWarning);
}
internal unsafe void Click(Chunk chunk, Payload? payload, ImGuiMouseButton button)
{
if (Plugin.Config.PlaySounds)
UIGlobals.PlaySoundEffect(PopupSfx);
switch (button)
{
case ImGuiMouseButton.Left:
LeftClickPayload(chunk, payload);
break;
case ImGuiMouseButton.Right:
RightClickPayload(chunk, payload);
break;
}
}
private unsafe void LeftClickPayload(Chunk chunk, Payload? payload)
{
switch (payload)
{
case MapLinkPayload map:
Plugin.GameGui.OpenMapWithMapLink(map);
break;
case QuestPayload quest:
GameFunctions.GameFunctions.OpenQuestLog(quest.Quest);
break;
case DalamudLinkPayload link:
ClickLinkPayload(chunk, payload, link);
break;
case DalamudPartyFinderPayload pf:
if (
pf.LinkType
== DalamudPartyFinderPayload.PartyFinderLinkType.PartyFinderNotification
)
GameFunctions.GameFunctions.OpenPartyFinder();
else
GameFunctions.GameFunctions.OpenPartyFinder(pf.ListingId);
break;
case ChatTwoPartyFinderPayload pf:
GameFunctions.GameFunctions.OpenPartyFinder(pf.Id);
break;
case AchievementPayload achievement:
GameFunctions.GameFunctions.OpenAchievement(achievement.Id);
break;
case RawPayload raw:
if (Equals(raw, ChunkUtil.PeriodicRecruitmentLink))
GameFunctions.GameFunctions.OpenPartyFinder();
break;
case UriPayload uri:
WrapperUtil.TryOpenUri(uri.Uri);
break;
default:
RightClickPayload(chunk, payload);
break;
}
}
private void ClickLinkPayload(Chunk chunk, Payload payload, DalamudLinkPayload link)
{
if (chunk.GetSeString() is not { } source)
return;
var start = source.Payloads.IndexOf(payload);
var end = source.Payloads.IndexOf(RawPayload.LinkTerminator, start == -1 ? 0 : start);
if (start == -1 || end == -1)
return;
var payloads = source.Payloads.Skip(start).Take(end - start + 1).ToList();
if (
!Plugin.ChatGui.RegisteredLinkHandlers.TryGetValue(
(link.Plugin, link.CommandId),
out var value
)
)
{
_logger.LogWarning("Could not find DalamudLinkHandlers");
return;
}
try
{
// Running XivCommon SendChat instantly, without RunOnTick, leads to a game freeze, for whatever reason
Plugin.Framework.RunOnTick(() => value.Invoke(link.CommandId, new SeString(payloads)));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing DalamudLinkPayload handler");
}
}
private void RightClickPayload(Chunk chunk, Payload? payload)
{
_popup = (chunk, payload);
ImGui.OpenPopup(PopupId);
}
} }