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.TabRenamePersistStep(this),
|
||||||
new SelfTests.NotificationSoundSelectStep(),
|
new SelfTests.NotificationSoundSelectStep(),
|
||||||
new SelfTests.SidebarGreetedGlyphStep(this),
|
new SelfTests.SidebarGreetedGlyphStep(this),
|
||||||
|
new SelfTests.SidebarSectionHeaderStep(this),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Re-surface the wizard for existing users when a major UX
|
// 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;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
|
using HellionChat.Resources;
|
||||||
using HellionChat.Themes;
|
using HellionChat.Themes;
|
||||||
using HellionChat.Ui.StyleEngine;
|
using HellionChat.Ui.StyleEngine;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
@@ -35,6 +36,10 @@ internal sealed class Sidebar
|
|||||||
// The SelfTest reads it after driving the real Draw — no dead service roundtrip.
|
// The SelfTest reads it after driving the real Draw — no dead service roundtrip.
|
||||||
internal int LastRenderedGreetedGlyphCount;
|
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
|
// Inline mirror of the old TabIconMapping table so the Ui layer carries
|
||||||
// its own glyph lookup once the standalone file is removed.
|
// its own glyph lookup once the standalone file is removed.
|
||||||
private static readonly Dictionary<string, FontAwesomeIcon> IconByName = new(
|
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)
|
public void Draw(float windowWidth, IList<Tab> tabs, ref Tab? activeTab)
|
||||||
{
|
{
|
||||||
LastRenderedGreetedGlyphCount = 0;
|
LastRenderedGreetedGlyphCount = 0;
|
||||||
|
LastDrawnSectionHeaderCount = 0;
|
||||||
|
|
||||||
if (!_fonts.FontsReady)
|
if (!_fonts.FontsReady)
|
||||||
{
|
{
|
||||||
@@ -120,18 +126,74 @@ internal sealed class Sidebar
|
|||||||
var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim);
|
var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim);
|
||||||
var dl = ImGui.GetWindowDrawList();
|
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++)
|
for (var i = 0; i < tabs.Count; i++)
|
||||||
DrawRow(
|
{
|
||||||
tabs[i],
|
if (TabLifecycleHelpers.IsInPinnedPool(tabs[i]))
|
||||||
i,
|
pinned.Add(i);
|
||||||
expanded,
|
else if (TabLifecycleHelpers.IsInUnpinnedPool(tabs[i]))
|
||||||
accentRgba,
|
unpinned.Add(i);
|
||||||
textAbgr,
|
else
|
||||||
mutedAbgr,
|
persistent.Add(i);
|
||||||
dimAbgr,
|
}
|
||||||
dl,
|
|
||||||
ref activeTab
|
persistent.AddRange(pinned);
|
||||||
);
|
persistent.AddRange(unpinned);
|
||||||
|
return persistent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawRow(
|
private void DrawRow(
|
||||||
|
|||||||
Reference in New Issue
Block a user