fix(layout): four axes that could always stale the cache, and the new roles

The height cache keys on a fingerprint of everything that can change a row's
height. Four things that can were never in it:

- ItalicFontV2.SizePt. ChunkRenderer pushes the italic face mid-row for emphasis,
  so there have been mixed sizes in a single line all along -- nobody called it
  that. Changing only the italic size moved every wrapped row and left the cache
  untouched.
- ItalicEnabled, which swaps between ItalicFont at its own size and AxisItalic at
  the base size.
- FontsEnabled and UseHellionFont, which swap the face outright. This pair is the
  quiet one: both size fields default to 12.75f, so the fingerprint did not move
  at all while the glyph widths underneath it did.

On top of those, the two new role sizes. They follow the base arithmetically, but
the resolved value is what belongs in the fingerprint -- a theme override moves
the base without moving any factor.

The three toggles go in the discrete half so they bypass the settle window, the
same way density already does. The sizes are sliders and wait it out.

Falsified rather than assumed: dropping FontsEnabled back out of Discrete turns
the new test red, so it is measuring the axis and not just passing.
This commit is contained in:
2026-08-19 09:48:35 +02:00
parent 34e343d8f2
commit 147034bda5
3 changed files with 39 additions and 7 deletions
+16 -3
View File
@@ -59,7 +59,7 @@ public sealed class FontManager : IDisposable
private Func<ThemeTypography?>? _typographySource; private Func<ThemeTypography?>? _typographySource;
// Lets RebuildDelegateFontsIfChanged skip rebuilds when the size is unchanged. // Lets RebuildDelegateFontsIfChanged skip rebuilds when the size is unchanged.
private (float Global, float Symbols) _lastBuiltFingerprint; private (float Global, float Symbols, float Sender, float Meta, float Italic) _lastBuiltFingerprint;
// True once every required atlas-owned handle reports Available. Components // True once every required atlas-owned handle reports Available. Components
// gate their first-frame draw on this — without it the layout math would // gate their first-frame draw on this — without it the layout math would
@@ -216,8 +216,21 @@ public sealed class FontManager : IDisposable
Plugin.Config.SymbolsFontSizeV2 Plugin.Config.SymbolsFontSizeV2
); );
internal (float Global, float Symbols) EffectiveFontFingerprint() => // Every size that can land in one message row. Roles follow the base size
(ResolveGlobalFontPt(), ResolveSymbolsFontPt()); // arithmetically, but the resolved value is what the height cache has to key
// on -- a theme override moves the base without moving any factor.
internal (float Global, float Symbols, float Sender, float Meta, float Italic)
EffectiveFontFingerprint()
{
var basePt = ResolveGlobalFontPt();
return (
basePt,
ResolveSymbolsFontPt(),
TypeScale.SizePtOf(TypeRole.Sender, basePt),
TypeScale.SizePtOf(TypeRole.Meta, basePt),
Plugin.Config.ItalicFontV2.SizePt
);
}
// Rebuilds only when the effective size changed (live fingerprint, TOCTOU-free). // Rebuilds only when the effective size changed (live fingerprint, TOCTOU-free).
// The atlas rebuild must run on the framework/draw thread — callers ensure that. // The atlas rebuild must run on the framework/draw thread — callers ensure that.
+15 -3
View File
@@ -93,13 +93,25 @@ internal sealed class MessageList
// Width is passed in (ContentRegionAvail is only valid inside the draw child); // Width is passed in (ContentRegionAvail is only valid inside the draw child);
// enum modes widened to int so the record stays comparable. UiScale is in here // enum modes widened to int so the record stays comparable. UiScale is in here
// because it feeds CalcWordWrapPositionA -- a scale change rewraps every row. // because it feeds CalcWordWrapPositionA -- a scale change rewraps every row.
//
// v1.13.0 added four axes that could always stale this cache and never did:
// the italic size (pushed mid-row by ChunkRenderer), ItalicEnabled (which
// swaps between two differently sized faces), FontsEnabled and UseHellionFont
// (both swap the face outright, and their two size fields default to the same
// 12.75f -- so the fingerprint did not move while the glyph widths did).
private LayoutFingerprint BuildLayoutFingerprint(float contentWidth) private LayoutFingerprint BuildLayoutFingerprint(float contentWidth)
{ {
var (global, symbols) = _fonts.EffectiveFontFingerprint(); var fonts = _fonts.EffectiveFontFingerprint();
return new LayoutFingerprint( return new LayoutFingerprint(
global, fonts.Global,
symbols, fonts.Symbols,
fonts.Sender,
fonts.Meta,
fonts.Italic,
Plugin.Config.UseCompactDensity, Plugin.Config.UseCompactDensity,
Plugin.Config.FontsEnabled,
Plugin.Config.UseHellionFont,
Plugin.Config.ItalicEnabled,
(int)Plugin.Config.NameFormMode, (int)Plugin.Config.NameFormMode,
(int)Plugin.Config.WorldSuffixMode, (int)Plugin.Config.WorldSuffixMode,
contentWidth, contentWidth,
+8 -1
View File
@@ -5,7 +5,13 @@ namespace HellionChat.Util;
internal readonly record struct LayoutFingerprint( internal readonly record struct LayoutFingerprint(
float FontGlobal, float FontGlobal,
float FontSymbols, float FontSymbols,
float FontSender,
float FontMeta,
float FontItalic,
bool Compact, bool Compact,
bool FontsEnabled,
bool UseHellionFont,
bool ItalicEnabled,
int NameForm, int NameForm,
int WorldSuffix, int WorldSuffix,
float Width, float Width,
@@ -15,7 +21,8 @@ internal readonly record struct LayoutFingerprint(
// Toggles: they land on a new value in one frame and stay there. Waiting on // Toggles: they land on a new value in one frame and stay there. Waiting on
// them would leave the planner running against the previous density's // them would leave the planner running against the previous density's
// heights while the rows are already painted the new way. // heights while the rows are already painted the new way.
internal (bool, int, int) Discrete => (Compact, NameForm, WorldSuffix); internal (bool, bool, bool, bool, int, int) Discrete =>
(Compact, FontsEnabled, UseHellionFont, ItalicEnabled, NameForm, WorldSuffix);
} }
// Dragging a window edge or the Dalamud UI-scale slider moves the continuous // Dragging a window edge or the Dalamud UI-scale slider moves the continuous