fix(ui): guard sidebar row at min drag and widen system-icon match

DrawRow asserted on a zero-width InvisibleButton when the window was
dragged below the pop-out hit threshold — the row now drops out cleanly
under 2px of remaining sidebar width, and the pop-out button only
splits off when there's room for both hit areas. The trailing pop-out
icon is hidden too when its strip is collapsed, so the row stays as a
single selectable strip on extreme drags.

System icon path: ResolveTabIcon used to look only at the first key in
SelectedChannels, so a System tab whose first filter happened to be a
generic ChatType slipped through to Comment. The resolve now walks
every key and keeps the first non-Comment match, and a final
case-insensitive name match flips the icon to fa-cog when the user's
filter set falls completely outside the channel-type table.
This commit is contained in:
2026-05-23 21:08:22 +02:00
parent 2f099fd4e1
commit 8e7149cadc
+38 -11
View File
@@ -110,10 +110,22 @@ internal sealed class Sidebar
var origin = ImGui.GetCursorScreenPos(); var origin = ImGui.GetCursorScreenPos();
var avail = ImGui.GetContentRegionAvail().X; var avail = ImGui.GetContentRegionAvail().X;
var tabHitWidth = MathF.Max(0f, avail - PopOutHitWidth);
// Tab hit area sits left of the pop-out button so the two never // Drop the row entirely when the sidebar is dragged below the width
// steal each other's clicks. // of a single hit target. ImGui's InvisibleButton asserts on a
// zero-width size, which crashes the whole window at min-drag.
if (avail < 2f)
{
ImGui.PopID();
return;
}
// Only split off a separate pop-out hit area when there's room for
// both buttons. Below that, the whole row stays as a single
// selectable strip without the pop-out affordance.
var hasPopOut = avail > PopOutHitWidth + 4f;
var tabHitWidth = hasPopOut ? avail - PopOutHitWidth : avail;
ImGui.InvisibleButton("row", new Vector2(tabHitWidth, RowHeight)); ImGui.InvisibleButton("row", new Vector2(tabHitWidth, RowHeight));
var rowHovered = ImGui.IsItemHovered(); var rowHovered = ImGui.IsItemHovered();
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
@@ -144,13 +156,17 @@ internal sealed class Sidebar
ImGui.EndPopup(); ImGui.EndPopup();
} }
var popHovered = false;
if (hasPopOut)
{
ImGui.SameLine(0f, 0f); ImGui.SameLine(0f, 0f);
ImGui.InvisibleButton("popout", new Vector2(PopOutHitWidth, RowHeight)); ImGui.InvisibleButton("popout", new Vector2(PopOutHitWidth, RowHeight));
var popHovered = ImGui.IsItemHovered(); popHovered = ImGui.IsItemHovered();
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
LogPopOutStub(tab); LogPopOutStub(tab);
}
if (rowHovered || popHovered) if (hasPopOut && (rowHovered || popHovered))
{ {
using (_fonts.FontAwesome.Push()) using (_fonts.FontAwesome.Push())
{ {
@@ -174,12 +190,23 @@ internal sealed class Sidebar
if (tab.IsTempTab) if (tab.IsTempTab)
return FontAwesomeIcon.Envelope; return FontAwesomeIcon.Envelope;
// Channel-type fallback. The v1.5.6 TabIconGlyphResolver did the // Channel-type fallback. Walk every selected key, not just the first,
// same thing — picks the first selected channel and maps its // so a System tab that filters multiple system-flavoured ChatTypes
// ChatType to a category icon so tabs without a user-set icon // still picks up fa-cog when one of the later keys carries the match.
// still look distinct. // The Comment default only wins when every key falls into the
if (tab.SelectedChannels.Count > 0) // generic-text bucket (Say / Yell / Shout etc.).
return ResolveByChannelType(tab.SelectedChannels.Keys.First()); foreach (var chatType in tab.SelectedChannels.Keys)
{
var glyph = ResolveByChannelType(chatType);
if (glyph != FontAwesomeIcon.Comment)
return glyph;
}
// Last-resort name match for tabs that filter exotic ChatTypes the
// mapping above doesn't cover — keeps the System tab visually
// distinct even with a custom channel set.
if (tab.Name.Contains("system", StringComparison.OrdinalIgnoreCase))
return FontAwesomeIcon.Cog;
return FontAwesomeIcon.Comment; return FontAwesomeIcon.Comment;
} }