feat(sidebar): restore section headers and compact separators
This commit is contained in:
@@ -403,6 +403,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
new SelfTests.TabRenamePersistStep(this),
|
||||
new SelfTests.NotificationSoundSelectStep(),
|
||||
new SelfTests.SidebarGreetedGlyphStep(this),
|
||||
new SelfTests.SidebarSectionHeaderStep(this),
|
||||
]);
|
||||
|
||||
// Re-surface the wizard for existing users when a major UX
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin.SelfTest;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// B3-4: section headers render once per non-empty temp-tab pool, and compact
|
||||
// mode suppresses the header text (separators stay). Drives the REAL
|
||||
// Sidebar.Draw inside the /xlperf window frame (same render precedent as
|
||||
// SidebarGreetedGlyphStep) and reads the render observability counter.
|
||||
// Injects a mixed tab set (persistent + unpinned temp + pinned temp) and
|
||||
// restores config in finally.
|
||||
internal sealed class SidebarSectionHeaderStep : ISelfTestStep
|
||||
{
|
||||
private readonly Plugin plugin;
|
||||
|
||||
public SidebarSectionHeaderStep(Plugin plugin) => this.plugin = plugin;
|
||||
|
||||
public string Name => "Hellion Chat - Sidebar section header";
|
||||
|
||||
public SelfTestStepResult RunStep()
|
||||
{
|
||||
var sidebar = plugin.MainWindow.GetSidebarForSelfTest();
|
||||
if (sidebar is null)
|
||||
{
|
||||
ImGui.Text("Sidebar null");
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
var savedCompact = Plugin.Config.AutoTellTabsCompactDisplay;
|
||||
var savedSidebarWidth = Plugin.Config.SidebarWidth;
|
||||
|
||||
// Both headers need a populated pool behind them. Persistent tabs
|
||||
// normally already exist — inject a probe only when the live config
|
||||
// has none, so the section order has a real first section.
|
||||
var injected = new List<Tab>();
|
||||
if (Plugin.Config.Tabs.All(t => t.IsTempTab))
|
||||
{
|
||||
injected.Add(
|
||||
new Tab
|
||||
{
|
||||
Name = "Persistent Probe@SelfTest",
|
||||
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
|
||||
{
|
||||
[ChatType.Say] = (ChatSourceExt.All, ChatSourceExt.All),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
injected.Add(BuildTempProbe("Tell Probe@SelfTest", pinned: false));
|
||||
injected.Add(BuildTempProbe("Pinned Probe@SelfTest", pinned: true));
|
||||
foreach (var tab in injected)
|
||||
Plugin.Config.Tabs.Add(tab);
|
||||
|
||||
Tab? active = null;
|
||||
var width = (float)Plugin.Config.SidebarAutoSwitchThresholdPx + 100f; // expanded
|
||||
try
|
||||
{
|
||||
// Headers are not width-gated, but the pinned width keeps the step
|
||||
// uniform with SidebarGreetedGlyphStep (expanded rows, no min-drag
|
||||
// row drops while the probes render).
|
||||
Plugin.Config.SidebarWidth = 220;
|
||||
|
||||
Plugin.Config.AutoTellTabsCompactDisplay = false;
|
||||
sidebar.Draw(width, Plugin.Config.Tabs, ref active);
|
||||
if (sidebar.LastDrawnSectionHeaderCount != 2)
|
||||
{
|
||||
ImGui.Text(
|
||||
$"Expected 2 section headers with compact OFF, got {sidebar.LastDrawnSectionHeaderCount}"
|
||||
);
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
|
||||
Plugin.Config.AutoTellTabsCompactDisplay = true;
|
||||
sidebar.Draw(width, Plugin.Config.Tabs, ref active);
|
||||
if (sidebar.LastDrawnSectionHeaderCount != 0)
|
||||
{
|
||||
ImGui.Text(
|
||||
$"Compact ON must suppress header text, got {sidebar.LastDrawnSectionHeaderCount}"
|
||||
);
|
||||
return SelfTestStepResult.Fail;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var tab in injected)
|
||||
Plugin.Config.Tabs.Remove(tab);
|
||||
Plugin.Config.AutoTellTabsCompactDisplay = savedCompact;
|
||||
Plugin.Config.SidebarWidth = savedSidebarWidth;
|
||||
}
|
||||
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
public void CleanUp() { }
|
||||
|
||||
// Mirror of AutoTellTabsService.BuildTempTab (the real builder is
|
||||
// private); only the sheet-based tab name is replaced with a literal.
|
||||
private static Tab BuildTempProbe(string name, bool pinned) =>
|
||||
new()
|
||||
{
|
||||
Name = name,
|
||||
IsTempTab = true,
|
||||
IsPinned = pinned,
|
||||
AllSenderMessages = true,
|
||||
TellTarget = new TellTarget(name, 0, 0, TellReason.Direct),
|
||||
Channel = InputChannel.Tell,
|
||||
DisplayTimestamp = true,
|
||||
UnreadMode = UnreadMode.Unseen,
|
||||
HideWhenInactive = false,
|
||||
SelectedChannels = new Dictionary<ChatType, (ChatSource, ChatSource)>
|
||||
{
|
||||
[ChatType.TellIncoming] = (ChatSourceExt.All, ChatSourceExt.All),
|
||||
[ChatType.TellOutgoing] = (ChatSourceExt.All, ChatSourceExt.All),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Ui.StyleEngine;
|
||||
using HellionChat.Util;
|
||||
@@ -35,6 +36,10 @@ internal sealed class Sidebar
|
||||
// The SelfTest reads it after driving the real Draw — no dead service roundtrip.
|
||||
internal int LastRenderedGreetedGlyphCount;
|
||||
|
||||
// B3-4 render observability: section headers actually drawn this frame.
|
||||
// Incremented only in the real header branch; reset at Draw start.
|
||||
internal int LastDrawnSectionHeaderCount;
|
||||
|
||||
// Inline mirror of the old TabIconMapping table so the Ui layer carries
|
||||
// its own glyph lookup once the standalone file is removed.
|
||||
private static readonly Dictionary<string, FontAwesomeIcon> IconByName = new(
|
||||
@@ -100,6 +105,7 @@ internal sealed class Sidebar
|
||||
public void Draw(float windowWidth, IList<Tab> tabs, ref Tab? activeTab)
|
||||
{
|
||||
LastRenderedGreetedGlyphCount = 0;
|
||||
LastDrawnSectionHeaderCount = 0;
|
||||
|
||||
if (!_fonts.FontsReady)
|
||||
{
|
||||
@@ -120,18 +126,74 @@ internal sealed class Sidebar
|
||||
var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim);
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
|
||||
// B3-4 sectioned render order (1.5.6 parity): persistent → pinned
|
||||
// TempTabs → unpinned TempTabs. Only the display sequence regroups;
|
||||
// the tab list itself stays untouched and every row keeps its
|
||||
// ORIGINAL list index for PushID, so an open context-menu popup
|
||||
// stays bound to its tab when sectioning moves it visually.
|
||||
var renderOrder = BuildRenderOrder(tabs);
|
||||
var pinnedHeaderRendered = false;
|
||||
var unpinnedHeaderRendered = false;
|
||||
foreach (var i in renderOrder)
|
||||
{
|
||||
var tab = tabs[i];
|
||||
if (TabLifecycleHelpers.IsInPinnedPool(tab) && !pinnedHeaderRendered)
|
||||
{
|
||||
DrawSectionHeader(
|
||||
HellionStrings.PinTab_SectionHeader,
|
||||
Plugin.Instance.AutoTellTabsService.PinnedTempTabCount
|
||||
);
|
||||
pinnedHeaderRendered = true;
|
||||
}
|
||||
else if (TabLifecycleHelpers.IsInUnpinnedPool(tab) && !unpinnedHeaderRendered)
|
||||
{
|
||||
DrawSectionHeader(
|
||||
HellionStrings.AutoTellTabs_SectionHeader,
|
||||
Plugin.Instance.AutoTellTabsService.ActiveTempTabCount
|
||||
);
|
||||
unpinnedHeaderRendered = true;
|
||||
}
|
||||
|
||||
DrawRow(tab, i, expanded, accentRgba, textAbgr, mutedAbgr, dimAbgr, dl, ref activeTab);
|
||||
}
|
||||
}
|
||||
|
||||
// Section transition marker (1.5.6 parity): the separator always renders,
|
||||
// compact mode suppresses only the header text. Real cursor-advancing
|
||||
// widgets on purpose — rows advance the cursor via InvisibleButton, so a
|
||||
// drawlist-only header would overlap the next row.
|
||||
private void DrawSectionHeader(string header, int count)
|
||||
{
|
||||
ImGui.Separator();
|
||||
if (Plugin.Config.AutoTellTabsCompactDisplay)
|
||||
return;
|
||||
|
||||
ImGui.TextDisabled($"{header} ({count})");
|
||||
LastDrawnSectionHeaderCount++;
|
||||
}
|
||||
|
||||
// Mirror of 1.5.6's BuildSidebarRenderOrder: returns indices into the
|
||||
// live tab list grouped by section, so the list order itself is never
|
||||
// mutated and headers gate on the first tab actually reached per pool
|
||||
// (an empty pool draws neither separator nor header).
|
||||
private static List<int> BuildRenderOrder(IList<Tab> 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++)
|
||||
DrawRow(
|
||||
tabs[i],
|
||||
i,
|
||||
expanded,
|
||||
accentRgba,
|
||||
textAbgr,
|
||||
mutedAbgr,
|
||||
dimAbgr,
|
||||
dl,
|
||||
ref activeTab
|
||||
);
|
||||
{
|
||||
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 DrawRow(
|
||||
|
||||
Reference in New Issue
Block a user