Restore pop-out exclusivity: hide popped tabs from main window + keybind/unread parity

This commit is contained in:
2026-06-16 18:30:23 +02:00
parent fa20b53455
commit 6733c7f92e
6 changed files with 129 additions and 31 deletions
+4 -25
View File
@@ -134,7 +134,10 @@ internal sealed class Sidebar
// the tab list itself stays untouched and every row keeps its
// ORIGINAL list index for PushID, so an open context-menu popup
// stays bound to its tab when sectioning moves it visually.
var renderOrder = BuildRenderOrder(tabs);
var renderOrder = TabLifecycleHelpers.BuildRenderOrder(
tabs,
t => _pool.IsOpen(t.Identifier)
);
var pinnedHeaderRendered = false;
var unpinnedHeaderRendered = false;
foreach (var i in renderOrder)
@@ -186,30 +189,6 @@ internal sealed class Sidebar
LastDrawnSectionHeaderCount++;
}
// Mirror of 1.5.6's BuildSidebarRenderOrder: returns indices into the
// live tab list grouped by section, so the list order itself is never
// mutated and headers gate on the first tab actually reached per pool
// (an empty pool draws neither separator nor header).
private static List<int> BuildRenderOrder(IList<Tab> tabs)
{
var persistent = new List<int>(tabs.Count);
var pinned = new List<int>();
var unpinned = new List<int>();
for (var i = 0; i < tabs.Count; i++)
{
if (TabLifecycleHelpers.IsInPinnedPool(tabs[i]))
pinned.Add(i);
else if (TabLifecycleHelpers.IsInUnpinnedPool(tabs[i]))
unpinned.Add(i);
else
persistent.Add(i);
}
persistent.AddRange(pinned);
persistent.AddRange(unpinned);
return persistent;
}
private void DrawRow(
Tab tab,
int index,
+8 -1
View File
@@ -18,11 +18,18 @@ internal sealed class TopTabBar
public void Draw(IList<Tab> tabs, ref Tab? activeTab)
{
var firstDrawn = true;
for (var i = 0; i < tabs.Count; i++)
{
var tab = tabs[i];
if (i > 0)
// POP-1b: a popped-out tab is owned by its pop-out window, not the main
// strip (1.5.6 exclusivity). Gate on the pool, not Tab.PopOut (stale flag).
if (_pool.IsOpen(tab.Identifier))
continue;
if (!firstDrawn)
ImGui.SameLine();
firstDrawn = false;
var selected = ReferenceEquals(tab, activeTab);
// Size the selectable to its own label width. A zero width makes ImGui
@@ -111,6 +111,11 @@ internal sealed class ChannelPopoutWindow : Window, IFocusableChatWindow
if (Bound is null)
return;
// POP-1f: 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(
+32 -4
View File
@@ -30,6 +30,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
private readonly Components.InputBar _input;
private readonly Components.StatusBar _status;
private readonly Lender<PayloadHandler> _handlerLender;
private readonly ChannelPopoutPool _pool;
private Tab? _activeTab;
@@ -51,7 +52,8 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
Components.MessageList messages,
Components.InputBar input,
Components.StatusBar status,
Lender<PayloadHandler> handlerLender
Lender<PayloadHandler> handlerLender,
ChannelPopoutPool pool
)
: base($"{Plugin.PluginName}###hellion-main")
{
@@ -62,6 +64,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
_input = input;
_status = status;
_handlerLender = handlerLender;
_pool = pool;
Size = new Vector2(DefaultWidth, DefaultHeight);
SizeCondition = ImGuiCond.FirstUseEver;
@@ -176,7 +179,13 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
if (idx < 0)
idx = 0; // active tab not in the list (mid-strip) -> start from the first
ActivateTab(tabs[TabLifecycleHelpers.WrapTabIndex(idx, delta, tabs.Count)]);
var nextIndex = TabLifecycleHelpers.NextMainTabIndex(
idx,
delta,
tabs,
t => _pool.IsOpen(t.Identifier)
);
ActivateTab(tabs[nextIndex]);
}
// Internal accessors for self-tests so the probes can reach the live
@@ -270,6 +279,25 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
TabLifecycleHelpers.OnTabActivated(reseed, active);
}
// POP-1c: a popped-out tab must not stay the main window's active surface
// (1.5.6 exclusivity). Re-anchor to the first non-popped tab the moment the
// active one is popped; null when every tab is popped (POP-1d guards Draw).
// Runs post-seed, before the sidebar/top-tab draw, so the popped tab never
// renders. Idempotent: PickMainActiveTab returns the same reference once
// settled, so OnTabActivated fires only on the pop frame.
var visibleActive = TabLifecycleHelpers.PickMainActiveTab(
_activeTab,
Plugin.Config.Tabs,
t => _pool.IsOpen(t.Identifier)
);
if (!ReferenceEquals(visibleActive, _activeTab))
{
var previousActive = _activeTab;
_activeTab = visibleActive;
if (visibleActive is not null)
TabLifecycleHelpers.OnTabActivated(visibleActive, previousActive);
}
// The active tab's messages are on screen, so it carries no unread badge
// (1.5.6 convention: zero the current tab every frame so the dot only ever
// shows on tabs you are NOT looking at).
@@ -336,8 +364,8 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
)
)
{
if (messages.Success)
_messages.Draw(_activeTab!);
if (messages.Success && _activeTab is not null)
_messages.Draw(_activeTab);
}
// Inside-mode inline render: measure first so PreviewHeight is fresh