Files
HellionChat/HellionChat/Ui/Windows/ChannelPopoutPool.cs
T
JonKazama-Hellion 5b738e6885 chore: comments say what the code does, not which task produced it
A comment that reads "MUST stay in lockstep with TryGetActiveCrossfade (K8)"
helps nobody outside the plan that used to have a K8 in it, and the plans are
not in this repo. Same for "Spec FR-4", "plan §B.2", "Sub-Task 4.4" and the
F/R/M/A/S round codes scattered through the style engine and the self-tests.

Personal names go too. "tester feedback from Jin (v1.4.7)" and "Flo decision
2026-06-15" carry the reason fine without naming anyone -- the version and the
reason are the parts a reader can act on, and a public repo should not need a
cast list to be read.

The rule applied throughout: keep the why, drop the reference. Version numbers
stay, since those resolve through the changelog. 77 files.

ChunkUtil also carried 281 lines of commented-out code -- an older ToChunks
variant and two helpers with no callers, inherited and never removed. Deleted;
git remembers them.
2026-08-19 21:50:31 +02:00

76 lines
2.7 KiB
C#

using HellionChat.Util;
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.
internal sealed class ChannelPopoutPool
{
private readonly List<ChannelPopoutWindow> _instances;
private readonly PopoutSlotMap _slots;
private readonly ILogger<ChannelPopoutPool> _logger;
private readonly int _capacity;
public ChannelPopoutPool(
Func<int, ChannelPopoutWindow> windowFactory,
ILogger<ChannelPopoutPool> logger
)
{
_logger = logger;
var capacity = Plugin.Config.MaxParallelPopouts;
_capacity = capacity;
_instances = new List<ChannelPopoutWindow>(capacity);
for (var i = 0; i < capacity; i++)
_instances.Add(windowFactory(i));
_slots = new PopoutSlotMap(capacity);
// Route each window's in-body close through the pool so closing releases
// the slot. Wired here (post-construction) rather than via ctor to avoid
// a Window->Pool edge that would re-enter pool resolution.
foreach (var window in _instances)
window.CloseRequested = TryClose;
}
// Iterated once by PluginLifecycle.RegisterWindows (framework thread) and
// by ChannelPopoutInitHostedService (PayloadHandler setter).
public IReadOnlyList<ChannelPopoutWindow> Instances => _instances;
public bool TryOpen(Tab tab)
{
// A popped tab gets its own input bar, so strip stale tell state first —
// otherwise a popped-out stale-tell tab would be a send surface that
// bypasses the click-path activation strip. Previous = the main window's
// active tab; popping the active tab itself must not strip (TR-4 guard).
TabLifecycleHelpers.OnTabActivated(tab, Plugin.Instance.MainWindow?.ActiveTab);
var slot = _slots.TryReserve(tab.Identifier);
if (slot < 0)
{
_logger.LogWarning(
"Channel popout pool is full ({Capacity} slots); ignoring open for {Name}.",
_capacity,
tab.Name
);
return false;
}
_instances[slot].Bind(tab);
return true;
}
public void TryClose(Guid id)
{
var slot = _slots.Release(id);
if (slot < 0)
return; // idempotent: unknown/unbound id is a silent no-op
_instances[slot].Unbind();
}
public bool IsOpen(Guid id) => _slots.IsActive(id);
}