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