diff --git a/HellionChat/Ui/Components/StatusBar.cs b/HellionChat/Ui/Components/StatusBar.cs index 4c561a8..b0eaeb2 100644 --- a/HellionChat/Ui/Components/StatusBar.cs +++ b/HellionChat/Ui/Components/StatusBar.cs @@ -3,8 +3,6 @@ using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; using HellionChat.Code; using HellionChat.Resources; using HellionChat.Themes; @@ -27,7 +25,7 @@ internal sealed class StatusBar // body height against this property, so the two drifting apart is the whole // failure mode. Slots are pills now, and a pill is taller than a text line. public static float Height => - StyleEngine.Widgets.Pill.CalcSize("X", withDot: false).Y + StyleEngine.Widgets.Pill.CalcSize(string.Empty, withDot: false).Y + StyleEngine.Metrics.StatusTopSpacer * 2f; private const long UpdateIntervalMs = 1000; @@ -38,6 +36,10 @@ internal sealed class StatusBar new StyleEngine.TokenResolver() ); + // Never changes at runtime; it used to be rebuilt on every frame. + private static readonly string VersionText = + $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion"; + private long _lastUpdateMs = -UpdateIntervalMs; private string _cachedCountsText = string.Empty; private string _cachedTellsText = string.Empty; @@ -154,34 +156,49 @@ internal sealed class StatusBar ? HellionStrings.StatusBar_Privacy_Enabled : HellionStrings.StatusBar_Privacy_Open; + // Every slot checks its own room. Only the right-hand one used to, so at + // 150% scaling with the window at its 480px minimum the counts and tells + // pills ran off the edge instead of dropping out. + var regionRight = origin.X + ImGui.GetContentRegionAvail().X; var x = origin.X; - x += DrawSlot(new Vector2(x, top), channelName, pillFill, pillText, dotAbgr) + gap; - x += - DrawSlot( - new Vector2(x, top), - privacyLabel, - pillFill, - pillText, - null, - (FontAwesomeIcon.Lock, _fonts.FontAwesome) - ) + gap; - x += DrawSlot(new Vector2(x, top), _cachedCountsText, pillFill, mutedText, null) + gap; - if (!string.IsNullOrEmpty(_cachedTellsText)) + bool Fits(string label, bool withDot, float iconWidth = 0f) => + x + StyleEngine.Widgets.Pill.CalcSize(label, withDot, iconWidth: iconWidth).X + <= regionRight; + + if (Fits(channelName, withDot: true)) + x += DrawSlot(new Vector2(x, top), channelName, pillFill, pillText, dotAbgr) + gap; + var lockWidth = StyleEngine.Widgets.Pill.MeasureIcon( + FontAwesomeIcon.Lock, + _fonts.FontAwesome + ); + if (Fits(privacyLabel, withDot: false, lockWidth)) + x += + DrawSlot( + new Vector2(x, top), + privacyLabel, + pillFill, + pillText, + null, + (FontAwesomeIcon.Lock, _fonts.FontAwesome) + ) + gap; + + if (Fits(_cachedCountsText, withDot: false)) + x += DrawSlot(new Vector2(x, top), _cachedCountsText, pillFill, mutedText, null) + gap; + + if (!string.IsNullOrEmpty(_cachedTellsText) && Fits(_cachedTellsText, withDot: false)) x += DrawSlot(new Vector2(x, top), _cachedTellsText, pillFill, pillText, null) + gap; // Slot 5: version + brand, right-aligned. Dropped when the left-hand run // would actually collide with it -- the old check compared against a flat // 200px and never measured the left slots at all. - var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion"; - var versionWidth = StyleEngine.Widgets.Pill.CalcSize(versionText, withDot: false).X; - var regionRight = origin.X + ImGui.GetContentRegionAvail().X; + var versionWidth = StyleEngine.Widgets.Pill.CalcSize(VersionText, withDot: false).X; var leftRunEnd = x - gap; if (regionRight - versionWidth - gap > leftRunEnd) DrawSlot( new Vector2(regionRight - versionWidth, top), - versionText, + VersionText, pillFill, mutedText, null diff --git a/HellionChat/Ui/StyleEngine/Metrics.cs b/HellionChat/Ui/StyleEngine/Metrics.cs index af4f4e5..37426d3 100644 --- a/HellionChat/Ui/StyleEngine/Metrics.cs +++ b/HellionChat/Ui/StyleEngine/Metrics.cs @@ -26,9 +26,6 @@ internal static class Metrics // --- Input bar --- internal const float InputBarHeightRaw = 32f; - internal const float InputPillHeightRaw = 22f; - internal const float InputPillPaddingXRaw = 8f; - internal const float InputPillRoundingRaw = 6f; internal const float InputQuickButtonsReserveRaw = 130f; // --- Honorific header --- @@ -44,8 +41,6 @@ internal static class Metrics // --- Status bar --- internal const float StatusBorderThicknessRaw = 1f; internal const float StatusTopSpacerRaw = 2f; - internal const float StatusMinOtherSlotsWidthRaw = 200f; - internal const float StatusDotRadiusRaw = 4f; // --- Message list --- internal const float MessageDummyWidthRaw = 10f; @@ -85,9 +80,6 @@ internal static class Metrics internal static float SidebarGlyphInset => MetricsMath.Scale(SidebarGlyphInsetRaw, Scale); internal static float InputBarHeight => MetricsMath.Scale(InputBarHeightRaw, Scale); - internal static float InputPillHeight => MetricsMath.Scale(InputPillHeightRaw, Scale); - internal static float InputPillPaddingX => MetricsMath.Scale(InputPillPaddingXRaw, Scale); - internal static float InputPillRounding => MetricsMath.Scale(InputPillRoundingRaw, Scale); internal static float InputQuickButtonsReserve => MetricsMath.Scale(InputQuickButtonsReserveRaw, Scale); @@ -106,9 +98,6 @@ internal static class Metrics internal static float StatusBorderThickness => MetricsMath.Scale(StatusBorderThicknessRaw, Scale); internal static float StatusTopSpacer => MathF.Round(StatusTopSpacerRaw * Scale); - internal static float StatusMinOtherSlotsWidth => - MetricsMath.Scale(StatusMinOtherSlotsWidthRaw, Scale); - internal static float StatusDotRadius => MetricsMath.Scale(StatusDotRadiusRaw, Scale); internal static float MessageDummyWidth => MetricsMath.Scale(MessageDummyWidthRaw, Scale); diff --git a/HellionChat/Ui/StyleEngine/Widgets/Pill.cs b/HellionChat/Ui/StyleEngine/Widgets/Pill.cs index f7cd511..772815a 100644 --- a/HellionChat/Ui/StyleEngine/Widgets/Pill.cs +++ b/HellionChat/Ui/StyleEngine/Widgets/Pill.cs @@ -12,6 +12,10 @@ internal readonly record struct PillStyle public float Height { get; init; } = 22f; public float PaddingX { get; init; } = 8f; + + // Only takes over once the text is taller than Height allows; at the default + // 12.75pt (17px line in a 22px pill) the floor still wins. + public float PaddingY { get; init; } = 2f; public float Rounding { get; init; } = 6f; public float DotRadius { get; init; } = 4f; public float DotGap { get; init; } = 6f; @@ -38,20 +42,39 @@ internal static class Pill return WidgetGeometry.Pill( ImGui.CalcTextSize(label), style.PaddingX * scale, + style.PaddingY * scale, style.Height * scale, lead ); } - // Measures a glyph in the icon font so a caller can feed iconWidth above - // without pushing the font twice. + // ToIconString allocates a fresh string on every call and holds no cache. + private static readonly Dictionary GlyphCache = []; + + private static string Glyph(FontAwesomeIcon icon) + { + if (GlyphCache.TryGetValue(icon, out var s)) + return s; + + s = icon.ToIconString(); + GlyphCache[icon] = s; + return s; + } + + // Measures a glyph in the icon font. A font push is not free -- it allocates + // a lock object and queues a deferred dispose -- so callers should let Draw + // return the size rather than measuring alongside it. internal static float MeasureIcon(FontAwesomeIcon icon, IFontHandle font) { using (font.Push()) - return ImGui.CalcTextSize(icon.ToIconString()).X; + return ImGui.CalcTextSize(Glyph(icon)).X; } - internal static void Draw( + // Returns the drawn size so a caller laying pills out in a row does not have + // to call CalcSize again -- doing so would repeat the text and icon + // measurement, and the two could drift apart if a style override is passed + // to only one of them. + internal static Vector2 Draw( Vector2 origin, string label, uint fillAbgr, @@ -87,7 +110,7 @@ internal static class Pill { using (glyph.Font.Push()) { - var text = glyph.Icon.ToIconString(); + var text = Glyph(glyph.Icon); var h = ImGui.CalcTextSize(text).Y; dl.AddText( new Vector2(textX, origin.Y + MetricsMath.CenterY(size.Y, h)), @@ -102,5 +125,6 @@ internal static class Pill // which display scaling does not feed into. var textY = origin.Y + MetricsMath.CenterY(size.Y, ImGui.GetTextLineHeight()); dl.AddText(new Vector2(textX, textY), textAbgr, label); + return size; } } diff --git a/HellionChat/Util/WidgetGeometry.cs b/HellionChat/Util/WidgetGeometry.cs index 7032714..1c0ec02 100644 --- a/HellionChat/Util/WidgetGeometry.cs +++ b/HellionChat/Util/WidgetGeometry.cs @@ -10,9 +10,19 @@ internal static class WidgetGeometry { private const float MinExtent = 1f; - internal static Vector2 Pill(Vector2 textSize, float paddingX, float height, float leadWidth) + // minHeight is a floor, not the answer: a pill whose height ignored the text + // would clip it as soon as the user picks a larger body font, which does not + // feed into display scaling. + internal static Vector2 Pill( + Vector2 textSize, + float paddingX, + float paddingY, + float minHeight, + float leadWidth + ) { var width = textSize.X + paddingX * 2f + leadWidth; + var height = MathF.Max(minHeight, textSize.Y + paddingY * 2f); return Clamp(new Vector2(width, height)); }