Files
HellionChat/HellionChat/FontManager.cs
T

212 lines
8.8 KiB
C#

using Dalamud;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Bindings.ImGui;
namespace HellionChat;
public class FontManager
{
internal IFontHandle Axis = null!;
internal IFontHandle AxisItalic = null!;
internal IFontHandle RegularFont = null!;
internal IFontHandle? ItalicFont;
internal IFontHandle FontAwesome = null!;
private ushort[] Ranges = [];
private ushort[] JpRange = [];
public static readonly HashSet<float> AxisFontSizeList =
[
9.6f, 10f, 12f, 14f, 16f,
18f, 18.4f, 20f, 23f, 34f,
36f, 40f, 45f, 46f, 68f, 90f,
];
/// <summary>
/// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
/// extracted from the assembly's manifest resources on first use; the
/// load happens inside the font atlas build callback so we keep the
/// allocation off the plugin constructor's hot path.
/// </summary>
private static byte[]? HellionFontBytes;
private static byte[] GetHellionFontBytes()
{
if (HellionFontBytes is not null)
return HellionFontBytes;
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream("HellionFont.ttf")
?? throw new FileNotFoundException("Hellion font resource not embedded in the assembly");
using var ms = new MemoryStream();
stream.CopyTo(ms);
HellionFontBytes = ms.ToArray();
return HellionFontBytes;
}
private unsafe void SetUpRanges()
{
ushort[] BuildRange(IReadOnlyList<ushort>? chars, params nint[] ranges)
{
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
// text
foreach (var range in ranges)
builder.AddRanges((ushort*)range);
// chars
if (chars != null)
{
for (var i = 0; i < chars.Count; i += 2)
{
if (chars[i] == 0)
break;
for (var j = (uint) chars[i]; j <= chars[i + 1]; j++)
builder.AddChar((ushort) j);
}
}
// Ingame supported ranges
var reader = new FdtReader(Plugin.DataManager.GetFile("common/font/axis_12.fdt")!.Data);
foreach (var c in reader.Glyphs)
builder.AddChar(c.Char);
// various symbols
// French
// Romanian
// builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─\~");
builder.AddText("Œœ");
builder.AddText("ĂăÂâÎîȘșȚț");
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
for (var i = 0x2460; i <= 0x24B5; i++)
builder.AddChar((char) i);
builder.AddChar('⓪');
return builder.BuildRangesToArray();
}
var ranges = new List<nint> { (nint)ImGui.GetIO().Fonts.GetGlyphRangesDefault() };
foreach (var extraRange in Enum.GetValues<ExtraGlyphRanges>())
if (Plugin.Config.ExtraGlyphRanges.HasFlag(extraRange))
ranges.Add(extraRange.Range());
Ranges = BuildRange(null, ranges.ToArray());
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
}
/// <summary>
/// Async wrapper around <see cref="BuildFonts"/> for the Phase-1 LoadAsync
/// path. The font-atlas build is CPU-bound, so we offload via Task.Run and
/// honour the cancellation token at the scheduling boundary; this lets the
/// font build run in parallel with the theme init without blocking the
/// loader. Settings-driven manual rebuilds keep using the sync entry point.
/// </summary>
public async Task BuildFontsAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Run(BuildFonts, cancellationToken).ConfigureAwait(false);
}
public void BuildFonts()
{
SetUpRanges();
Axis = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2)));
AxisItalic = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
{
SkewStrength = SizeInPx(Plugin.Config.FontSizeV2) / 6
});
FontAwesome = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
{
e.OnPreBuild(tk => tk.AddFontAwesomeIconFont(new SafeFontConfig { SizePx = GetFontSize() }));
e.OnPostBuild(tk => tk.FitRatio(tk.Font));
});
RegularFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk =>
{
// v1.2.0 — Bei aktiver Hellion-Schrift (Exo 2 ist Variable-Font)
// wird die User-Schriftgröße aus FontSizeV2 als SizePt angewendet.
// Der Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem
// Custom-Font-Stack. Ohne diese Verzweigung war FontSizeV2 bei
// UseHellionFont=true wirkungslos, was 4K-User mit größerer
// Skalierung blockierte (Settings → Erscheinungsbild → Schriftarten).
var basePt = Plugin.Config.UseHellionFont
? Plugin.Config.FontSizeV2
: Plugin.Config.GlobalFontV2.SizePt;
var config = new SafeFontConfig {SizePt = basePt, GlyphRanges = Ranges};
config.MergeFont = Plugin.Config.UseHellionFont
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
config.GlyphRanges = JpRange;
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
tk.AddGameSymbol(config);
tk.Font = config.MergeFont;
}
));
if (Plugin.Config.ItalicEnabled)
{
ItalicFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk =>
{
var config = new SafeFontConfig {SizePt = Plugin.Config.ItalicFontV2.SizePt, GlyphRanges = Ranges};
config.MergeFont = AddFontWithFallback(tk, Plugin.Config.ItalicFontV2.FontId, config, "italic");
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
config.GlyphRanges = JpRange;
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
tk.AddGameSymbol(config);
tk.Font = config.MergeFont;
}
));
}
else
{
ItalicFont = null;
}
}
/// <summary>
/// Try to add a user-configured font to the build toolkit, falling back to
/// the bundled NotoSansCjkRegular asset if the configured font isn't
/// available on the system. Without this guard a stale SystemFontId
/// pointing at a font the user uninstalled or that never existed on
/// Linux (e.g. "Crimson Text") tears down the entire font atlas build.
/// </summary>
private static ImFontPtr AddFontWithFallback(IFontAtlasBuildToolkitPreBuild tk, IFontId fontId, SafeFontConfig config, string slot)
{
try
{
return fontId.AddToBuildToolkit(tk, config);
}
catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException or IOException)
{
Plugin.Log.Warning(e, $"Configured {slot} font unavailable, falling back to NotoSansCjkRegular");
var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
return fallback.AddToBuildToolkit(tk, config);
}
}
public static float SizeInPt(float px) => (float) (px * 3.0 / 4.0);
public static float SizeInPx(float pt) => (float) (pt * 4.0 / 3.0);
public static float GetFontSize() => Plugin.Config.FontsEnabled ? Plugin.Config.GlobalFontV2.SizePx : SizeInPx(Plugin.Config.FontSizeV2);
}