feat(sidebar): pinned section, dimmed pin glyph, configurable width

Smoke-test round 3 feedback from Jin:

- Sidebar now groups tabs into three sections rendered in this order:
  persistent → pinned TempTabs → unpinned TempTabs. Each TempTab
  section carries its own divider header ("Angepinnt (n)" / "Aktive
  Tells (n)"). Plugin.Config.Tabs order is untouched — only the
  display order changes, so tabI still mirrors the real index and
  LastTab/WantedTab stay consistent.

- The thumbtack glyph overlay on a pinned tab dropped from accent
  colour at full alpha to TextMuted at ~47% alpha. The section header
  is now the primary discoverability cue; the glyph is just a per-tab
  confirmation hint.

- Sidebar width is now a Config field (default 44, range 44-160).
  Slider lives in Theme & Layout under the existing Sidebar-Tab-View
  toggle. The icon button inside each row stretches with the width so
  a widened sidebar doesn't leave the icon floating in dead space.
This commit is contained in:
2026-05-13 10:16:53 +02:00
parent cddd29a986
commit 80b48ac3ad
6 changed files with 110 additions and 15 deletions
+6
View File
@@ -113,6 +113,11 @@ public class Configuration : IPluginConfiguration
public int AutoTellTabsLimit = 15; public int AutoTellTabsLimit = 15;
public bool AutoTellTabsCompactDisplay; public bool AutoTellTabsCompactDisplay;
public int AutoTellTabsHistoryPreload = 20; public int AutoTellTabsHistoryPreload = 20;
// Sidebar width in pixels. Default 44 mirrors the icon-only layout from
// v1.2.0; users can widen up to 160 to fit a section-header line like
// "Active Tells (3)" without truncation.
public int SidebarWidth = 44;
public bool AutoTellTabsShowGreetedToggle; public bool AutoTellTabsShowGreetedToggle;
public bool SeenPopOutInputHint; public bool SeenPopOutInputHint;
public bool PopOutInputEnabled = true; public bool PopOutInputEnabled = true;
@@ -339,6 +344,7 @@ public class Configuration : IPluginConfiguration
AutoTellTabsLimit = other.AutoTellTabsLimit; AutoTellTabsLimit = other.AutoTellTabsLimit;
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload; AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
SidebarWidth = other.SidebarWidth;
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle; AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
SeenPopOutInputHint = other.SeenPopOutInputHint; SeenPopOutInputHint = other.SeenPopOutInputHint;
+3
View File
@@ -177,6 +177,9 @@ internal class HellionStrings
internal static string PinTab_LimitReached => Get(nameof(PinTab_LimitReached)); internal static string PinTab_LimitReached => Get(nameof(PinTab_LimitReached));
internal static string PinTab_PinnedTooltip => Get(nameof(PinTab_PinnedTooltip)); internal static string PinTab_PinnedTooltip => Get(nameof(PinTab_PinnedTooltip));
internal static string PinTab_PinTooltip => Get(nameof(PinTab_PinTooltip)); internal static string PinTab_PinTooltip => Get(nameof(PinTab_PinTooltip));
internal static string PinTab_SectionHeader => Get(nameof(PinTab_SectionHeader));
internal static string Settings_ThemeAndLayout_SidebarWidth_Name => Get(nameof(Settings_ThemeAndLayout_SidebarWidth_Name));
internal static string Settings_ThemeAndLayout_SidebarWidth_Description => Get(nameof(Settings_ThemeAndLayout_SidebarWidth_Description));
// Hellion Chat — Auto-Tell-Tabs Chat settings tab // Hellion Chat — Auto-Tell-Tabs Chat settings tab
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title)); internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
@@ -404,6 +404,15 @@
<data name="PinTab_PinTooltip" xml:space="preserve"> <data name="PinTab_PinTooltip" xml:space="preserve">
<value>Angepinnte Tabs überleben Relog und behalten die Bindung an die Tell-Person.</value> <value>Angepinnte Tabs überleben Relog und behalten die Bindung an die Tell-Person.</value>
</data> </data>
<data name="PinTab_SectionHeader" xml:space="preserve">
<value>Angepinnt</value>
</data>
<data name="Settings_ThemeAndLayout_SidebarWidth_Name" xml:space="preserve">
<value>Sidebar-Breite</value>
</data>
<data name="Settings_ThemeAndLayout_SidebarWidth_Description" xml:space="preserve">
<value>Breite der Tab-Sidebar in Pixeln. Default (44 px) ist Icon-only; breiter machen damit Sektion-Header wie „Aktive Tells (3)" nicht abgeschnitten werden.</value>
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) --> <!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve"> <data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
@@ -398,6 +398,15 @@
<data name="PinTab_PinTooltip" xml:space="preserve"> <data name="PinTab_PinTooltip" xml:space="preserve">
<value>Pinned tabs survive relog and stay bound to this conversation partner.</value> <value>Pinned tabs survive relog and stay bound to this conversation partner.</value>
</data> </data>
<data name="PinTab_SectionHeader" xml:space="preserve">
<value>Pinned</value>
</data>
<data name="Settings_ThemeAndLayout_SidebarWidth_Name" xml:space="preserve">
<value>Sidebar width</value>
</data>
<data name="Settings_ThemeAndLayout_SidebarWidth_Description" xml:space="preserve">
<value>Width of the tab sidebar in pixels. The default (44 px) is icon-only; widen it to fit the section headers like "Active Tells (3)" without truncation.</value>
</data>
<data name="PinTab_LimitReached" xml:space="preserve"> <data name="PinTab_LimitReached" xml:space="preserve">
<value>Maximum of {0} pinned tell tabs reached. Unpin one first, or use Promote to permanent.</value> <value>Maximum of {0} pinned tell tabs reached. Unpin one first, or use Promote to permanent.</value>
</data> </data>
+63 -15
View File
@@ -1673,6 +1673,30 @@ public sealed class ChatLogWindow : Window
Plugin.WantedTab = null; Plugin.WantedTab = null;
} }
// Sidebar render order: persistent tabs in their original Plugin.Config.Tabs
// position, then pinned TempTabs, then unpinned TempTabs. Returns indices
// into Plugin.Config.Tabs so tabI in the loop body still mirrors the real
// list position (LastTab / WantedTab stay consistent).
private static List<int> BuildSidebarRenderOrder()
{
var tabs = Plugin.Config.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 DrawTabSidebar() private void DrawTabSidebar()
{ {
var currentTab = -1; var currentTab = -1;
@@ -1685,7 +1709,8 @@ public sealed class ChatLogWindow : Window
if (!tabTable.Success) if (!tabTable.Success)
return; return;
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, 44f); var sidebarWidth = Math.Clamp(Plugin.Config.SidebarWidth, 44, 160);
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, sidebarWidth);
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1); ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@@ -1704,23 +1729,42 @@ public sealed class ChatLogWindow : Window
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing())); ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
var previousTab = Plugin.CurrentTab; var previousTab = Plugin.CurrentTab;
// Divider rendered once before the first temp tab with a live unit counter. // Render order: persistent → pinned TempTabs → unpinned TempTabs.
// Underlying Plugin.Config.Tabs order is untouched (tabI mirrors
// the real list index), only the display sequence groups by
// section so each section can carry its own divider header.
var renderOrder = BuildSidebarRenderOrder();
var pinnedHeaderRendered = false;
var tempTabHeaderRendered = false; var tempTabHeaderRendered = false;
var tempTabCount = Plugin.Config.Tabs.Count(t => t.IsTempTab); var pinnedCount = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
var unpinnedTempCount = Plugin.Config.Tabs.Count(
TabLifecycleHelpers.IsInUnpinnedPool
);
for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++) foreach (var tabI in renderOrder)
{ {
var tab = Plugin.Config.Tabs[tabI]; var tab = Plugin.Config.Tabs[tabI];
if (tab.PopOut) if (tab.PopOut)
continue; continue;
if (tab.IsTempTab && !tempTabHeaderRendered) if (TabLifecycleHelpers.IsInPinnedPool(tab) && !pinnedHeaderRendered)
{ {
ImGui.Separator(); ImGui.Separator();
if (!Plugin.Config.AutoTellTabsCompactDisplay) if (!Plugin.Config.AutoTellTabsCompactDisplay)
{ {
ImGui.TextDisabled( ImGui.TextDisabled(
$"{HellionStrings.AutoTellTabs_SectionHeader} ({tempTabCount})" $"{HellionStrings.PinTab_SectionHeader} ({pinnedCount})"
);
}
pinnedHeaderRendered = true;
}
else if (TabLifecycleHelpers.IsInUnpinnedPool(tab) && !tempTabHeaderRendered)
{
ImGui.Separator();
if (!Plugin.Config.AutoTellTabsCompactDisplay)
{
ImGui.TextDisabled(
$"{HellionStrings.AutoTellTabs_SectionHeader} ({unpinnedTempCount})"
); );
} }
tempTabHeaderRendered = true; tempTabHeaderRendered = true;
@@ -1809,9 +1853,12 @@ public sealed class ChatLogWindow : Window
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor))) using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor)))
using (Plugin.FontManager.FontAwesome.Push()) using (Plugin.FontManager.FontAwesome.Push())
{ {
// Button stretches with the configured sidebar width so a
// user-widened sidebar feels intentional, not a 36px icon
// floating in empty space.
clicked = ImGui.Button( clicked = ImGui.Button(
$"{icon.ToIconString()}##sidebar-tab-{tabI}", $"{icon.ToIconString()}##sidebar-tab-{tabI}",
new Vector2(36f, ImGui.GetFrameHeight()) new Vector2(sidebarWidth - 8f, ImGui.GetFrameHeight())
); );
} }
@@ -1871,22 +1918,23 @@ public sealed class ChatLogWindow : Window
); );
} }
// Pin indicator: small thumbtack glyph top-left of the icon. // Pin indicator: subtle thumbtack glyph top-left of the icon.
// Sits opposite the unread dot so they never collide. // Muted colour because the "Pinned" section header already
// groups these tabs visually — this is just a per-tab
// confirmation glyph, not the primary discoverability cue.
if (tab.IsPinned) if (tab.IsPinned)
{ {
var min = ImGui.GetItemRectMin(); var min = ImGui.GetItemRectMin();
const float pinPadding = 2f; const float pinPadding = 1f;
var pinPos = new Vector2(min.X + pinPadding, min.Y + pinPadding); var pinPos = new Vector2(min.X + pinPadding, min.Y + pinPadding);
var pinColor = theme.Colors.TextMuted;
// Dim further so the glyph reads as a hint, not a badge.
var pinAbgr = ColourUtil.RgbaToAbgr(pinColor) & 0x77FFFFFFu;
using (Plugin.FontManager.FontAwesome.Push()) using (Plugin.FontManager.FontAwesome.Push())
{ {
ImGui ImGui
.GetWindowDrawList() .GetWindowDrawList()
.AddText( .AddText(pinPos, pinAbgr, FontAwesomeIcon.Thumbtack.ToIconString());
pinPos,
ColourUtil.RgbaToAbgr(theme.Colors.Accent),
FontAwesomeIcon.Thumbtack.ToIconString()
);
} }
} }
@@ -250,6 +250,26 @@ internal sealed class ThemeAndLayout : ISettingsTab
string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName) string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)
); );
if (Mutable.SidebarTabView)
{
var sidebarWidth = Mutable.SidebarWidth;
if (
ImGui.SliderInt(
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Name,
ref sidebarWidth,
44,
160,
$"{sidebarWidth} px"
)
)
{
Mutable.SidebarWidth = sidebarWidth;
}
ImGuiUtil.HelpMarker(
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Description
);
}
ImGui.Spacing(); ImGui.Spacing();
ImGui.Separator(); ImGui.Separator();
ImGui.Spacing(); ImGui.Spacing();