feat(ui): add HonorificHeader component

30px header row pinned to the top of the chat window. Crown always renders
as a brand anchor — even with the Honorific IPC down — while the bracketed
title only appears when CurrentTitle has content. First-frame guard reads
FontManager.FontsReady so layout math never runs against placeholder font
metrics.
This commit is contained in:
2026-05-23 18:17:33 +02:00
parent 9c490fa066
commit 0e0c563608
2 changed files with 79 additions and 0 deletions
+7
View File
@@ -116,6 +116,13 @@ internal static class PluginHostFactory
sp.GetRequiredService<IChatGui>(), sp.GetRequiredService<IChatGui>(),
sp.GetRequiredService<ILogger<Services.TellRouterService>>() sp.GetRequiredService<ILogger<Services.TellRouterService>>()
)); ));
services.AddSingleton(sp => new Ui.Components.HonorificHeader(
sp.GetRequiredService<Integrations.HonorificService>(),
sp.GetRequiredService<FontManager>(),
sp.GetRequiredService<ThemeRegistry>(),
sp.GetRequiredService<Ui.StyleEngine.TokenResolver>()
));
services.AddSingleton(sp => new Integrations.FailedTellNotifier( services.AddSingleton(sp => new Integrations.FailedTellNotifier(
sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>() sp.GetRequiredService<ILogger<Integrations.FailedTellNotifier>>()
)); ));
@@ -0,0 +1,72 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using HellionChat.Integrations;
using HellionChat.Themes;
using HellionChat.Ui.StyleEngine;
using HellionChat.Util;
namespace HellionChat.Ui.Components;
// 30px header row pinned to the top of the main chat window. Crown stays
// rendered as a brand anchor even when the Honorific IPC drops out; the
// bracketed title only appears when there is actually a title to show.
internal sealed class HonorificHeader
{
public const float Height = 30f;
private readonly HonorificService _honorific;
private readonly FontManager _fonts;
private readonly ThemeRegistry _themes;
private readonly TokenResolver _resolver;
public HonorificHeader(
HonorificService honorific,
FontManager fonts,
ThemeRegistry themes,
TokenResolver resolver
)
{
_honorific = honorific;
_fonts = fonts;
_themes = themes;
_resolver = resolver;
}
public void Draw(float maxWidth)
{
// First-frame guard: components must not lay out before the atlas
// is finished or text metrics collapse into placeholder widths.
if (!_fonts.FontsReady)
{
ImGui.TextUnformatted("Loading fonts…");
return;
}
var theme = _themes.Active;
var origin = ImGui.GetCursorScreenPos();
var dl = ImGui.GetWindowDrawList();
var crownColor = ColourUtil.RgbaToAbgr(
_resolver.Resolve(Token.HonorificCrown, theme.Colors)
);
var crownGlyph = FontAwesomeIcon.Crown.ToIconString();
float crownWidth;
using (_fonts.FontAwesome.Push())
{
crownWidth = ImGui.CalcTextSize(crownGlyph).X;
dl.AddText(origin + new Vector2(0f, 8f), crownColor, crownGlyph);
}
var title = _honorific.IsAvailable ? _honorific.CurrentTitle?.Title : null;
if (!string.IsNullOrWhiteSpace(title))
{
var titleColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
dl.AddText(origin + new Vector2(crownWidth + 6f, 8f), titleColor, $"«{title}»");
}
// Reserve the row height even when no title rendered so the layout
// below stays stable across IPC reconnect cycles.
ImGui.Dummy(new Vector2(maxWidth, Height));
}
}