feat(honorific): wire title gate + colour + truncation, restore preview glyphs
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -166,7 +166,8 @@ internal static class PluginHostFactory
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.LivePreviewPanel(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
|
||||
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>(),
|
||||
sp.GetRequiredService<FontManager>()
|
||||
));
|
||||
services.AddSingleton(sp => new Ui.Components.Settings.ThemeImportExportRow(
|
||||
sp.GetRequiredService<ThemeRegistry>(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user