feat(config): add MainWindowLayoutMode + v22 migration; scaffold popout pool

This commit is contained in:
2026-05-29 11:56:18 +02:00
parent b221a6e418
commit e786257cb3
6 changed files with 253 additions and 3 deletions
@@ -0,0 +1,45 @@
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.Windows;
// Central orchestration: pre-allocates Config.MaxParallelPopouts pop-out
// windows via the injected factory, all registered once in the WindowSystem
// (PluginLifecycle.RegisterWindows, framework thread). Open/Close is IsOpen +
// Bind/Unbind only — NEVER runtime AddWindow/RemoveWindow (v1.4.9 Stage-2
// freeze lesson). Pure DI-sink: no PayloadHandler in the ctor (plan §B.2).
internal sealed class ChannelPopoutPool
{
private readonly List<ChannelPopoutWindow> _instances;
private readonly PopoutSlotMap _slots;
private readonly ILogger<ChannelPopoutPool> _logger;
public ChannelPopoutPool(
Func<int, ChannelPopoutWindow> windowFactory,
ILogger<ChannelPopoutPool> logger
)
{
_logger = logger;
var capacity = Plugin.Config.MaxParallelPopouts;
_instances = new List<ChannelPopoutWindow>(capacity);
for (var i = 0; i < capacity; i++)
_instances.Add(windowFactory(i));
_slots = new PopoutSlotMap(capacity);
}
// Iterated once by PluginLifecycle.RegisterWindows (framework thread) and
// by ChannelPopoutInitHostedService (PayloadHandler setter).
public IReadOnlyList<ChannelPopoutWindow> Instances => _instances;
public bool TryOpen(Tab tab)
{
// Filled in Phase B.
return false;
}
public void TryClose(Guid id)
{
// Filled in Phase B.
}
public bool IsOpen(Guid id) => _slots.IsActive(id);
}
@@ -0,0 +1,64 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Windowing;
using HellionChat.Ui.Components;
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 — see plan §B.2. 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
{
private readonly int _slotIndex;
private readonly MessageList _messages;
private readonly InputBar _input;
private readonly ILogger<ChannelPopoutWindow> _logger;
public ChannelPopoutWindow(
int slotIndex,
MessageList messages,
InputBar input,
ILogger<ChannelPopoutWindow> logger
)
: base($"{Plugin.PluginName}###hellion_popout_{slotIndex}")
{
_slotIndex = slotIndex;
_messages = messages;
_input = input;
_logger = logger;
IsOpen = false;
RespectCloseHotkey = false;
}
public int SlotIndex => _slotIndex;
public Tab? Bound { get; private set; }
// Post-build setter — see plan §B.2. Wired by ChannelPopoutInitHostedService.
public void AttachPayloadHandler(PayloadHandler handler) =>
_messages.AttachPayloadHandler(handler);
public void Bind(Tab tab)
{
// Filled in Phase B.
Bound = tab;
IsOpen = true;
}
public void Unbind()
{
Bound = null;
IsOpen = false;
}
public override void Draw()
{
// Filled in Phase B. Defensive guard so an unbound slot renders nothing.
if (Bound is null)
return;
}
}
+55
View File
@@ -0,0 +1,55 @@
namespace HellionChat.Ui.Windows;
// Pure slot bookkeeping for the channel-popout pool: maps a tab's session
// identifier (Guid) to a fixed slot index. Deliberately Dalamud-free so the
// Build-Suite can unit-test reserve/release/capacity in isolation
// (Dalamud-coupled classes cannot be instantiated in the xUnit AppDomain).
internal sealed class PopoutSlotMap
{
private readonly int _capacity;
private readonly Dictionary<Guid, int> _active = new();
private readonly bool[] _slotUsed;
public PopoutSlotMap(int capacity)
{
_capacity = capacity < 0 ? 0 : capacity;
_slotUsed = new bool[_capacity];
}
public int Count => _active.Count;
public bool IsActive(Guid id) => _active.ContainsKey(id);
// Reserves the lowest free slot for id and returns its index. If id is
// already bound, returns its existing slot (idempotent re-open). Returns
// -1 when the pool is full.
public int TryReserve(Guid id)
{
if (_active.TryGetValue(id, out var existing))
return existing;
for (var i = 0; i < _capacity; i++)
{
if (!_slotUsed[i])
{
_slotUsed[i] = true;
_active[id] = i;
return i;
}
}
return -1;
}
// Releases id's slot and returns its index, or -1 if id was not bound
// (idempotent no-op for unknown ids).
public int Release(Guid id)
{
if (!_active.TryGetValue(id, out var slot))
return -1;
_active.Remove(id);
_slotUsed[slot] = false;
return slot;
}
}