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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user