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.
184 lines
6.7 KiB
C#
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);
|
|
}
|
|
}
|