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 { // Scaled: MainWindow reserves body height against this, so it follows. public static float Height => StyleEngine.Metrics.HonorificHeight; // 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; private readonly TokenResolver _resolver; public HonorificHeader( HonorificService honorific, FontManager fonts, ThemeRegistry themes, TokenResolver resolver ) { _honorific = honorific; _fonts = fonts; _themes = themes; _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) { 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, StyleEngine.Metrics.CenterY(Height)), crownColor, crownGlyph ); } // 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 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 gap = StyleEngine.Metrics.HonorificBracketGap; var maxTitleWidth = maxWidth - crownWidth - gap - StyleEngine.Metrics.HonorificInset; if (maxTitleWidth > 0f) { var rendered = StringUtil.TruncateToFitWidth($"«{current.Title}»", maxTitleWidth); LastRenderedTitle = rendered; dl.AddText( origin + new Vector2(crownWidth + gap, StyleEngine.Metrics.CenterY(Height)), titleColor, rendered ); LastTitleRendered = true; } } // 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)); } }