From fe766285ad32e762ff991a4cfe5cff4e858acb6f Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 15 Jun 2026 14:26:22 +0200 Subject: [PATCH] feat(honorific): wire title gate + colour + truncation, restore preview glyphs --- .../Integrations/HonorificTitleData.cs | 9 ++-- HellionChat/PluginHostFactory.cs | 3 +- HellionChat/Ui/Components/HonorificHeader.cs | 45 ++++++++++++++-- .../Ui/Components/HonorificTitleColor.cs | 21 ++++++++ .../Components/Settings/LivePreviewPanel.cs | 53 +++++++++++++------ HellionChat/Util/StringUtil.cs | 3 +- 6 files changed, 107 insertions(+), 27 deletions(-) create mode 100644 HellionChat/Ui/Components/HonorificTitleColor.cs diff --git a/HellionChat/Integrations/HonorificTitleData.cs b/HellionChat/Integrations/HonorificTitleData.cs index 267b7af..ccb2c9a 100644 --- a/HellionChat/Integrations/HonorificTitleData.cs +++ b/HellionChat/Integrations/HonorificTitleData.cs @@ -5,11 +5,10 @@ namespace HellionChat.Integrations; // Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll // so HellionChat loads cleanly when Honorific is absent. // -// Only Glow is rendered. Color3, GradientColourSet and GradientAnimationStyle -// are parsed but unused — the animated gradient lives entirely inside Honorific -// and is not exposed over IPC, so reproducing it here would mean shipping our -// own copy of Honorific's colour palette. The fields stay in the DTO so the -// JSON roundtrip remains lossless. +// Color is rendered in the header title slot (HonorificHeader). Glow, Color3, +// GradientColourSet and GradientAnimationStyle are parsed but not rendered — +// the animated gradient lives inside Honorific and is not exposed over IPC. +// The fields stay in the DTO so the JSON roundtrip remains lossless. internal sealed record HonorificTitleData( string? Title, bool IsPrefix, diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index 5fc04ec..b585a2c 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -166,7 +166,8 @@ internal static class PluginHostFactory )); services.AddSingleton(sp => new Ui.Components.Settings.LivePreviewPanel( sp.GetRequiredService(), - sp.GetRequiredService() + sp.GetRequiredService(), + sp.GetRequiredService() )); services.AddSingleton(sp => new Ui.Components.Settings.ThemeImportExportRow( sp.GetRequiredService(), diff --git a/HellionChat/Ui/Components/HonorificHeader.cs b/HellionChat/Ui/Components/HonorificHeader.cs index 2dad71f..d1e2ae3 100644 --- a/HellionChat/Ui/Components/HonorificHeader.cs +++ b/HellionChat/Ui/Components/HonorificHeader.cs @@ -15,6 +15,12 @@ internal sealed class HonorificHeader { public const float Height = 30f; + // SelfTest observables — set on the real Draw path so a headless step can + // assert the gate/colour/truncation outcome instead of re-implementing it. + internal bool LastTitleRendered { get; private set; } + internal uint LastTitleColorAbgr { get; private set; } + internal string? LastRenderedTitle { get; private set; } + private readonly HonorificService _honorific; private readonly FontManager _fonts; private readonly ThemeRegistry _themes; @@ -33,8 +39,15 @@ internal sealed class HonorificHeader _resolver = resolver; } + // Same singleton the AboutTab integrations section uses; lets a SelfTest + // drive the gate branches via HonorificService.TestOnly_SetState. + internal HonorificService GetServiceForSelfTest() => _honorific; + public void Draw(float maxWidth) { + LastTitleRendered = false; + LastRenderedTitle = null; + // First-frame guard: components must not lay out before the atlas // is finished or text metrics collapse into placeholder widths. if (!_fonts.FontsReady) @@ -58,11 +71,35 @@ internal sealed class HonorificHeader dl.AddText(origin + new Vector2(0f, 8f), crownColor, crownGlyph); } - var title = _honorific.IsAvailable ? _honorific.CurrentTitle?.Title : null; - if (!string.IsNullOrWhiteSpace(title)) + // Gate the bracketed title through the 1.5.6 contract (toggle, IPC + // availability, IsOriginal, empty-title) — the crown above stays + // unconditional as the permanent brand anchor. NOTE divergence from + // 1.5.6: there a failed gate hid the whole slot incl. crown; here the + // crown persists by design. + if ( + HonorificService.ShouldRenderSlot( + Plugin.Config.ShowHonorificTitleInHeader, + _honorific.IsAvailable, + _honorific.CurrentTitle + ) + ) { - var titleColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); - dl.AddText(origin + new Vector2(crownWidth + 6f, 8f), titleColor, $"«{title}»"); + var current = _honorific.CurrentTitle!; + var titleColor = HonorificTitleColor.ResolveTitleAbgr(current.Color, theme); + LastTitleColorAbgr = titleColor; + + // Budget the title against the row width. CalcTextSize inside + // TruncateToFitWidth measures the *Regular* font, so this must run + // OUTSIDE the FontAwesome.Push block above (crownWidth was measured + // inside it, which is correct). + var maxTitleWidth = maxWidth - crownWidth - 6f - 8f; + if (maxTitleWidth > 0f) + { + var rendered = StringUtil.TruncateToFitWidth($"«{current.Title}»", maxTitleWidth); + LastRenderedTitle = rendered; + dl.AddText(origin + new Vector2(crownWidth + 6f, 8f), titleColor, rendered); + LastTitleRendered = true; + } } // Reserve the row height even when no title rendered so the layout diff --git a/HellionChat/Ui/Components/HonorificTitleColor.cs b/HellionChat/Ui/Components/HonorificTitleColor.cs new file mode 100644 index 0000000..850e1db --- /dev/null +++ b/HellionChat/Ui/Components/HonorificTitleColor.cs @@ -0,0 +1,21 @@ +using System.Numerics; +using HellionChat.Themes; +using HellionChat.Util; + +namespace HellionChat.Ui.Components; + +// Resolves the bracketed-title colour for the Honorific header, shared by the +// real header (HonorificHeader) and the settings theme preview (LivePreviewPanel) +// so the fallback never drifts between them. A title colour supplied by Honorific +// (0..1 normalised RGB over IPC) renders as-is; absent colour falls back to the +// theme's primary text. The Vector4ToRgba path clamps each component to [0,1] so +// an out-of-range value from the JSON IPC payload cannot wrap the byte cast. +internal static class HonorificTitleColor +{ + internal static uint ResolveTitleAbgr(Vector3? color, Theme theme) + { + return color is { } c + ? ColourUtil.RgbaToAbgr(ColourUtil.Vector4ToRgba(new Vector4(c, 1f))) + : ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); + } +} diff --git a/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs b/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs index ea99560..c058133 100644 --- a/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs +++ b/HellionChat/Ui/Components/Settings/LivePreviewPanel.cs @@ -1,6 +1,7 @@ using System.Numerics; using System.Threading; using Dalamud.Bindings.ImGui; +using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using HellionChat.Themes; using HellionChat.Ui.StyleEngine; @@ -21,21 +22,21 @@ internal sealed class LivePreviewPanel : IDisposable private const string MockTell = "Tell → Player: Hey, want to party?"; private const string MockFc = "FC: Welcome aboard."; - // FontAwesome is intentionally not pulled in — crown/cog render as Unicode - // glyphs in the default font so this panel stays DI-light (Step 2 scope). - private const string CrownGlyph = "♛"; - private const string CogGlyph = "⚙"; + // Crown/cog render via the FontAwesome font (FontManager) so the preview + // matches the real header glyphs; the bundled text font has no crown glyph. private const float MiddleBandHeight = 220f; private const float SidebarWidth = 70f; private readonly ThemeRegistry _themes; private readonly TokenResolver _resolver; + private readonly FontManager _fonts; - public LivePreviewPanel(ThemeRegistry themes, TokenResolver resolver) + public LivePreviewPanel(ThemeRegistry themes, TokenResolver resolver, FontManager fonts) { _themes = themes; _resolver = resolver; + _fonts = fonts; _themes.OnEditingBufferChanged += OnBufferChanged; Interlocked.Increment(ref InstanceCount); } @@ -124,7 +125,7 @@ internal sealed class LivePreviewPanel : IDisposable ImGui.Dummy(new Vector2(width, height)); } - private static void DrawHonorificHeader(Theme theme) + private void DrawHonorificHeader(Theme theme) { const float height = 32f; var draw = ImGui.GetWindowDrawList(); @@ -139,15 +140,30 @@ internal sealed class LivePreviewPanel : IDisposable ); var crownAbgr = ColourUtil.RgbaToAbgr(theme.Colors.Identity); - var textAbgr = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary); + // Shared fallback path with the real header (Weiche 3). The mock has no + // Honorific colour, so this resolves to TextPrimary today — visually + // unchanged — but both paths now share one resolver. No truncation here: + // the preview draws a fixed, centred "«Champion» Preview" string. + var textAbgr = HonorificTitleColor.ResolveTitleAbgr(null, theme); var title = "«Champion» Preview"; - var crownSize = ImGui.CalcTextSize(CrownGlyph); + var crownGlyph = FontAwesomeIcon.Crown.ToIconString(); + + // Crown is a FontAwesome glyph (matches the real header); measure + draw + // it inside the FontAwesome push, the title stays in the default font. + float crownWidth; + using (_fonts.FontAwesome.Push()) + { + crownWidth = ImGui.CalcTextSize(crownGlyph).X; + } var titleSize = ImGui.CalcTextSize(title); - var totalWidth = crownSize.X + 4f + titleSize.X; + var totalWidth = crownWidth + 4f + titleSize.X; var startX = origin.X + (width - totalWidth) * 0.5f; var y = origin.Y + (height - titleSize.Y) * 0.5f; - draw.AddText(new Vector2(startX, y), crownAbgr, CrownGlyph); - draw.AddText(new Vector2(startX + crownSize.X + 4f, y), textAbgr, title); + using (_fonts.FontAwesome.Push()) + { + draw.AddText(new Vector2(startX, y), crownAbgr, crownGlyph); + } + draw.AddText(new Vector2(startX + crownWidth + 4f, y), textAbgr, title); ImGui.Dummy(new Vector2(width, height)); } @@ -240,7 +256,7 @@ internal sealed class LivePreviewPanel : IDisposable ImGui.Dummy(new Vector2(totalWidth, MiddleBandHeight)); } - private static void DrawInputBar(Theme theme) + private void DrawInputBar(Theme theme) { const float height = 24f; const float pillWidth = 50f; @@ -268,9 +284,16 @@ internal sealed class LivePreviewPanel : IDisposable var phPos = new Vector2(pillMax.X + 6f, origin.Y + (height - phSize.Y) * 0.5f); draw.AddText(phPos, ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary), placeholder); - var cogSize = ImGui.CalcTextSize(CogGlyph); - var cogPos = new Vector2(max.X - cogSize.X - 6f, origin.Y + (height - cogSize.Y) * 0.5f); - draw.AddText(cogPos, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted), CogGlyph); + var cogGlyph = FontAwesomeIcon.Cog.ToIconString(); + using (_fonts.FontAwesome.Push()) + { + var cogSize = ImGui.CalcTextSize(cogGlyph); + var cogPos = new Vector2( + max.X - cogSize.X - 6f, + origin.Y + (height - cogSize.Y) * 0.5f + ); + draw.AddText(cogPos, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted), cogGlyph); + } ImGui.Dummy(new Vector2(width, height)); } diff --git a/HellionChat/Util/StringUtil.cs b/HellionChat/Util/StringUtil.cs index ccdcc98..efb593b 100755 --- a/HellionChat/Util/StringUtil.cs +++ b/HellionChat/Util/StringUtil.cs @@ -34,8 +34,7 @@ internal static class StringUtil // Returns the text unchanged when it already fits the width budget, // otherwise the longest prefix plus a horizontal-ellipsis character that - // still fits. Used by the chat header Honorific title slot and reused by - // the chat-line truncation path in later cycles. + // still fits. Used by the HonorificHeader title slot (HonorificHeader.Draw). public static string TruncateToFitWidth(string text, float maxWidth) { if (ImGui.CalcTextSize(text).X <= maxWidth)