diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index 9faa784..179eaa5 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -44,6 +44,10 @@ internal sealed class Sidebar internal int LastRenderedGreetedGlyphCount; internal int LastRenderedUnreadDotCount; + // Small enough to read as a marker on the icon rather than as a second icon + // beside it. + private const float PinGlyphScale = 0.6f; + // B3-4 render observability: section headers actually drawn this frame. // Incremented only in the real header branch; reset at Draw start. internal int LastDrawnSectionHeaderCount; @@ -348,15 +352,40 @@ internal sealed class Sidebar // height would misplace the glyph at any other body size. var iconStr = icon.ToIconString(); var iconSize = ImGui.CalcTextSize(iconStr); - dl.AddText( + var iconPos = origin - + new Vector2(iconInset + contentX, MetricsMath.CenterY(RowHeight, iconSize.Y)), - iconColor, - iconStr - ); + + new Vector2(iconInset + contentX, MetricsMath.CenterY(RowHeight, iconSize.Y)); + dl.AddText(iconPos, iconColor, iconStr); iconRight = iconInset + contentX + iconSize.X; + + // Pinned marker: a small thumbtack tucked into the icon's lower + // left. Drawn from the same font push and inside the row rectangle + // the InvisibleButton already reserved, so it claims no layout of + // its own -- badges in this very sidebar are where "draw into + // unreserved space" caught this project last. + // + // Lower left because the unread dot owns the upper right. + if (tab.IsPinned) + { + var pinStr = FontAwesomeIcon.Thumbtack.ToIconString(); + var pinFontSize = ImGui.GetFontSize() * PinGlyphScale; + var pinSize = ImGui.CalcTextSize(pinStr) * PinGlyphScale; + dl.AddText( + ImGui.GetFont(), + pinFontSize, + iconPos + new Vector2(-pinSize.X * 0.45f, iconSize.Y - pinSize.Y * 0.75f), + _palette.Abgr(Token.AccentPrimary, colors), + pinStr + ); + } } + // Only for pinned rows, and only in the sidebar's own hover state -- + // this is the one place a user meets the marker without having opened + // the menu that produced it. + if (tab.IsPinned && surfaceHovered) + ImGuiUtil.Tooltip(HellionStrings.PinTab_PinnedTooltip); + if (expanded) dl.AddText(origin + new Vector2(iconRight + 6f * scale, contentY), textAbgr, tab.Name); diff --git a/HellionChat/Ui/Components/TabContextMenu.cs b/HellionChat/Ui/Components/TabContextMenu.cs index f11b639..10ffa80 100644 --- a/HellionChat/Ui/Components/TabContextMenu.cs +++ b/HellionChat/Ui/Components/TabContextMenu.cs @@ -97,6 +97,63 @@ internal static class TabContextMenu if (ImGui.MenuItem("Pop Out")) pool.TryOpen(tab); + + DrawPinControls(tab); + } + + // Pinning has been complete since v1.4.7 -- pools, cap, persistence, logout + // symmetry, the notification -- and has had no way in since the menu that + // called it was removed. + // + // That left a dead end in saved data, which is the real reason this is here: + // a tab pinned in v1.5.6 survives every save and load, permanently occupying + // one of five pool slots, with nothing anywhere to release it. + // + // Promote-to-permanent deliberately does not come back. It was removed on + // purpose after a tester kept hitting it by accident, and reconnecting every + // caller-less method without asking why it lost its caller would rebuild the + // problem. + private static void DrawPinControls(Tab tab) + { + if (!tab.IsTempTab) + return; + + // Instance property today, not the static the old menu reached for. + var service = Plugin.Instance.AutoTellTabsService; + if (service is null) + return; + + ImGui.Separator(); + + if (tab.IsPinned) + { + if (ImGui.MenuItem(HellionStrings.PinTab_MenuUnpin)) + { + service.Unpin(tab); + ImGui.CloseCurrentPopup(); + } + + return; + } + + var atCap = service.PinnedTempTabCount >= AutoTellTabsService.MaxPinnedTempTabs; + + // Disabled rather than absent: the cap is a state the user can undo by + // unpinning something, and the tooltip below is what says so. + if (ImGui.MenuItem(HellionStrings.PinTab_MenuPin, enabled: !atCap) && service.TryPin(tab)) + ImGui.CloseCurrentPopup(); + + if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + return; + + ImGuiUtil.Tooltip( + atCap + ? string.Format( + HellionStrings.PinTab_LimitReached, + AutoTellTabsService.MaxPinnedTempTabs + ) + : HellionStrings.PinTab_PinTooltip + ); } // The flush depends on Draw running once more for this tab. If it never does —