feat(sidebar): restore per-tab greeted toggle glyph
This commit is contained in:
@@ -402,6 +402,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
new SelfTests.TellPillTransparencyStep(this),
|
new SelfTests.TellPillTransparencyStep(this),
|
||||||
new SelfTests.TabRenamePersistStep(this),
|
new SelfTests.TabRenamePersistStep(this),
|
||||||
new SelfTests.NotificationSoundSelectStep(),
|
new SelfTests.NotificationSoundSelectStep(),
|
||||||
|
new SelfTests.SidebarGreetedGlyphStep(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,103 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
using HellionChat.Code;
|
||||||
|
using HellionChat.GameFunctions.Types;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// B3-2: greeted glyph renders only for temp tabs when the toggle is on. Drives
|
||||||
|
// the REAL Sidebar.Draw (render precedent: HonorificHeaderRenderStep, the only
|
||||||
|
// real .Draw in this pool — NOT SidebarModeAutoSwitchStep which only calls
|
||||||
|
// IsExpanded/GetWidth) inside the /xlperf window frame and reads the render
|
||||||
|
// observability counter, then drives the real toggle hook both ways. Injects
|
||||||
|
// a temp tab and restores config in finally.
|
||||||
|
internal sealed class SidebarGreetedGlyphStep : ISelfTestStep
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
public SidebarGreetedGlyphStep(Plugin plugin) => this.plugin = plugin;
|
||||||
|
|
||||||
|
public string Name => "Hellion Chat - Sidebar greeted glyph";
|
||||||
|
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var sidebar = plugin.MainWindow.GetSidebarForSelfTest();
|
||||||
|
if (sidebar is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("Sidebar null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedFlag = Plugin.Config.AutoTellTabsShowGreetedToggle;
|
||||||
|
var savedSidebarWidth = Plugin.Config.SidebarWidth;
|
||||||
|
|
||||||
|
// Mirror of AutoTellTabsService.BuildTempTab (the real builder is
|
||||||
|
// private); only the sheet-based tab name is replaced with a literal.
|
||||||
|
var injected = new Tab
|
||||||
|
{
|
||||||
|
Name = "Greeted Probe@SelfTest",
|
||||||
|
IsTempTab = true,
|
||||||
|
AllSenderMessages = true,
|
||||||
|
TellTarget = new TellTarget("Greeted Probe", 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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Plugin.Config.Tabs.Add(injected);
|
||||||
|
Tab? active = null;
|
||||||
|
var width = (float)Plugin.Config.SidebarAutoSwitchThresholdPx + 100f; // expanded
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Plugin.Config.AutoTellTabsShowGreetedToggle = true;
|
||||||
|
// Default SidebarWidth (44px) has no room for the third hit area;
|
||||||
|
// pin a wide value so the glyph branch is reachable, restore after.
|
||||||
|
Plugin.Config.SidebarWidth = 220;
|
||||||
|
sidebar.Draw(width, Plugin.Config.Tabs, ref active);
|
||||||
|
if (sidebar.LastRenderedGreetedGlyphCount == 0)
|
||||||
|
{
|
||||||
|
ImGui.Text("No greeted glyph drawn with flag ON");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin.Config.AutoTellTabsShowGreetedToggle = false;
|
||||||
|
sidebar.Draw(width, Plugin.Config.Tabs, ref active);
|
||||||
|
if (sidebar.LastRenderedGreetedGlyphCount != 0)
|
||||||
|
{
|
||||||
|
ImGui.Text("Greeted glyph drawn with flag OFF");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drive the same hook DrawRow's click handler uses (the real toggle
|
||||||
|
// path, not a direct MarkGreeted call) and assert the flip both ways.
|
||||||
|
sidebar.ToggleGreetedForSelfTest(injected);
|
||||||
|
if (!plugin.AutoTellTabsService.IsGreeted(injected))
|
||||||
|
{
|
||||||
|
ImGui.Text("Toggle did not mark the tab greeted");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar.ToggleGreetedForSelfTest(injected);
|
||||||
|
if (plugin.AutoTellTabsService.IsGreeted(injected))
|
||||||
|
{
|
||||||
|
ImGui.Text("Toggle did not unmark the tab greeted");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Plugin.Config.Tabs.Remove(injected);
|
||||||
|
Plugin.Config.AutoTellTabsShowGreetedToggle = savedFlag;
|
||||||
|
Plugin.Config.SidebarWidth = savedSidebarWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUp() { }
|
||||||
|
}
|
||||||
@@ -28,6 +28,12 @@ internal sealed class Sidebar
|
|||||||
|
|
||||||
private const float RowHeight = 32f;
|
private const float RowHeight = 32f;
|
||||||
private const float PopOutHitWidth = 22f;
|
private const float PopOutHitWidth = 22f;
|
||||||
|
private const float GreetedHitWidth = 22f;
|
||||||
|
|
||||||
|
// B3-2 render observability: counts greeted glyphs actually drawn this frame.
|
||||||
|
// Incremented ONLY in the real glyph branch in DrawRow; reset at Draw start.
|
||||||
|
// The SelfTest reads it after driving the real Draw — no dead service roundtrip.
|
||||||
|
internal int LastRenderedGreetedGlyphCount;
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -81,8 +87,20 @@ internal sealed class Sidebar
|
|||||||
? Math.Clamp((float)Plugin.Config.SidebarWidth, MinSidebarWidth, MaxSidebarWidth)
|
? Math.Clamp((float)Plugin.Config.SidebarWidth, MinSidebarWidth, MaxSidebarWidth)
|
||||||
: IconOnlyWidth;
|
: IconOnlyWidth;
|
||||||
|
|
||||||
|
// Factored click logic so the SelfTest exercises the real toggle, not a direct
|
||||||
|
// MarkGreeted call (which would be a dead path the render never takes).
|
||||||
|
internal void ToggleGreetedForSelfTest(Tab tab)
|
||||||
|
{
|
||||||
|
if (Plugin.Instance.AutoTellTabsService.IsGreeted(tab))
|
||||||
|
Plugin.Instance.AutoTellTabsService.UnmarkGreeted(tab);
|
||||||
|
else
|
||||||
|
Plugin.Instance.AutoTellTabsService.MarkGreeted(tab);
|
||||||
|
}
|
||||||
|
|
||||||
public void Draw(float windowWidth, IList<Tab> tabs, ref Tab? activeTab)
|
public void Draw(float windowWidth, IList<Tab> tabs, ref Tab? activeTab)
|
||||||
{
|
{
|
||||||
|
LastRenderedGreetedGlyphCount = 0;
|
||||||
|
|
||||||
if (!_fonts.FontsReady)
|
if (!_fonts.FontsReady)
|
||||||
{
|
{
|
||||||
ImGui.Dummy(new Vector2(IconOnlyWidth, 0));
|
ImGui.Dummy(new Vector2(IconOnlyWidth, 0));
|
||||||
@@ -99,10 +117,21 @@ internal sealed class Sidebar
|
|||||||
var accentRgba = _resolver.Resolve(Token.AccentPrimary, theme.Colors);
|
var accentRgba = _resolver.Resolve(Token.AccentPrimary, theme.Colors);
|
||||||
var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
||||||
var mutedAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted);
|
var mutedAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted);
|
||||||
|
var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim);
|
||||||
var dl = ImGui.GetWindowDrawList();
|
var dl = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
for (var i = 0; i < tabs.Count; i++)
|
for (var i = 0; i < tabs.Count; i++)
|
||||||
DrawRow(tabs[i], i, expanded, accentRgba, textAbgr, mutedAbgr, dl, ref activeTab);
|
DrawRow(
|
||||||
|
tabs[i],
|
||||||
|
i,
|
||||||
|
expanded,
|
||||||
|
accentRgba,
|
||||||
|
textAbgr,
|
||||||
|
mutedAbgr,
|
||||||
|
dimAbgr,
|
||||||
|
dl,
|
||||||
|
ref activeTab
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawRow(
|
private void DrawRow(
|
||||||
@@ -112,6 +141,7 @@ internal sealed class Sidebar
|
|||||||
uint accentRgba,
|
uint accentRgba,
|
||||||
uint textAbgr,
|
uint textAbgr,
|
||||||
uint mutedAbgr,
|
uint mutedAbgr,
|
||||||
|
uint dimAbgr,
|
||||||
ImDrawListPtr dl,
|
ImDrawListPtr dl,
|
||||||
ref Tab? activeTab
|
ref Tab? activeTab
|
||||||
)
|
)
|
||||||
@@ -130,11 +160,26 @@ internal sealed class Sidebar
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1.5.6 parity: greeted state dims the tab icon whenever the toggle is
|
||||||
|
// configured on. The clickable affordance additionally needs an expanded
|
||||||
|
// sidebar with room for a third hit area beside the pop-out slot — in
|
||||||
|
// icon-only or min-drag mode it is skipped entirely.
|
||||||
|
var greetedConfigured = tab.IsTempTab && Plugin.Config.AutoTellTabsShowGreetedToggle;
|
||||||
|
var showGreeted =
|
||||||
|
greetedConfigured && expanded && avail > GreetedHitWidth + PopOutHitWidth + 4f;
|
||||||
|
|
||||||
// Only split off a separate pop-out hit area when there's room for
|
// 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
|
// both buttons. Below that, the whole row stays as a single
|
||||||
// selectable strip without the pop-out affordance.
|
// selectable strip without the pop-out affordance.
|
||||||
var hasPopOut = avail > PopOutHitWidth + 4f;
|
var hasPopOut = avail > PopOutHitWidth + 4f;
|
||||||
var tabHitWidth = hasPopOut ? avail - PopOutHitWidth : avail;
|
var tabHitWidth = hasPopOut ? avail - PopOutHitWidth : avail;
|
||||||
|
if (showGreeted)
|
||||||
|
{
|
||||||
|
// Greeted slot sits at the left edge (1.5.6 placement); the row
|
||||||
|
// button starts after it so the three hit areas never overlap.
|
||||||
|
tabHitWidth -= GreetedHitWidth;
|
||||||
|
ImGui.SetCursorScreenPos(origin + new Vector2(GreetedHitWidth, 0f));
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.InvisibleButton("row", new Vector2(tabHitWidth, RowHeight));
|
ImGui.InvisibleButton("row", new Vector2(tabHitWidth, RowHeight));
|
||||||
var rowHovered = ImGui.IsItemHovered();
|
var rowHovered = ImGui.IsItemHovered();
|
||||||
@@ -153,11 +198,25 @@ internal sealed class Sidebar
|
|||||||
);
|
);
|
||||||
|
|
||||||
var icon = ResolveTabIcon(tab);
|
var icon = ResolveTabIcon(tab);
|
||||||
|
|
||||||
|
// Dim precedence (1.5.6): the active tab always keeps its regular
|
||||||
|
// color; only greeted, non-active tabs drop to TextDim.
|
||||||
|
var isCurrentTab = tab == activeTab;
|
||||||
|
var iconColor = textAbgr;
|
||||||
|
if (
|
||||||
|
!isCurrentTab
|
||||||
|
&& greetedConfigured
|
||||||
|
&& Plugin.Instance.AutoTellTabsService.IsGreeted(tab)
|
||||||
|
)
|
||||||
|
iconColor = dimAbgr;
|
||||||
|
|
||||||
|
// Icon and label shift right by the greeted slot when it is shown.
|
||||||
|
var contentX = showGreeted ? GreetedHitWidth : 0f;
|
||||||
using (_fonts.FontAwesome.Push())
|
using (_fonts.FontAwesome.Push())
|
||||||
dl.AddText(origin + new Vector2(10f, 8f), textAbgr, icon.ToIconString());
|
dl.AddText(origin + new Vector2(10f + contentX, 8f), iconColor, icon.ToIconString());
|
||||||
|
|
||||||
if (expanded)
|
if (expanded)
|
||||||
dl.AddText(origin + new Vector2(32f, 8f), textAbgr, tab.Name);
|
dl.AddText(origin + new Vector2(32f + contentX, 8f), textAbgr, tab.Name);
|
||||||
|
|
||||||
TabContextMenu.Draw(tab, "ctx", _pool);
|
TabContextMenu.Draw(tab, "ctx", _pool);
|
||||||
|
|
||||||
@@ -180,6 +239,26 @@ internal sealed class Sidebar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showGreeted)
|
||||||
|
{
|
||||||
|
// The hit area sits at the LEFT edge of the row, but the item must
|
||||||
|
// be submitted AFTER TabContextMenu.Draw — any interactive item
|
||||||
|
// between the row button and the popup call would steal the
|
||||||
|
// right-click trigger (B3-1 ordering constraint).
|
||||||
|
ImGui.SetCursorScreenPos(origin);
|
||||||
|
ImGui.InvisibleButton("greeted", new Vector2(GreetedHitWidth, RowHeight));
|
||||||
|
if (ImGui.IsItemClicked())
|
||||||
|
ToggleGreetedForSelfTest(tab);
|
||||||
|
|
||||||
|
// CheckCircle = greeted, plain Check = still pending (1.5.6 mapping).
|
||||||
|
var greetedGlyph = Plugin.Instance.AutoTellTabsService.IsGreeted(tab)
|
||||||
|
? FontAwesomeIcon.CheckCircle
|
||||||
|
: FontAwesomeIcon.Check;
|
||||||
|
using (_fonts.FontAwesome.Push())
|
||||||
|
dl.AddText(origin + new Vector2(4f, 8f), mutedAbgr, greetedGlyph.ToIconString());
|
||||||
|
LastRenderedGreetedGlyphCount++;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.PopID();
|
ImGui.PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user