feat(style): add a scale-aware metrics layer

Layout constants lived as bare floats in the components: row heights, hit
widths, insets, reserve widths. None of them multiplied by GlobalScale, so at
125% or 150% display scaling the text grows and the boxes do not. The quick
buttons stop fitting into their 130px column.

Metrics holds the design values and exposes scaled properties. The Raw
constants stay reachable for the few places that must store or compare an
unscaled value: Sidebar.GetWidth (a SelfTest pins it against raw numbers) and
the width slider bounds.

Scale is pinned once per frame against ImGui.GetFrameCount(). GlobalScale is a
live value the Dalamud slider moves on every dragged frame, so reading it per
access can shift a CalcSize away from its matching Draw inside one frame, and
costs three native calls each time.

Not in ThemeLayout: that record is serialised into theme JSON, and layout
customisation is out of scope per the master spec.

MetricsMath is the pure half so the build suite can pin the arithmetic. Text
heights are measured rather than scaled -- the font is built from
Config.FontSizeV2, which GlobalScale does not feed into.
This commit is contained in:
2026-08-17 23:34:08 +02:00
parent 0ef33934a2
commit 28b97b3f70
2 changed files with 125 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using HellionChat.Util;
namespace HellionChat.Ui.StyleEngine;
// Layout values, authored at 100% display scaling. Draw code reads the scaled
// properties; the Raw constants exist only for the few places that must store or
// compare an unscaled value (Sidebar.GetWidth, the width slider bounds).
//
// Deliberately NOT part of ThemeLayout: that record is serialised into theme
// JSON, and layout customisation is out of scope per the master spec.
internal static class Metrics
{
// --- Sidebar ---
internal const float SidebarIconOnlyWidthRaw = 38f;
internal const float SidebarMinWidthRaw = 40f;
internal const float SidebarMaxWidthRaw = 300f;
internal const float SidebarRowHeightRaw = 32f;
internal const float SidebarPopOutHitWidthRaw = 22f;
internal const float SidebarGreetedHitWidthRaw = 22f;
internal const float SidebarHitSlackRaw = 4f;
internal const float SidebarMinDrawWidthRaw = 2f;
internal const float SidebarUnreadRadiusRaw = 4f;
internal const float SidebarGlyphInsetRaw = 4f;
// --- 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 ---
internal const float HonorificHeightRaw = 30f;
internal const float HonorificInsetRaw = 8f;
internal const float HonorificBracketGapRaw = 6f;
// --- Top tab bar ---
internal const float TopTabUnreadRadiusRaw = 3.5f;
internal const float TopTabUnreadInsetRaw = 4f;
// --- 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;
private static float _cachedScale = 1f;
private static int _cachedFrame = -1;
// GlobalScale resolves to ImGui.GetIO().FontGlobalScale, which the Dalamud
// settings slider moves on every frame while it is dragged. Pinning it once
// per frame keeps a CalcSize and its matching Draw on the same value, and
// saves three native calls per access.
internal static float Scale
{
get
{
var frame = ImGui.GetFrameCount();
if (frame == _cachedFrame)
return _cachedScale;
_cachedFrame = frame;
_cachedScale = ImGuiHelpers.GlobalScale;
return _cachedScale;
}
}
internal static float SidebarIconOnlyWidth => MetricsMath.Scale(SidebarIconOnlyWidthRaw, Scale);
internal static float SidebarRowHeight => MetricsMath.Scale(SidebarRowHeightRaw, Scale);
internal static float SidebarPopOutHitWidth =>
MetricsMath.Scale(SidebarPopOutHitWidthRaw, Scale);
internal static float SidebarGreetedHitWidth =>
MetricsMath.Scale(SidebarGreetedHitWidthRaw, Scale);
internal static float SidebarHitSlack => MetricsMath.Scale(SidebarHitSlackRaw, Scale);
internal static float SidebarMinDrawWidth => MetricsMath.Scale(SidebarMinDrawWidthRaw, Scale);
internal static float SidebarUnreadRadius => MetricsMath.Scale(SidebarUnreadRadiusRaw, Scale);
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);
internal static float HonorificHeight => MetricsMath.Scale(HonorificHeightRaw, Scale);
internal static float HonorificInset => MetricsMath.Scale(HonorificInsetRaw, Scale);
internal static float HonorificBracketGap => MetricsMath.Scale(HonorificBracketGapRaw, Scale);
internal static float TopTabUnreadRadius => MetricsMath.Scale(TopTabUnreadRadiusRaw, Scale);
internal static float TopTabUnreadInset => MetricsMath.Scale(TopTabUnreadInsetRaw, Scale);
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);
// Text-dependent heights are measured, never scaled: the font is built from
// Config.FontSizeV2, which GlobalScale does not feed into. StatusBar.Height
// has done it this way since 1.8.x.
internal static float CenterY(float rowHeight) =>
MetricsMath.CenterY(rowHeight, ImGui.GetTextLineHeight());
}
+13
View File
@@ -0,0 +1,13 @@
namespace HellionChat.Util;
// Pure scaling arithmetic, split out from Metrics so the build suite can pin it
// without standing up an ImGui frame.
internal static class MetricsMath
{
internal static float Scale(float raw, float scale) => raw * scale;
// Vertical centering for text inside a fixed-height row. Frozen offsets keep
// their mis-centering at every other font size; a computed one does not.
internal static float CenterY(float rowHeight, float textHeight) =>
(rowHeight - textHeight) * 0.5f;
}