diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs index a01764c..86d9495 100644 --- a/HellionChat/FontManager.cs +++ b/HellionChat/FontManager.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; using HellionChat.Themes; +using HellionChat.Ui.StyleEngine; namespace HellionChat; @@ -40,6 +41,20 @@ public sealed class FontManager : IDisposable internal IFontHandle? RegularFont; internal IFontHandle? ItalicFont; + // v1.13.0: one handle per type role that needs a face of its own. Sender + // carries extra weight, Meta a smaller size on a tiny glyph range. + internal IFontHandle? SenderFont; + internal IFontHandle? MetaFont; + + // The mockup asks for weight 600 on the sender. There is no bold face in the + // plugin and none in the bundled file, so the weight comes from a denser + // rasterisation of the same outline. 1.0 is the SafeFontConfig default; + // below ~1.2 the difference is not visible, above ~1.4 the glyphs smear. + // + // Not const: the smoke test compares three values side by side, and the + // widget gallery exposes it. + internal static float SenderWeight = 1.3f; + // Wired post-build (B4b-3); a Func keeps FontManager off the theme layer. private Func? _typographySource; @@ -56,7 +71,13 @@ public sealed class FontManager : IDisposable && AxisItalic.Available && FontAwesome.Available && RegularFont is { Available: true } - && (ItalicFont is null || ItalicFont.Available); + && (ItalicFont is null || ItalicFont.Available) + // Unconditional, unlike ItalicFont: these two are always built. A handle + // that is not ready yet makes SimplePushedFont push nothing at all -- + // silently -- so the first frame after a rebuild would measure the wrong + // face and write those heights into the row cache. + && SenderFont is { Available: true } + && MetaFont is { Available: true }; private ushort[] Ranges = []; private ushort[] JpRange = []; @@ -66,6 +87,20 @@ public sealed class FontManager : IDisposable // by the global font, so the fallback no longer re-merges the full Ranges array. private ushort[] CjkFallbackGlyphRange = []; + // The meta role draws clock faces, world names and a separator. FFXIV world + // names are Latin in every client, so ASCII plus the middle dot covers it. + // A full range here would rasterise the whole CJK set a second time for + // eighty glyphs' worth of use. Anything translated -- the header's stand-in + // when no world is known -- goes through the body face instead. + private static readonly ushort[] MetaRange = + [ + 0x0020, + 0x007E, + 0x00B7, + 0x00B7, + 0, + ]; + // Report accessor for the ctor self-test: built glyph-range array lengths so // the step can show the B1 dedup effect (a small trimmed fallback vs the large // primary range) in its on-disk report instead of a bare Pass. @@ -121,6 +156,9 @@ public sealed class FontManager : IDisposable if (Plugin.Config.ItalicEnabled) ItalicFont = BuildItalicFontHandle(atlas); + + SenderFont = BuildSenderFontHandle(atlas); + MetaFont = BuildMetaFontHandle(atlas); } // Source is still null here, so this is the config-only baseline. @@ -141,11 +179,23 @@ public sealed class FontManager : IDisposable var atlas = _pluginInterface.UiBuilder.FontAtlas; - RegularFont?.Dispose(); - RegularFont = BuildRegularFontHandle(atlas); + // Without the suppression each handle triggers its own atlas rebuild. + // With two handles that was tolerable; with four it is four rebuilds for + // one size change. + using (atlas.SuppressAutoRebuild()) + { + RegularFont?.Dispose(); + RegularFont = BuildRegularFontHandle(atlas); - ItalicFont?.Dispose(); - ItalicFont = Plugin.Config.ItalicEnabled ? BuildItalicFontHandle(atlas) : null; + ItalicFont?.Dispose(); + ItalicFont = Plugin.Config.ItalicEnabled ? BuildItalicFontHandle(atlas) : null; + + SenderFont?.Dispose(); + SenderFont = BuildSenderFontHandle(atlas); + + MetaFont?.Dispose(); + MetaFont = BuildMetaFontHandle(atlas); + } _lastBuiltFingerprint = EffectiveFontFingerprint(); } @@ -232,6 +282,54 @@ public sealed class FontManager : IDisposable }) ); + // Same outline as the body face, rasterised denser. Only works on the + // delegate path: with FontsEnabled and UseHellionFont both off the game's own + // Axis handle draws, and a game font handle has no such knob. The sender then + // leans on channel colour alone, which is a deliberate limitation. + private IFontHandle BuildSenderFontHandle(IFontAtlas atlas) => + atlas.NewDelegateFontHandle(e => + e.OnPreBuild(tk => + { + var basePt = TypeScale.SizePtOf( + TypeRole.Sender, + ResolveGlobalFontPt() + ); + var config = new SafeFontConfig + { + SizePt = basePt, + GlyphRanges = Ranges, + RasterizerMultiply = SenderWeight, + }; + var bundledBytes = Plugin.Config.UseHellionFont ? TryGetBundledFontBytes() : null; + config.MergeFont = bundledBytes is not null + ? tk.AddFontFromMemory(bundledBytes, config, "Inter-Light-Sender") + : AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "sender"); + + AddCjkAndSymbols(tk, config, basePt); + + tk.Font = config.MergeFont; + }) + ); + + private IFontHandle BuildMetaFontHandle(IFontAtlas atlas) => + atlas.NewDelegateFontHandle(e => + e.OnPreBuild(tk => + { + var basePt = TypeScale.SizePtOf( + TypeRole.Meta, + ResolveGlobalFontPt() + ); + var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = MetaRange }; + var bundledBytes = Plugin.Config.UseHellionFont ? TryGetBundledFontBytes() : null; + config.MergeFont = bundledBytes is not null + ? tk.AddFontFromMemory(bundledBytes, config, "Inter-Light-Meta") + : AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "meta"); + + // No CJK merge on purpose: MetaRange cannot reach those glyphs. + tk.Font = config.MergeFont; + }) + ); + private IFontHandle BuildItalicFontHandle(IFontAtlas atlas) => atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => @@ -262,6 +360,8 @@ public sealed class FontManager : IDisposable // lifetime, so the plugin must not dispose it. RegularFont?.Dispose(); ItalicFont?.Dispose(); + SenderFont?.Dispose(); + MetaFont?.Dispose(); } // Returns null when the embedded font resource is missing. Should not diff --git a/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs b/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs index c0e8f8c..f9a30fe 100644 --- a/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs +++ b/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs @@ -56,6 +56,20 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep return SelfTestStepResult.Fail; } + // v1.13.0: unconditional, unlike ItalicFont. Both are always built, and a + // handle that never arrives makes SimplePushedFont push nothing silently. + if (fm.SenderFont is null) + { + ImGui.Text("SenderFont handle is null"); + return SelfTestStepResult.Fail; + } + + if (fm.MetaFont is null) + { + ImGui.Text("MetaFont handle is null"); + return SelfTestStepResult.Fail; + } + if (fm.Axis.LoadException is { } e1) { ImGui.Text($"Axis load exception: {e1.Message}"); @@ -86,6 +100,18 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep return SelfTestStepResult.Fail; } + if (fm.SenderFont.LoadException is { } e6) + { + ImGui.Text($"SenderFont load exception: {e6.Message}"); + return SelfTestStepResult.Fail; + } + + if (fm.MetaFont.LoadException is { } e7) + { + ImGui.Text($"MetaFont load exception: {e7.Message}"); + return SelfTestStepResult.Fail; + } + // B1: assert the atlas actually finished building all required handles, // not just that the references are non-null. FontsReady is the observable // state the trimmed-fallback rebuild must still reach; a half-built atlas @@ -119,6 +145,8 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep $"FontAwesome available: {fm.FontAwesome.Available}", $"RegularFont available: {fm.RegularFont.Available}", $"ItalicFont: {italicState}", + $"SenderFont available: {fm.SenderFont.Available} (weight {FontManager.SenderWeight:0.00})", + $"MetaFont available: {fm.MetaFont.Available}", $"FontsReady: {fm.FontsReady}", $"Glyph-range entries: primary={counts.Ranges}, jp={counts.JpRange}, " + $"cjk-fallback={counts.CjkFallback} (B1 trimmed)", diff --git a/HellionChat/SelfTests/FontPushSmokeStep.cs b/HellionChat/SelfTests/FontPushSmokeStep.cs index 71e561d..b58706d 100644 --- a/HellionChat/SelfTests/FontPushSmokeStep.cs +++ b/HellionChat/SelfTests/FontPushSmokeStep.cs @@ -28,10 +28,18 @@ internal sealed class FontPushSmokeStep : ISelfTestStep return SelfTestStepResult.Fail; } + if (fm.SenderFont is null || fm.MetaFont is null) + { + ImGui.Text("SenderFont or MetaFont missing - see FontManager ctor smoke"); + return SelfTestStepResult.Fail; + } + try { using (fm.RegularFont.Push()) { } using (fm.FontAwesome.Push()) { } + using (fm.SenderFont.Push()) { } + using (fm.MetaFont.Push()) { } } catch (Exception e) {