fix(popouts): the context menu never opened outside the main window

Right-clicking a name or an item inside a pop-out did nothing at all.

One payload handler is shared by the main window, every pop-out and the
input preview, and it holds a single popup state. The main window is
registered first, so it draws first, finds no open popup in its own scope,
reads that as "closed" and clears the state -- before the window that
actually opened the popup gets its turn.

The popup now belongs to the surface that opened it. The others leave its
state alone instead of dropping it. The rule itself sits in its own helper
because the handler pulls in Dalamud and cannot be loaded from a test.
This commit is contained in:
2026-08-20 07:54:32 +02:00
parent d5c7db9f43
commit a3379818eb
4 changed files with 28 additions and 1 deletions
+10 -1
View File
@@ -49,6 +49,7 @@ internal sealed class PayloadHandler
public uint LastHoverCounter;
private (Chunk, Payload?)? _popup;
private object? _popupOwner;
public PayloadHandler(
ThemeRegistry themes,
@@ -69,6 +70,10 @@ internal sealed class PayloadHandler
_logger = logger;
}
// Every chat surface shares this handler, so each one claims it before it
// renders. Without that the popup cannot tell whose click it belongs to.
internal object? ActiveSurface { get; set; }
internal void Draw()
{
DrawPopups();
@@ -84,7 +89,9 @@ internal sealed class PayloadHandler
private void DrawPopups()
{
if (_popup == null)
// A foreign surface finds no open popup here and would otherwise read
// that as "closed" and drop the state before the owner gets to draw.
if (_popup == null || !PopupOwnership.Owns(_popupOwner, ActiveSurface))
return;
var (chunk, payload) = _popup.Value;
@@ -93,6 +100,7 @@ internal sealed class PayloadHandler
if (!popup.Success)
{
_popup = null;
_popupOwner = null;
return;
}
@@ -934,6 +942,7 @@ internal sealed class PayloadHandler
private void RightClickPayload(Chunk chunk, Payload? payload)
{
_popup = (chunk, payload);
_popupOwner = ActiveSurface;
ImGui.OpenPopup(PopupId);
}
}
+5
View File
@@ -158,6 +158,11 @@ internal sealed class MessageList
return;
}
// Claim the shared handler, so a popup opened here stays with this
// window instead of being cleared by whichever surface draws first.
if (_handler is not null)
_handler.ActiveSurface = this;
// No own ImRaii.Child here — MainWindow already wraps the message
// area in one. Nesting would give the window two stacked scrolls
// and a runaway content-height computation.
+1
View File
@@ -175,6 +175,7 @@ internal sealed class InputPreview : Window
_handlerLender.ResetCounter();
var handler = _handlerLender.Borrow();
handler.ActiveSurface = this;
_chunkRenderer.DrawChunks(
_previewMessage!.Content,
wrap: true,
+12
View File
@@ -0,0 +1,12 @@
namespace HellionChat.Util;
// Chat surfaces share one payload handler and all render it in the same frame.
// A popup belongs to the surface that opened it; the others must leave its
// state alone instead of reading "no popup here" as "popup was closed".
internal static class PopupOwnership
{
// Reference identity on purpose -- two surfaces can compare equal without
// being the same window.
internal static bool Owns(object? owner, object? surface) =>
owner is not null && ReferenceEquals(owner, surface);
}