Files
HellionChat/HellionChat/Ui/Windows/ChannelPopoutWindow.cs
T
JonKazama-Hellion b9feb8650f chore(comments): drop the spec task codes the last pass missed
Codes like POP-1c or B4b-2 name a task in a planning document, not
anything in the code. A reader has no way to resolve them and they age
into noise the moment the document is closed. Where a code was used as a
reference, the sentence now names the function it meant.
2026-08-20 07:54:45 +02:00

184 lines
6.7 KiB
C#

using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using HellionChat.Themes;
using HellionChat.Ui.Components;
using HellionChat.Ui.StyleEngine.Widgets;
using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.Windows;
// One pre-allocated pop-out window bound to a single Tab. Pure DI-sink: the
// PayloadHandler arrives via AttachPayloadHandler (post-build setter), NEVER
// via ctor. The ###id carries the slot index so all N
// instances are unique for WindowSystem.AddWindow and ImGui state is stable
// per slot (not per bound tab).
internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
{
private readonly int _slotIndex;
private readonly MessageList _messages;
private readonly InputBar _input;
private readonly ILogger<ChannelPopoutWindow> _logger;
private readonly FontManager _fonts;
private readonly Ui.StyleEngine.SurfaceBackdrop _backdrop;
private readonly ThemeRegistry _themes;
private readonly Ui.StyleEngine.TokenResolver _resolver;
public ChannelPopoutWindow(
int slotIndex,
MessageList messages,
InputBar input,
ILogger<ChannelPopoutWindow> logger,
FontManager fonts,
Ui.StyleEngine.SurfaceBackdrop backdrop,
ThemeRegistry themes,
Ui.StyleEngine.TokenResolver resolver
)
: base($"{Plugin.PluginName}###hellion_popout_{slotIndex}")
{
_slotIndex = slotIndex;
_messages = messages;
_input = input;
_logger = logger;
_fonts = fonts;
_backdrop = backdrop;
_themes = themes;
_resolver = resolver;
// The pop-in button lives in the input row. Wired here rather than
// through the constructor because this window is what it has to call.
_input.OnPopIn = () =>
{
if (Bound is { } tab)
CloseRequested?.Invoke(tab.Identifier);
};
IsOpen = false;
RespectCloseHotkey = false;
ShowCloseButton = false;
}
public int SlotIndex => _slotIndex;
public Tab? Bound { get; private set; }
// Wired post-build by ChannelPopoutPool so closing routes through the pool
// (which owns the slot map). The window can't reach the pool by ctor without
// a DI cycle, so the pool sets this after construction.
public Action<Guid>? CloseRequested { get; set; }
// Post-build setter. Wired by ChannelPopoutInitHostedService.
public void AttachPayloadHandler(PayloadHandler handler) =>
_messages.AttachPayloadHandler(handler);
public void Bind(Tab tab)
{
Bound = tab;
var isTell = tab is { IsTempTab: true, TellTarget: { } target } && target.IsSet();
// Default sizes: Tell is the more compact conversation window.
Size = isTell ? new Vector2(380f, 320f) : new Vector2(420f, 320f);
SizeCondition = ImGuiCond.FirstUseEver;
// Visible label tracks the bound tab; the ###id stays slot-stable so
// ImGui keeps this slot's position/size across binds.
// The title bar is a surface too, and the header deliberately stays
// silent in this mode because the title already carries the name.
var label = Util.TabDisplayName.Resolve(
tab.Name,
tab.NameCameFromPartner,
Plugin.Config.ScreenshotMode
);
WindowName = $"{label}###hellion_popout_{_slotIndex}";
IsOpen = true;
}
public void Unbind()
{
Bound = null;
IsOpen = false;
}
// IFocusableChatWindow — this pop-out's own InputBar carries the focus state
// the keybind tail checks when deciding whether to route at this surface.
public bool HasFocusedInput => _input.IsFocused;
// Arm-and-hold the one-frame Activate flag; the pop-out's Draw applies the
// ImGui focus next frame. Framework-thread safe (field write only).
public void RequestInputFocus()
{
BringToFront();
_input.Activate = true;
}
public override void PreDraw()
{
// Gate the native title bar on the user toggle (1.5.6 parity). DrawHeader
// carries the close button in-body regardless, so hiding the title bar
// never strands the pop-out. Reset from a fresh base each frame so
// toggling the bar back on clears NoTitleBar.
Flags = Plugin.Config.ShowPopOutTitleBar
? ImGuiWindowFlags.None
: ImGuiWindowFlags.NoTitleBar;
}
public override void Draw()
{
if (Bound is null)
return;
// v1.13.0: the plain title row became the channel header. The warning it
// left behind still holds and is now the rule the header follows -- with
// the title bar on, the window title already carries the tab name, so the
// header drops the name and shows only the world and the clock.
//
// Drawn before the body child on purpose, and the child's height is left
// alone: it is a negative value, which ImGui resolves against the space
// still available from the current cursor, and the header has already
// taken its share.
StyleEngine.Widgets.ChannelHeader.Draw(
Bound,
Plugin.Config.ShowPopOutTitleBar
? StyleEngine.Widgets.ChannelHeaderMode.DetailOnly
: StyleEngine.Widgets.ChannelHeaderMode.Full,
Plugin.Instance.FontManager,
StyleEngine.Widgets.ChannelHeader.CurrentDetail(),
InputBar.Height
);
// Anything drawn above can unbind us mid-frame (CloseRequested ->
// pool.TryClose -> Unbind nulls Bound). Re-check before the body so we
// never hand a null tab to MessageList/InputBar in this same Draw call.
if (Bound is null)
return;
// The bound tab is live-visible in this pop-out, so it carries no
// unread badge — mirror MainWindow's per-frame zero for the active tab.
// View-state reset only (tab.Messages store is untouched).
Bound.Unread = 0;
var inputHeight = InputBar.Height;
using (
var body = ImRaii.Child(
$"##hellion-popout-body-{_slotIndex}",
new Vector2(-1f, -inputHeight)
)
)
{
if (body.Success)
{
// Same floor as the main window's log, and the same reasoning:
// no accent wash and barely any motes, because a chat log is read
// line by line.
_backdrop.Draw(accentWashHeight: 0f, moteIntensity: 0.10f, strength: 0.45f);
_messages.Draw(Bound);
}
}
_input.Draw(Bound);
}
}