diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index c26c306..eb932a3 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -402,6 +402,7 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.TellPillTransparencyStep(this), new SelfTests.TabRenamePersistStep(this), new SelfTests.NotificationSoundSelectStep(), + new SelfTests.SidebarGreetedGlyphStep(this), ]); // Re-surface the wizard for existing users when a major UX diff --git a/HellionChat/SelfTests/SidebarGreetedGlyphStep.cs b/HellionChat/SelfTests/SidebarGreetedGlyphStep.cs new file mode 100644 index 0000000..3687ee6 --- /dev/null +++ b/HellionChat/SelfTests/SidebarGreetedGlyphStep.cs @@ -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.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() { } +} diff --git a/HellionChat/Ui/Components/Sidebar.cs b/HellionChat/Ui/Components/Sidebar.cs index 9de2109..b02ca2f 100644 --- a/HellionChat/Ui/Components/Sidebar.cs +++ b/HellionChat/Ui/Components/Sidebar.cs @@ -28,6 +28,12 @@ internal sealed class Sidebar private const float RowHeight = 32f; 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 // 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) : 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 tabs, ref Tab? activeTab) { + LastRenderedGreetedGlyphCount = 0; + if (!_fonts.FontsReady) { ImGui.Dummy(new Vector2(IconOnlyWidth, 0)); @@ -99,10 +117,21 @@ internal sealed class Sidebar var accentRgba = _resolver.Resolve(Token.AccentPrimary, theme.Colors); var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); var mutedAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted); + var dimAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextDim); var dl = ImGui.GetWindowDrawList(); 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( @@ -112,6 +141,7 @@ internal sealed class Sidebar uint accentRgba, uint textAbgr, uint mutedAbgr, + uint dimAbgr, ImDrawListPtr dl, ref Tab? activeTab ) @@ -130,11 +160,26 @@ internal sealed class Sidebar 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 // both buttons. Below that, the whole row stays as a single // selectable strip without the pop-out affordance. var hasPopOut = avail > PopOutHitWidth + 4f; 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)); var rowHovered = ImGui.IsItemHovered(); @@ -153,11 +198,25 @@ internal sealed class Sidebar ); 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()) - dl.AddText(origin + new Vector2(10f, 8f), textAbgr, icon.ToIconString()); + dl.AddText(origin + new Vector2(10f + contentX, 8f), iconColor, icon.ToIconString()); 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); @@ -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(); }