fix(unread): restore the tab unread badge and fix the post-F2 unread decision

The v1.8.x sidebar/top-bar rebuild never re-rendered the unread dot, so inactive tabs showed no badge even though the counter was tracked. Draw it again top-right of the tab icon in both Sidebar and TopTabBar, gated on !active && UnreadMode != None && Unread > 0, and zero the active tab's counter every frame (1.5.6 convention) so the dot only ever shows on tabs you are not looking at.

The unread decision moves to MessageManager.ShouldCountUnread and snapshots the active tab + whether it shows the message once before the loop: Unseen suppresses unread on an inactive tab only when the active (real, post-F2) tab also shows that message. Adds SidebarUnreadDotStep (render) and UnreadDecisionStep (decision) self-tests (step count 32 -> 34).
This commit is contained in:
2026-06-13 18:49:27 +02:00
parent 0ca8513065
commit 5dfe8e3b49
7 changed files with 221 additions and 8 deletions
+30 -2
View File
@@ -35,6 +35,7 @@ internal sealed class Sidebar
// Incremented ONLY in the real glyph branch in DrawRow; reset at Draw start.
// The SelfTest reads it after driving the real Draw — no dead service roundtrip.
internal int LastRenderedGreetedGlyphCount;
internal int LastRenderedUnreadDotCount;
// B3-4 render observability: section headers actually drawn this frame.
// Incremented only in the real header branch; reset at Draw start.
@@ -105,6 +106,7 @@ internal sealed class Sidebar
public void Draw(float windowWidth, IList<Tab> tabs, ref Tab? activeTab)
{
LastRenderedGreetedGlyphCount = 0;
LastRenderedUnreadDotCount = 0;
LastDrawnSectionHeaderCount = 0;
if (!_fonts.FontsReady)
@@ -124,6 +126,7 @@ internal sealed class Sidebar
var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
var mutedAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted);
var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim);
var dangerAbgr = ColourUtil.RgbaToAbgr(theme.Colors.StatusDanger);
var dl = ImGui.GetWindowDrawList();
// B3-4 sectioned render order (1.5.6 parity): persistent → pinned
@@ -154,7 +157,18 @@ internal sealed class Sidebar
unpinnedHeaderRendered = true;
}
DrawRow(tab, i, expanded, accentRgba, textAbgr, mutedAbgr, dimAbgr, dl, ref activeTab);
DrawRow(
tab,
i,
expanded,
accentRgba,
textAbgr,
mutedAbgr,
dimAbgr,
dangerAbgr,
dl,
ref activeTab
);
}
}
@@ -204,6 +218,7 @@ internal sealed class Sidebar
uint textAbgr,
uint mutedAbgr,
uint dimAbgr,
uint dangerAbgr,
ImDrawListPtr dl,
ref Tab? activeTab
)
@@ -276,7 +291,20 @@ internal sealed class Sidebar
// Icon and label shift right by the greeted slot when it is shown.
var contentX = showGreeted ? GreetedHitWidth : 0f;
using (_fonts.FontAwesome.Push())
dl.AddText(origin + new Vector2(10f + contentX, 8f), iconColor, icon.ToIconString());
{
var iconStr = icon.ToIconString();
dl.AddText(origin + new Vector2(10f + contentX, 8f), iconColor, iconStr);
// 1.5.6-parity unread dot, top-right of the icon. The active tab is
// zeroed every frame (MainWindow.Draw), so the dot never shows on the
// tab you're viewing; UnreadMode.None opts a tab out entirely.
if (!isCurrentTab && tab.UnreadMode != UnreadMode.None && tab.Unread > 0)
{
var iconRight = 10f + contentX + ImGui.CalcTextSize(iconStr).X;
dl.AddCircleFilled(origin + new Vector2(iconRight - 2f, 6f), 4f, dangerAbgr, 12);
LastRenderedUnreadDotCount++;
}
}
if (expanded)
dl.AddText(origin + new Vector2(32f + contentX, 8f), textAbgr, tab.Name);
+20
View File
@@ -39,6 +39,26 @@ internal sealed class TopTabBar
TabLifecycleHelpers.OnTabActivated(tab, previous);
}
// 1.5.6-parity unread dot at the item's top-right. Gate on the
// POST-click selection (not the frame-start 'selected') so clicking a
// tab suppresses its dot the same frame, like the sidebar. The active
// tab is also zeroed every frame (MainWindow.Draw).
if (
!ReferenceEquals(tab, activeTab)
&& tab.UnreadMode != UnreadMode.None
&& tab.Unread > 0
)
{
var max = ImGui.GetItemRectMax();
var min = ImGui.GetItemRectMin();
var danger = ColourUtil.RgbaToAbgr(
Plugin.Instance.ThemeRegistry.Active.Colors.StatusDanger
);
ImGui
.GetWindowDrawList()
.AddCircleFilled(new Vector2(max.X - 4f, min.Y + 4f), 3.5f, danger, 12);
}
TabContextMenu.Draw(tab, $"toptab_ctx_{i}", _pool);
}
+6
View File
@@ -192,6 +192,12 @@ internal sealed class MainWindow : Window
TabLifecycleHelpers.OnTabActivated(reseed, active);
}
// 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).
if (_activeTab is { } seenTab)
seenTab.Unread = 0;
var statusHeight = Components.StatusBar.Height;
using (var body = ImRaii.Child("##hellion-body", new Vector2(-1f, -statusHeight)))