Revert "IFontAtlas: font atlas per plugin"

This commit is contained in:
goat 2024-01-18 21:37:05 +01:00 committed by KazWolfe
parent 14c5ad1605
commit b5696afe94
44 changed files with 1499 additions and 7943 deletions

View file

@ -148,9 +148,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
public bool UseAxisFontsFromGame { get; set; } = false;
/// <summary>
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
/// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness.
///
/// Before gamma is applied...
/// * ...TTF fonts loaded with stb or FreeType are in linear space.
/// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4.
/// </summary>
[Obsolete("It happens that nobody touched this setting", true)]
public float FontGammaLevel { get; set; } = 1.4f;
/// <summary>

View file

@ -1,159 +0,0 @@
using System.Collections.Generic;
using System.IO;
namespace Dalamud.Interface.GameFonts;
/// <summary>
/// Reference member view of a .fdt file data.
/// </summary>
internal readonly unsafe struct FdtFileView
{
private readonly byte* ptr;
/// <summary>
/// Initializes a new instance of the <see cref="FdtFileView"/> struct.
/// </summary>
/// <param name="ptr">Pointer to the data.</param>
/// <param name="length">Length of the data.</param>
public FdtFileView(void* ptr, int length)
{
this.ptr = (byte*)ptr;
if (length < sizeof(FdtReader.FdtHeader))
throw new InvalidDataException("Not enough space for a FdtHeader");
if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader))
throw new InvalidDataException("Not enough space for a FontTableHeader");
if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) +
(sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount))
throw new InvalidDataException("Not enough space for all the FontTableEntry");
if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader))
throw new InvalidDataException("Not enough space for a KerningTableHeader");
if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) +
(sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount))
throw new InvalidDataException("Not enough space for all the KerningTableEntry");
}
/// <summary>
/// Gets the file header.
/// </summary>
public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr;
/// <summary>
/// Gets the font header.
/// </summary>
public ref FdtReader.FontTableHeader FontHeader =>
ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset);
/// <summary>
/// Gets the glyphs.
/// </summary>
public Span<FdtReader.FontTableEntry> Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount);
/// <summary>
/// Gets the kerning header.
/// </summary>
public ref FdtReader.KerningTableHeader KerningHeader =>
ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset);
/// <summary>
/// Gets the number of kerning entries.
/// </summary>
public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count);
/// <summary>
/// Gets the kerning entries.
/// </summary>
public Span<FdtReader.KerningTableEntry> PairAdjustments => new(
this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader),
this.KerningEntryCount);
/// <summary>
/// Gets the maximum texture index.
/// </summary>
public int MaxTextureIndex
{
get
{
var i = 0;
foreach (ref var g in this.Glyphs)
{
if (g.TextureIndex > i)
i = g.TextureIndex;
}
return i;
}
}
private FdtReader.FontTableEntry* GlyphsUnsafe =>
(FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset +
sizeof(FdtReader.FontTableHeader));
/// <summary>
/// Finds the glyph index for the corresponding codepoint.
/// </summary>
/// <param name="codepoint">Unicode codepoint (UTF-32 value).</param>
/// <returns>Corresponding index, or a negative number according to <see cref="List{T}.BinarySearch(int,int,T,System.Collections.Generic.IComparer{T}?)"/>.</returns>
public int FindGlyphIndex(int codepoint)
{
var comp = FdtReader.CodePointToUtf8Int32(codepoint);
var glyphs = this.GlyphsUnsafe;
var lo = 0;
var hi = this.FontHeader.FontTableEntryCount - 1;
while (lo <= hi)
{
var i = (int)(((uint)hi + (uint)lo) >> 1);
switch (comp.CompareTo(glyphs[i].CharUtf8))
{
case 0:
return i;
case > 0:
lo = i + 1;
break;
default:
hi = i - 1;
break;
}
}
return ~lo;
}
/// <summary>
/// Create a glyph range for use with <see cref="Interface.ManagedFontAtlas.SafeFontConfig.GlyphRanges"/>.
/// </summary>
/// <param name="mergeDistance">Merge two ranges into one if distance is below the value specified in this parameter.</param>
/// <returns>Glyph ranges.</returns>
public ushort[] ToGlyphRanges(int mergeDistance = 8)
{
var glyphs = this.Glyphs;
var ranges = new List<ushort>(glyphs.Length)
{
checked((ushort)glyphs[0].CharInt),
checked((ushort)glyphs[0].CharInt),
};
foreach (ref var glyph in glyphs[1..])
{
var c32 = glyph.CharInt;
if (c32 >= 0x10000)
break;
var c16 = unchecked((ushort)c32);
if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1])
{
ranges[^1] = c16;
}
else if (ranges[^1] + 1 < c16)
{
ranges.Add(c16);
ranges.Add(c16);
}
}
ranges.Add(0);
return ranges.ToArray();
}
}

View file

@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts;
/// <summary>
/// Enum of available game fonts in specific sizes.
/// </summary>
public enum GameFontFamilyAndSize
public enum GameFontFamilyAndSize : int
{
/// <summary>
/// Placeholder meaning unused.
@ -15,7 +15,6 @@ public enum GameFontFamilyAndSize
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
[GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)]
Axis96,
/// <summary>
@ -23,7 +22,6 @@ public enum GameFontFamilyAndSize
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
[GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)]
Axis12,
/// <summary>
@ -31,7 +29,6 @@ public enum GameFontFamilyAndSize
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
[GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)]
Axis14,
/// <summary>
@ -39,7 +36,6 @@ public enum GameFontFamilyAndSize
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
[GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)]
Axis18,
/// <summary>
@ -47,7 +43,6 @@ public enum GameFontFamilyAndSize
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
[GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)]
Axis36,
/// <summary>
@ -55,7 +50,6 @@ public enum GameFontFamilyAndSize
///
/// Serif font. Contains mostly ASCII range. Used in game for job names.
/// </summary>
[GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)]
Jupiter16,
/// <summary>
@ -63,7 +57,6 @@ public enum GameFontFamilyAndSize
///
/// Serif font. Contains mostly ASCII range. Used in game for job names.
/// </summary>
[GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)]
Jupiter20,
/// <summary>
@ -71,7 +64,6 @@ public enum GameFontFamilyAndSize
///
/// Serif font. Contains mostly ASCII range. Used in game for job names.
/// </summary>
[GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)]
Jupiter23,
/// <summary>
@ -79,7 +71,6 @@ public enum GameFontFamilyAndSize
///
/// Serif font. Contains mostly numbers. Used in game for flying texts.
/// </summary>
[GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)]
Jupiter45,
/// <summary>
@ -87,7 +78,6 @@ public enum GameFontFamilyAndSize
///
/// Serif font. Contains mostly ASCII range. Used in game for job names.
/// </summary>
[GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)]
Jupiter46,
/// <summary>
@ -95,7 +85,6 @@ public enum GameFontFamilyAndSize
///
/// Serif font. Contains mostly numbers. Used in game for flying texts.
/// </summary>
[GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)]
Jupiter90,
/// <summary>
@ -103,7 +92,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
/// </summary>
[GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)]
Meidinger16,
/// <summary>
@ -111,7 +99,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
/// </summary>
[GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)]
Meidinger20,
/// <summary>
@ -119,7 +106,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
/// </summary>
[GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)]
Meidinger40,
/// <summary>
@ -127,7 +113,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
[GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)]
MiedingerMid10,
/// <summary>
@ -135,7 +120,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
[GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)]
MiedingerMid12,
/// <summary>
@ -143,7 +127,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
[GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)]
MiedingerMid14,
/// <summary>
@ -151,7 +134,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
[GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)]
MiedingerMid18,
/// <summary>
@ -159,7 +141,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
[GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)]
MiedingerMid36,
/// <summary>
@ -167,7 +148,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
/// </summary>
[GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)]
TrumpGothic184,
/// <summary>
@ -175,7 +155,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
/// </summary>
[GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)]
TrumpGothic23,
/// <summary>
@ -183,7 +162,6 @@ public enum GameFontFamilyAndSize
///
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
/// </summary>
[GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)]
TrumpGothic34,
/// <summary>
@ -191,6 +169,5 @@ public enum GameFontFamilyAndSize
///
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
/// </summary>
[GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)]
TrumpGothic68,
}

View file

@ -1,37 +0,0 @@
namespace Dalamud.Interface.GameFonts;
/// <summary>
/// Marks the path for an enum value.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
internal class GameFontFamilyAndSizeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="GameFontFamilyAndSizeAttribute"/> class.
/// </summary>
/// <param name="path">Inner path of the file.</param>
/// <param name="texPathFormat">the file path format for the relevant .tex files.</param>
/// <param name="horizontalOffset">Horizontal offset of the corresponding font.</param>
public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset)
{
this.Path = path;
this.TexPathFormat = texPathFormat;
this.HorizontalOffset = horizontalOffset;
}
/// <summary>
/// Gets the path.
/// </summary>
public string Path { get; }
/// <summary>
/// Gets the file path format for the relevant .tex files.<br />
/// Used for <see cref="string.Format(string,object?)"/>(<see cref="TexPathFormat"/>, <see cref="int"/>).
/// </summary>
public string TexPathFormat { get; }
/// <summary>
/// Gets the horizontal offset of the corresponding font.
/// </summary>
public int HorizontalOffset { get; }
}

View file

@ -1,76 +1,75 @@
using System;
using System.Numerics;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using ImGuiNET;
namespace Dalamud.Interface.GameFonts;
/// <summary>
/// ABI-compatible wrapper for <see cref="IFontHandle"/>.
/// Prepare and keep game font loaded for use in OnDraw.
/// </summary>
public sealed class GameFontHandle : IFontHandle
public class GameFontHandle : IDisposable
{
private readonly IFontHandle.IInternal fontHandle;
private readonly FontAtlasFactory fontAtlasFactory;
private readonly GameFontManager manager;
private readonly GameFontStyle fontStyle;
/// <summary>
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.
/// </summary>
/// <param name="fontHandle">The wrapped <see cref="IFontHandle"/>.</param>
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory)
/// <param name="manager">GameFontManager instance.</param>
/// <param name="font">Font to use.</param>
internal GameFontHandle(GameFontManager manager, GameFontStyle font)
{
this.fontHandle = fontHandle;
this.fontAtlasFactory = fontAtlasFactory;
this.manager = manager;
this.fontStyle = font;
}
/// <inheritdoc />
public Exception? LoadException => this.fontHandle.LoadException;
/// <inheritdoc />
public bool Available => this.fontHandle.Available;
/// <inheritdoc cref="IFontHandle.IInternal.ImFont"/>
[Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)]
public ImFontPtr ImFont => this.fontHandle.ImFont;
/// <summary>
/// Gets the font style. Only applicable for <see cref="GameFontHandle"/>.
/// Gets the font style.
/// </summary>
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle;
public GameFontStyle Style => this.fontStyle;
/// <summary>
/// Gets the relevant <see cref="FdtReader"/>.<br />
/// <br />
/// Only applicable for game fonts. Otherwise it will throw.
/// Gets a value indicating whether this font is ready for use.
/// </summary>
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!;
/// <inheritdoc />
public void Dispose() => this.fontHandle.Dispose();
/// <inheritdoc/>
public IDisposable Push() => this.fontHandle.Push();
public bool Available
{
get
{
unsafe
{
return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null;
}
}
}
/// <summary>
/// Creates a new <see cref="GameFontLayoutPlan.Builder"/>.<br />
/// <br />
/// Only applicable for game fonts. Otherwise it will throw.
/// Gets the font.
/// </summary>
public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value;
/// <summary>
/// Gets the FdtReader.
/// </summary>
public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize);
/// <summary>
/// Creates a new GameFontLayoutPlan.Builder.
/// </summary>
/// <param name="text">Text.</param>
/// <returns>A new builder for GameFontLayoutPlan.</returns>
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text);
public GameFontLayoutPlan.Builder LayoutBuilder(string text)
{
return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text);
}
/// <inheritdoc/>
public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle);
/// <summary>
/// Draws text.
/// </summary>
/// <param name="text">Text to draw.</param>
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
public void Text(string text)
{
if (!this.Available)
@ -94,7 +93,6 @@ public sealed class GameFontHandle : IFontHandle
/// </summary>
/// <param name="col">Color.</param>
/// <param name="text">Text to draw.</param>
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
public void TextColored(Vector4 col, string text)
{
ImGui.PushStyleColor(ImGuiCol.Text, col);
@ -106,7 +104,6 @@ public sealed class GameFontHandle : IFontHandle
/// Draws disabled text.
/// </summary>
/// <param name="text">Text to draw.</param>
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
public void TextDisabled(string text)
{
unsafe

View file

@ -0,0 +1,507 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Utility.Timing;
using ImGuiNET;
using Lumina.Data.Files;
using Serilog;
using static Dalamud.Interface.Utility.ImGuiHelpers;
namespace Dalamud.Interface.GameFonts;
/// <summary>
/// Loads game font for use in ImGui.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal class GameFontManager : IServiceType
{
private static readonly string?[] FontNames =
{
null,
"AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36",
"Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90",
"Meidinger_16", "Meidinger_20", "Meidinger_40",
"MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36",
"TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68",
};
private readonly object syncRoot = new();
private readonly FdtReader?[] fdts;
private readonly List<byte[]> texturePixels;
private readonly Dictionary<GameFontStyle, ImFontPtr> fonts = new();
private readonly Dictionary<GameFontStyle, int> fontUseCounter = new();
private readonly Dictionary<GameFontStyle, Dictionary<char, Tuple<int, FdtReader.FontTableEntry>>> glyphRectIds = new();
#pragma warning disable CS0414
private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false;
#pragma warning restore CS0414
[ServiceManager.ServiceConstructor]
private GameFontManager(DataManager dataManager)
{
using (Timings.Start("Getting fdt data"))
{
this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray();
}
using (Timings.Start("Getting texture data"))
{
var texTasks = Enumerable
.Range(1, 1 + this.fdts
.Where(x => x != null)
.Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max())
.Max())
.Select(x => dataManager.GetFile<TexFile>($"common/font/font{x}.tex")!)
.Select(x => new Task<byte[]>(Timings.AttachTimingHandle(() => x.ImageData!)))
.ToArray();
foreach (var task in texTasks)
task.Start();
this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList();
}
}
/// <summary>
/// Describe font into a string.
/// </summary>
/// <param name="font">Font to describe.</param>
/// <returns>A string in a form of "FontName (NNNpt)".</returns>
public static string DescribeFont(GameFontFamilyAndSize font)
{
return font switch
{
GameFontFamilyAndSize.Undefined => "-",
GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)",
GameFontFamilyAndSize.Axis12 => "AXIS (12pt)",
GameFontFamilyAndSize.Axis14 => "AXIS (14pt)",
GameFontFamilyAndSize.Axis18 => "AXIS (18pt)",
GameFontFamilyAndSize.Axis36 => "AXIS (36pt)",
GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)",
GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)",
GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)",
GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)",
GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)",
GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)",
GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)",
GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)",
GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)",
GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)",
GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)",
GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)",
GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)",
GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)",
GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)",
GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)",
GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)",
GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)",
_ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"),
};
}
/// <summary>
/// Determines whether a font should be able to display most of stuff.
/// </summary>
/// <param name="font">Font to check.</param>
/// <returns>True if it can.</returns>
public static bool IsGenericPurposeFont(GameFontFamilyAndSize font)
{
return font switch
{
GameFontFamilyAndSize.Axis96 => true,
GameFontFamilyAndSize.Axis12 => true,
GameFontFamilyAndSize.Axis14 => true,
GameFontFamilyAndSize.Axis18 => true,
GameFontFamilyAndSize.Axis36 => true,
_ => false,
};
}
/// <summary>
/// Unscales fonts after they have been rendered onto atlas.
/// </summary>
/// <param name="fontPtr">Font to unscale.</param>
/// <param name="fontScale">Scale factor.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true)
{
if (fontScale == 1)
return;
unsafe
{
var font = fontPtr.NativePtr;
for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i)
{
font->IndexedHotData.Ref<ImFontGlyphHotDataReal>(i).AdvanceX /= fontScale;
font->IndexedHotData.Ref<ImFontGlyphHotDataReal>(i).OccupiedWidth /= fontScale;
}
font->FontSize /= fontScale;
font->Ascent /= fontScale;
font->Descent /= fontScale;
if (font->ConfigData != null)
font->ConfigData->SizePixels /= fontScale;
var glyphs = (ImFontGlyphReal*)font->Glyphs.Data;
for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++)
{
var glyph = &glyphs[i];
glyph->X0 /= fontScale;
glyph->X1 /= fontScale;
glyph->Y0 /= fontScale;
glyph->Y1 /= fontScale;
glyph->AdvanceX /= fontScale;
}
for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++)
font->KerningPairs.Ref<ImFontKerningPair>(i).AdvanceXAdjustment /= fontScale;
for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++)
font->FrequentKerningPairs.Ref<float>(i) /= fontScale;
}
if (rebuildLookupTable && fontPtr.Glyphs.Size > 0)
fontPtr.BuildLookupTableNonstandard();
}
/// <summary>
/// Create a glyph range for use with ImGui AddFont.
/// </summary>
/// <param name="family">Font family and size.</param>
/// <param name="mergeDistance">Merge two ranges into one if distance is below the value specified in this parameter.</param>
/// <returns>Glyph ranges.</returns>
public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8)
{
var fdt = this.fdts[(int)family]!;
var ranges = new List<ushort>(fdt.Glyphs.Count)
{
checked((ushort)fdt.Glyphs[0].CharInt),
checked((ushort)fdt.Glyphs[0].CharInt),
};
foreach (var glyph in fdt.Glyphs.Skip(1))
{
var c32 = glyph.CharInt;
if (c32 >= 0x10000)
break;
var c16 = unchecked((ushort)c32);
if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1])
{
ranges[^1] = c16;
}
else if (ranges[^1] + 1 < c16)
{
ranges.Add(c16);
ranges.Add(c16);
}
}
return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned);
}
/// <summary>
/// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process.
/// </summary>
/// <param name="style">Font to use.</param>
/// <returns>Handle to game font that may or may not be ready yet.</returns>
public GameFontHandle NewFontRef(GameFontStyle style)
{
var interfaceManager = Service<InterfaceManager>.Get();
var needRebuild = false;
lock (this.syncRoot)
{
this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1;
}
needRebuild = !this.fonts.ContainsKey(style);
if (needRebuild)
{
Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString());
Service<Framework>.GetAsync()
.ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts()));
}
return new(this, style);
}
/// <summary>
/// Gets the font.
/// </summary>
/// <param name="style">Font to get.</param>
/// <returns>Corresponding font or null.</returns>
public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null);
/// <summary>
/// Gets the corresponding FdtReader.
/// </summary>
/// <param name="family">Font to get.</param>
/// <returns>Corresponding FdtReader or null.</returns>
public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family];
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable)
{
ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable);
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable)
{
ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable);
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable)
{
ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable);
}
/// <summary>
/// Build fonts before plugins do something more. To be called from InterfaceManager.
/// </summary>
public void BuildFonts()
{
this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true;
this.glyphRectIds.Clear();
this.fonts.Clear();
lock (this.syncRoot)
{
foreach (var style in this.fontUseCounter.Keys)
this.EnsureFont(style);
}
}
/// <summary>
/// Record that ImGui.GetIO().Fonts.Build() has been called.
/// </summary>
public void AfterIoFontsBuild()
{
this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false;
}
/// <summary>
/// Checks whether GameFontMamager owns an ImFont.
/// </summary>
/// <param name="fontPtr">ImFontPtr to check.</param>
/// <returns>Whether it owns.</returns>
public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr);
/// <summary>
/// Post-build fonts before plugins do something more. To be called from InterfaceManager.
/// </summary>
public unsafe void AfterBuildFonts()
{
var interfaceManager = Service<InterfaceManager>.Get();
var ioFonts = ImGui.GetIO().Fonts;
var fontGamma = interfaceManager.FontGamma;
var pixels8s = new byte*[ioFonts.Textures.Size];
var pixels32s = new uint*[ioFonts.Textures.Size];
var widths = new int[ioFonts.Textures.Size];
var heights = new int[ioFonts.Textures.Size];
for (var i = 0; i < pixels8s.Length; i++)
{
ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]);
pixels32s[i] = (uint*)pixels8s[i];
}
foreach (var (style, font) in this.fonts)
{
var fdt = this.fdts[(int)style.FamilyAndSize];
var scale = style.SizePt / fdt.FontHeader.Size;
var fontPtr = font.NativePtr;
Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale);
fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3;
if (fontPtr->ConfigData != null)
fontPtr->ConfigData->SizePixels = fontPtr->FontSize;
fontPtr->Ascent = fdt.FontHeader.Ascent;
fontPtr->Descent = fdt.FontHeader.Descent;
fontPtr->EllipsisChar = '…';
foreach (var fallbackCharCandidate in "〓?!")
{
var glyph = font.FindGlyphNoFallback(fallbackCharCandidate);
if ((IntPtr)glyph.NativePtr != IntPtr.Zero)
{
var ptr = font.NativePtr;
ptr->FallbackChar = fallbackCharCandidate;
ptr->FallbackGlyph = glyph.NativePtr;
ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address<ImFontGlyphHotDataReal>(fallbackCharCandidate);
break;
}
}
// I have no idea what's causing NPE, so just to be safe
try
{
if (font.NativePtr != null && font.NativePtr->ConfigData != null)
{
var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0");
Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count));
}
}
catch (NullReferenceException)
{
// do nothing
}
foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style])
{
var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr;
var pixels8 = pixels8s[rc->TextureIndex];
var pixels32 = pixels32s[rc->TextureIndex];
var width = widths[rc->TextureIndex];
var height = heights[rc->TextureIndex];
var sourceBuffer = this.texturePixels[glyph.TextureFileIndex];
var sourceBufferDelta = glyph.TextureChannelByteIndex;
var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph);
if (widthAdjustment == 0)
{
for (var y = 0; y < glyph.BoundingHeight; y++)
{
for (var x = 0; x < glyph.BoundingWidth; x++)
{
var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))];
pixels32[((rc->Y + y) * width) + rc->X + x] = (uint)(a << 24) | 0xFFFFFFu;
}
}
}
else
{
for (var y = 0; y < glyph.BoundingHeight; y++)
{
for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++)
pixels32[((rc->Y + y) * width) + rc->X + x] = 0xFFFFFFu;
}
for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++)
{
var boldStrength = Math.Min(1f, style.Weight + 1 - xbold);
for (var y = 0; y < glyph.BoundingHeight; y++)
{
float xDelta = xbold;
if (style.BaseSkewStrength > 0)
xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight;
else if (style.BaseSkewStrength < 0)
xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight;
var xDeltaInt = (int)Math.Floor(xDelta);
var xness = xDelta - xDeltaInt;
for (var x = 0; x < glyph.BoundingWidth; x++)
{
var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x;
var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)];
var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))];
var n = (a1 * xness) + (a2 * (1 - xness));
var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt;
pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n));
}
}
}
}
if (Math.Abs(fontGamma - 1.4f) >= 0.001)
{
// Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4)
for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++)
{
for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++)
{
var i = (((y * width) + x) * 4) + 3;
pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f);
}
}
}
}
UnscaleFont(font, 1 / scale, false);
}
}
/// <summary>
/// Decrease font reference counter.
/// </summary>
/// <param name="style">Font to release.</param>
internal void DecreaseFontRef(GameFontStyle style)
{
lock (this.syncRoot)
{
if (!this.fontUseCounter.ContainsKey(style))
return;
if ((this.fontUseCounter[style] -= 1) == 0)
this.fontUseCounter.Remove(style);
}
}
private unsafe void EnsureFont(GameFontStyle style)
{
var rectIds = this.glyphRectIds[style] = new();
var fdt = this.fdts[(int)style.FamilyAndSize];
if (fdt == null)
return;
ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig();
fontConfig.OversampleH = 1;
fontConfig.OversampleV = 1;
fontConfig.PixelSnapH = false;
var io = ImGui.GetIO();
var font = io.Fonts.AddFontDefault(fontConfig);
fontConfig.Destroy();
this.fonts[style] = font;
foreach (var glyph in fdt.Glyphs)
{
var c = glyph.Char;
if (c < 32 || c >= 0xFFFF)
continue;
var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph);
rectIds[c] = Tuple.Create(
io.Fonts.AddCustomRectFontGlyph(
font,
c,
glyph.BoundingWidth + widthAdjustment,
glyph.BoundingHeight,
glyph.AdvanceWidth,
new Vector2(0, glyph.CurrentOffsetY)),
glyph);
}
foreach (var kernPair in fdt.Distances)
font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset);
}
}

View file

@ -64,7 +64,7 @@ public struct GameFontStyle
/// </summary>
public float SizePt
{
readonly get => this.SizePx * 3 / 4;
get => this.SizePx * 3 / 4;
set => this.SizePx = value * 4 / 3;
}
@ -73,14 +73,14 @@ public struct GameFontStyle
/// </summary>
public float BaseSkewStrength
{
readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx;
get => this.SkewStrength * this.BaseSizePx / this.SizePx;
set => this.SkewStrength = value * this.SizePx / this.BaseSizePx;
}
/// <summary>
/// Gets the font family.
/// </summary>
public readonly GameFontFamily Family => this.FamilyAndSize switch
public GameFontFamily Family => this.FamilyAndSize switch
{
GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined,
GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis,
@ -112,7 +112,7 @@ public struct GameFontStyle
/// <summary>
/// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes.
/// </summary>
public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch
public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch
{
GameFontFamily.Axis => GameFontFamilyAndSize.Axis96,
GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16,
@ -126,7 +126,7 @@ public struct GameFontStyle
/// <summary>
/// Gets the base font size in point unit.
/// </summary>
public readonly float BaseSizePt => this.FamilyAndSize switch
public float BaseSizePt => this.FamilyAndSize switch
{
GameFontFamilyAndSize.Undefined => 0,
GameFontFamilyAndSize.Axis96 => 9.6f,
@ -158,14 +158,14 @@ public struct GameFontStyle
/// <summary>
/// Gets the base font size in pixel unit.
/// </summary>
public readonly float BaseSizePx => this.BaseSizePt * 4 / 3;
public float BaseSizePx => this.BaseSizePt * 4 / 3;
/// <summary>
/// Gets or sets a value indicating whether this font is bold.
/// </summary>
public bool Bold
{
readonly get => this.Weight > 0f;
get => this.Weight > 0f;
set => this.Weight = value ? 1f : 0f;
}
@ -174,8 +174,8 @@ public struct GameFontStyle
/// </summary>
public bool Italic
{
readonly get => this.SkewStrength != 0;
set => this.SkewStrength = value ? this.SizePx / 6 : 0;
get => this.SkewStrength != 0;
set => this.SkewStrength = value ? this.SizePx / 7 : 0;
}
/// <summary>
@ -233,26 +233,13 @@ public struct GameFontStyle
_ => GameFontFamilyAndSize.Undefined,
};
/// <summary>
/// Creates a new scaled instance of <see cref="GameFontStyle"/> struct.
/// </summary>
/// <param name="scale">The scale.</param>
/// <returns>The scaled instance.</returns>
public readonly GameFontStyle Scale(float scale) => new()
{
FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale),
SizePx = this.SizePx * scale,
Weight = this.Weight,
SkewStrength = this.SkewStrength * scale,
};
/// <summary>
/// Calculates the adjustment to width resulting fron Weight and SkewStrength.
/// </summary>
/// <param name="header">Font header.</param>
/// <param name="glyph">Glyph.</param>
/// <returns>Width adjustment in pixel unit.</returns>
public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph)
public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph)
{
var widthDelta = this.Weight;
switch (this.BaseSkewStrength)
@ -276,11 +263,11 @@ public struct GameFontStyle
/// <param name="reader">Font information.</param>
/// <param name="glyph">Glyph.</param>
/// <returns>Width adjustment in pixel unit.</returns>
public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) =>
public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) =>
this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph);
/// <inheritdoc/>
public override readonly string ToString()
public override string ToString()
{
return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})";
}

View file

@ -11,7 +11,6 @@ using System.Text.Unicode;
using Dalamud.Game.Text;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using ImGuiNET;
@ -197,9 +196,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
{
if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
{
if (Service<FontAtlasFactory>.Get()
?.GetFdtReader(GameFontFamilyAndSize.Axis12)
.FindGlyph(chr) is null)
if (Service<GameFontManager>.Get()
.GetFdtReader(GameFontFamilyAndSize.Axis12)
?.FindGlyph(chr) is null)
{
if (!this.EncounteredHan)
{

View file

@ -21,7 +21,6 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.SelfTest;
using Dalamud.Interface.Internal.Windows.Settings;
using Dalamud.Interface.Internal.Windows.StyleEditor;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
@ -94,8 +93,7 @@ internal class DalamudInterface : IDisposable, IServiceType
private DalamudInterface(
Dalamud dalamud,
DalamudConfiguration configuration,
FontAtlasFactory fontAtlasFactory,
InterfaceManager interfaceManager,
InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene,
PluginImageCache pluginImageCache,
DalamudAssetManager dalamudAssetManager,
Game.Framework framework,
@ -105,7 +103,7 @@ internal class DalamudInterface : IDisposable, IServiceType
{
this.dalamud = dalamud;
this.configuration = configuration;
this.interfaceManager = interfaceManager;
this.interfaceManager = interfaceManagerWithScene.Manager;
this.WindowSystem = new WindowSystem("DalamudCore");
@ -124,14 +122,10 @@ internal class DalamudInterface : IDisposable, IServiceType
clientState,
configuration,
dalamudAssetManager,
fontAtlasFactory,
framework,
gameGui,
titleScreenMenu) { IsOpen = false };
this.changelogWindow = new ChangelogWindow(
this.titleScreenMenuWindow,
fontAtlasFactory,
dalamudAssetManager) { IsOpen = false };
this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false };
this.profilerWindow = new ProfilerWindow() { IsOpen = false };
this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false };
this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false };
@ -213,7 +207,6 @@ internal class DalamudInterface : IDisposable, IServiceType
{
this.interfaceManager.Draw -= this.OnDraw;
this.WindowSystem.Windows.OfType<IDisposable>().AggregateToDisposable().Dispose();
this.WindowSystem.RemoveAllWindows();
this.changelogWindow.Dispose();

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
using System.IO;
using System.Linq;
using System.Numerics;
@ -6,8 +7,6 @@ using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
@ -35,12 +34,6 @@ internal sealed class ChangelogWindow : Window, IDisposable
private readonly TitleScreenMenuWindow tsmWindow;
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
private readonly IFontAtlas privateAtlas;
private readonly Lazy<IFontHandle> bannerFont;
private readonly Lazy<IDalamudTextureWrap> apiBumpExplainerTexture;
private readonly Lazy<IDalamudTextureWrap> logoTexture;
private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f))
{
Point1 = Vector2.Zero,
@ -53,6 +46,10 @@ internal sealed class ChangelogWindow : Window, IDisposable
Point2 = Vector2.One,
};
private IDalamudTextureWrap? apiBumpExplainerTexture;
private IDalamudTextureWrap? logoTexture;
private GameFontHandle? bannerFont;
private State state = State.WindowFadeIn;
private bool needFadeRestart = false;
@ -61,28 +58,15 @@ internal sealed class ChangelogWindow : Window, IDisposable
/// Initializes a new instance of the <see cref="ChangelogWindow"/> class.
/// </summary>
/// <param name="tsmWindow">TSM window.</param>
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
/// <param name="assets">An instance of <see cref="DalamudAssetManager"/>.</param>
public ChangelogWindow(
TitleScreenMenuWindow tsmWindow,
FontAtlasFactory fontAtlasFactory,
DalamudAssetManager assets)
public ChangelogWindow(TitleScreenMenuWindow tsmWindow)
: base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true)
{
this.tsmWindow = tsmWindow;
this.Namespace = "DalamudChangelogWindow";
this.privateAtlas = this.scopedFinalizer.Add(
fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async));
this.bannerFont = new(
() => this.scopedFinalizer.Add(
this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18))));
this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon));
this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo));
// If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch
if (WarrantsChangelog())
_ = this.bannerFont.Value;
Service<GameFontManager>.GetAsync().ContinueWith(t => this.MakeFont(t.Result));
}
private enum State
@ -113,13 +97,21 @@ internal sealed class ChangelogWindow : Window, IDisposable
Service<DalamudInterface>.Get().SetCreditsDarkeningAnimation(true);
this.tsmWindow.AllowDrawing = false;
_ = this.bannerFont;
this.MakeFont(Service<GameFontManager>.Get());
this.state = State.WindowFadeIn;
this.windowFade.Reset();
this.bodyFade.Reset();
this.needFadeRestart = true;
if (this.apiBumpExplainerTexture == null)
{
var dalamud = Service<Dalamud>.Get();
var tm = Service<TextureManager>.Get();
this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png")))
?? throw new Exception("Could not load api bump explainer.");
}
base.OnOpen();
}
@ -194,7 +186,10 @@ internal sealed class ChangelogWindow : Window, IDisposable
ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2));
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f)))
ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize);
{
this.logoTexture ??= Service<DalamudAssetManager>.Get().GetDalamudTextureWrap(DalamudAsset.Logo);
ImGui.Image(this.logoTexture.ImGuiHandle, logoSize);
}
}
ImGui.SameLine();
@ -210,7 +205,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f)))
{
using var font = this.bannerFont.Value.Push();
using var font = ImRaii.PushFont(this.bannerFont!.ImFont);
switch (this.state)
{
@ -281,10 +276,8 @@ internal sealed class ChangelogWindow : Window, IDisposable
ImGuiHelpers.ScaledDummy(15);
ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width);
ImGui.Image(
this.apiBumpExplainerTexture.Value.ImGuiHandle,
this.apiBumpExplainerTexture.Value.Size);
ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width);
ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size);
DrawNextButton(State.Links);
break;
@ -384,4 +377,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
public void Dispose()
{
}
private void MakeFont(GameFontManager gfm) =>
this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18));
}

View file

@ -6,8 +6,6 @@ using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Utility;
using ImGuiNET;
using Serilog;
@ -16,7 +14,7 @@ namespace Dalamud.Interface.Internal.Windows.Data;
/// <summary>
/// Class responsible for drawing the data/debug window.
/// </summary>
internal class DataWindow : Window, IDisposable
internal class DataWindow : Window
{
private readonly IDataWindowWidget[] modules =
{
@ -36,7 +34,6 @@ internal class DataWindow : Window, IDisposable
new FlyTextWidget(),
new FontAwesomeTestWidget(),
new GameInventoryTestWidget(),
new GamePrebakedFontsTestWidget(),
new GamepadWidget(),
new GaugeWidget(),
new HookWidget(),
@ -79,9 +76,6 @@ internal class DataWindow : Window, IDisposable
this.Load();
}
/// <inheritdoc/>
public void Dispose() => this.modules.OfType<IDisposable>().AggregateToDisposable().Dispose();
/// <inheritdoc/>
public override void OnOpen()
{

View file

@ -1,213 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
/// Widget for testing game prebaked fonts.
/// </summary>
internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
{
private ImVectorWrapper<byte> testStringBuffer;
private IFontAtlas? privateAtlas;
private IReadOnlyDictionary<GameFontFamily, (GameFontStyle Size, Lazy<IFontHandle> Handle)[]>? fontHandles;
private bool useGlobalScale;
private bool useWordWrap;
private bool useItalic;
private bool useBold;
private bool useMinimumBuild;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; }
/// <inheritdoc/>
public string DisplayName { get; init; } = "Game Prebaked Fonts";
/// <inheritdoc/>
public bool Ready { get; set; }
/// <inheritdoc/>
public void Load() => this.Ready = true;
/// <inheritdoc/>
public unsafe void Draw()
{
ImGui.AlignTextToFramePadding();
fixed (byte* labelPtr = "Global Scale"u8)
{
var v = (byte)(this.useGlobalScale ? 1 : 0);
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
{
this.useGlobalScale = v != 0;
this.ClearAtlas();
}
}
ImGui.SameLine();
fixed (byte* labelPtr = "Word Wrap"u8)
{
var v = (byte)(this.useWordWrap ? 1 : 0);
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
this.useWordWrap = v != 0;
}
ImGui.SameLine();
fixed (byte* labelPtr = "Italic"u8)
{
var v = (byte)(this.useItalic ? 1 : 0);
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
{
this.useItalic = v != 0;
this.ClearAtlas();
}
}
ImGui.SameLine();
fixed (byte* labelPtr = "Bold"u8)
{
var v = (byte)(this.useBold ? 1 : 0);
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
{
this.useBold = v != 0;
this.ClearAtlas();
}
}
ImGui.SameLine();
fixed (byte* labelPtr = "Minimum Range"u8)
{
var v = (byte)(this.useMinimumBuild ? 1 : 0);
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
{
this.useMinimumBuild = v != 0;
this.ClearAtlas();
}
}
ImGui.SameLine();
if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed)
{
this.testStringBuffer.Dispose();
this.testStringBuffer = ImVectorWrapper.CreateFromSpan(
"(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8,
minCapacity: 1024);
}
fixed (byte* labelPtr = "Test Input"u8)
{
if (ImGuiNative.igInputTextMultiline(
labelPtr,
this.testStringBuffer.Data,
(uint)this.testStringBuffer.Capacity,
new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale),
0,
null,
null) != 0)
{
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
if (len + 4 >= this.testStringBuffer.Capacity)
this.testStringBuffer.EnsureCapacityExponential(len + 4);
if (len < this.testStringBuffer.Capacity)
{
this.testStringBuffer.LengthUnsafe = len;
this.testStringBuffer.StorageSpan[len] = default;
}
if (this.useMinimumBuild)
_ = this.privateAtlas?.BuildFontsAsync();
}
}
this.privateAtlas ??=
Service<FontAtlasFactory>.Get().CreateFontAtlas(
nameof(GamePrebakedFontsTestWidget),
FontAtlasAutoRebuildMode.Async,
this.useGlobalScale);
this.fontHandles ??=
Enum.GetValues<GameFontFamilyAndSize>()
.Where(x => x.GetAttribute<GameFontFamilyAndSizeAttribute>() is not null)
.Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold })
.GroupBy(x => x.Family)
.ToImmutableDictionary(
x => x.Key,
x => x.Select(
y => (y, new Lazy<IFontHandle>(
() => this.useMinimumBuild
? this.privateAtlas.NewDelegateFontHandle(
e =>
e.OnPreBuild(
tk => tk.AddGameGlyphs(
y,
Encoding.UTF8.GetString(
this.testStringBuffer.DataSpan).ToGlyphRange(),
default)))
: this.privateAtlas.NewGameFontHandle(y))))
.ToArray());
var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2);
foreach (var (family, items) in this.fontHandles)
{
if (!ImGui.CollapsingHeader($"{family} Family"))
continue;
foreach (var (gfs, handle) in items)
{
ImGui.TextUnformatted($"{gfs.SizePt}pt");
ImGui.SameLine(offsetX);
ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f);
try
{
if (handle.Value.LoadException is { } exc)
{
ImGui.TextUnformatted(exc.ToString());
}
else if (!handle.Value.Available)
{
fixed (byte* labelPtr = "Loading..."u8)
ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3));
}
else
{
if (!this.useGlobalScale)
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
using var pushPop = handle.Value.Push();
ImGuiNative.igTextUnformatted(
this.testStringBuffer.Data,
this.testStringBuffer.Data + this.testStringBuffer.Length);
}
}
finally
{
ImGuiNative.igPopTextWrapPos();
ImGuiNative.igSetWindowFontScale(1);
}
}
}
}
/// <inheritdoc/>
public void Dispose()
{
this.ClearAtlas();
this.testStringBuffer.Dispose();
}
private void ClearAtlas()
{
this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value))
.AggregateToDisposable().Dispose();
this.fontHandles = null;
this.privateAtlas?.Dispose();
this.privateAtlas = null;
}
}

View file

@ -5,10 +5,10 @@ using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.Windows.Settings.Tabs;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;
using ImGuiNET;
@ -19,7 +19,14 @@ namespace Dalamud.Interface.Internal.Windows.Settings;
/// </summary>
internal class SettingsWindow : Window
{
private SettingsTab[]? tabs;
private readonly SettingsTab[] tabs =
{
new SettingsTabGeneral(),
new SettingsTabLook(),
new SettingsTabDtr(),
new SettingsTabExperimental(),
new SettingsTabAbout(),
};
private string searchInput = string.Empty;
@ -42,15 +49,6 @@ internal class SettingsWindow : Window
/// <inheritdoc/>
public override void OnOpen()
{
this.tabs ??= new SettingsTab[]
{
new SettingsTabGeneral(),
new SettingsTabLook(),
new SettingsTabDtr(),
new SettingsTabExperimental(),
new SettingsTabAbout(),
};
foreach (var settingsTab in this.tabs)
{
settingsTab.Load();
@ -66,12 +64,15 @@ internal class SettingsWindow : Window
{
var configuration = Service<DalamudConfiguration>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
var fontAtlasFactory = Service<FontAtlasFactory>.Get();
var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame;
var rebuildFont =
ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale ||
interfaceManager.FontGamma != configuration.FontGammaLevel ||
interfaceManager.UseAxis != configuration.UseAxisFontsFromGame;
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
fontAtlasFactory.UseAxisOverride = null;
interfaceManager.FontGammaOverride = null;
interfaceManager.UseAxisOverride = null;
if (rebuildFont)
interfaceManager.RebuildFonts();

View file

@ -1,13 +1,13 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Numerics;
using CheapLoc;
using Dalamud.Game.Gui;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Internal;
@ -15,6 +15,7 @@ using Dalamud.Storage.Assets;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiNET;
using ImGuiScene;
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
@ -172,21 +173,16 @@ Contribute at: https://github.com/goatcorp/Dalamud
";
private readonly Stopwatch creditsThrottler;
private readonly IFontAtlas privateAtlas;
private string creditsText;
private bool resetNow = false;
private IDalamudTextureWrap? logoTexture;
private IFontHandle? thankYouFont;
private GameFontHandle? thankYouFont;
public SettingsTabAbout()
{
this.creditsThrottler = new();
this.privateAtlas = Service<FontAtlasFactory>
.Get()
.CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async);
}
public override SettingsEntry[] Entries { get; } = { };
@ -211,7 +207,11 @@ Contribute at: https://github.com/goatcorp/Dalamud
this.creditsThrottler.Restart();
this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34));
if (this.thankYouFont == null)
{
var gfm = Service<GameFontManager>.Get();
this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34));
}
this.resetNow = true;
@ -269,12 +269,14 @@ Contribute at: https://github.com/goatcorp/Dalamud
if (this.thankYouFont != null)
{
using var fontPush = this.thankYouFont.Push();
ImGui.PushFont(this.thankYouFont.ImFont);
var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X;
ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f));
ImGui.SameLine();
ImGui.TextUnformatted(ThankYouText);
ImGui.PopFont();
}
ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f);
@ -303,5 +305,9 @@ Contribute at: https://github.com/goatcorp/Dalamud
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public override void Dispose() => this.privateAtlas.Dispose();
public override void Dispose()
{
this.logoTexture?.Dispose();
this.thankYouFont?.Dispose();
}
}

View file

@ -1,14 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Text;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using ImGuiNET;
@ -30,6 +28,7 @@ public class SettingsTabLook : SettingsTab
};
private float globalUiScale;
private float fontGamma;
public override SettingsEntry[] Entries { get; } =
{
@ -42,8 +41,9 @@ public class SettingsTabLook : SettingsTab
(v, c) => c.UseAxisFontsFromGame = v,
v =>
{
Service<FontAtlasFactory>.Get().UseAxisOverride = v;
Service<InterfaceManager>.Get().RebuildFonts();
var im = Service<InterfaceManager>.Get();
im.UseAxisOverride = v;
im.RebuildFonts();
}),
new GapSettingsEntry(5, true),
@ -145,7 +145,6 @@ public class SettingsTabLook : SettingsTab
public override void Draw()
{
var interfaceManager = Service<InterfaceManager>.Get();
var fontBuildTask = interfaceManager.FontBuildTask;
ImGui.AlignTextToFramePadding();
ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale"));
@ -165,19 +164,6 @@ public class SettingsTabLook : SettingsTab
}
}
if (!fontBuildTask.IsCompleted)
{
ImGui.SameLine();
var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts...");
unsafe
{
var len = Encoding.UTF8.GetByteCount(buildingFonts);
var p = stackalloc byte[len];
Encoding.UTF8.GetBytes(buildingFonts, new(p, len));
ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2);
}
}
var globalUiScaleInPt = 12f * this.globalUiScale;
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp))
{
@ -188,25 +174,33 @@ public class SettingsTabLook : SettingsTab
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays."));
if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled)
ImGuiHelpers.ScaledDummy(5);
ImGui.AlignTextToFramePadding();
ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma"));
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset"))
{
ImGui.TextColored(
ImGuiColors.DalamudRed,
Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested."));
if (fontBuildTask.Exception is not null
&& ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason"))
this.fontGamma = 1.4f;
interfaceManager.FontGammaOverride = this.fontGamma;
interfaceManager.RebuildFonts();
}
if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp))
{
foreach (var e in fontBuildTask.Exception.InnerExceptions)
ImGui.TextUnformatted(e.ToString());
}
interfaceManager.FontGammaOverride = this.fontGamma;
interfaceManager.RebuildFonts();
}
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text."));
base.Draw();
}
public override void Load()
{
this.globalUiScale = Service<DalamudConfiguration>.Get().GlobalUiScale;
this.fontGamma = Service<DalamudConfiguration>.Get().FontGammaLevel;
base.Load();
}

View file

@ -7,14 +7,11 @@ using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
@ -30,17 +27,16 @@ internal class TitleScreenMenuWindow : Window, IDisposable
private readonly ClientState clientState;
private readonly DalamudConfiguration configuration;
private readonly Framework framework;
private readonly GameGui gameGui;
private readonly TitleScreenMenu titleScreenMenu;
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
private readonly IFontAtlas privateAtlas;
private readonly Lazy<IFontHandle> myFontHandle;
private readonly Lazy<IDalamudTextureWrap> shadeTexture;
private readonly Dictionary<Guid, InOutCubic> shadeEasings = new();
private readonly Dictionary<Guid, InOutQuint> moveEasings = new();
private readonly Dictionary<Guid, InOutCubic> logoEasings = new();
private readonly Dictionary<string, InterfaceManager.SpecialGlyphRequest> specialGlyphRequests = new();
private InOutCubic? fadeOutEasing;
@ -52,7 +48,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
/// <param name="clientState">An instance of <see cref="ClientState"/>.</param>
/// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
/// <param name="dalamudAssetManager">An instance of <see cref="DalamudAssetManager"/>.</param>
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
/// <param name="framework">An instance of <see cref="Framework"/>.</param>
/// <param name="titleScreenMenu">An instance of <see cref="TitleScreenMenu"/>.</param>
/// <param name="gameGui">An instance of <see cref="gameGui"/>.</param>
@ -60,7 +55,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
ClientState clientState,
DalamudConfiguration configuration,
DalamudAssetManager dalamudAssetManager,
FontAtlasFactory fontAtlasFactory,
Framework framework,
GameGui gameGui,
TitleScreenMenu titleScreenMenu)
@ -71,6 +65,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
{
this.clientState = clientState;
this.configuration = configuration;
this.framework = framework;
this.gameGui = gameGui;
this.titleScreenMenu = titleScreenMenu;
@ -82,25 +77,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable
this.PositionCondition = ImGuiCond.Always;
this.RespectCloseHotkey = false;
this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade));
this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async);
this.scopedFinalizer.Add(this.privateAtlas);
this.myFontHandle = new(
() => this.scopedFinalizer.Add(
this.privateAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
toolkit => toolkit.AddDalamudDefaultFont(
TargetFontSizePx,
titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange())))));
titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange;
this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange);
this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade));
framework.Update += this.FrameworkOnUpdate;
this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate);
}
private enum State
@ -115,9 +94,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
/// </summary>
public bool AllowDrawing { get; set; } = true;
/// <inheritdoc/>
public void Dispose() => this.scopedFinalizer.Dispose();
/// <inheritdoc/>
public override void PreDraw()
{
@ -133,6 +109,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable
base.PostDraw();
}
/// <inheritdoc/>
public void Dispose()
{
this.framework.Update -= this.FrameworkOnUpdate;
}
/// <inheritdoc/>
public override void Draw()
{
@ -264,12 +246,33 @@ internal class TitleScreenMenuWindow : Window, IDisposable
break;
}
}
var srcText = entries.Select(e => e.Name).ToHashSet();
var keys = this.specialGlyphRequests.Keys.ToHashSet();
keys.RemoveWhere(x => srcText.Contains(x));
foreach (var key in keys)
{
this.specialGlyphRequests[key].Dispose();
this.specialGlyphRequests.Remove(key);
}
}
private bool DrawEntry(
TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable)
{
using var fontScopeDispose = this.myFontHandle.Value.Push();
InterfaceManager.SpecialGlyphRequest fontHandle;
if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx)
{
fontHandle.Dispose();
this.specialGlyphRequests.Remove(entry.Name);
fontHandle = null;
}
if (fontHandle == null)
this.specialGlyphRequests[entry.Name] = fontHandle = Service<InterfaceManager>.Get().NewFontSizeRef(TargetFontSizePx, entry.Name);
ImGui.PushFont(fontHandle.Font);
ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size);
var scale = ImGui.GetIO().FontGlobalScale;
@ -380,6 +383,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
initialCursor.Y += entry.Texture.Height * scale;
ImGui.SetCursorPos(initialCursor);
ImGui.PopFont();
return isHover;
}
@ -396,6 +401,4 @@ internal class TitleScreenMenuWindow : Window, IDisposable
if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero)
this.IsOpen = false;
}
private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync();
}

View file

@ -1,22 +0,0 @@
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// How to rebuild <see cref="IFontAtlas"/>.
/// </summary>
public enum FontAtlasAutoRebuildMode
{
/// <summary>
/// Do not rebuild.
/// </summary>
Disable,
/// <summary>
/// Rebuild on new frame.
/// </summary>
OnNewFrame,
/// <summary>
/// Rebuild asynchronously.
/// </summary>
Async,
}

View file

@ -1,38 +0,0 @@
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Build step for <see cref="IFontAtlas"/>.
/// </summary>
public enum FontAtlasBuildStep
{
/// <summary>
/// An invalid value. This should never be passed through event callbacks.
/// </summary>
Invalid,
/// <summary>
/// Called before calling <see cref="ImFontAtlasPtr.Build"/>.<br />
/// Expect <see cref="IFontAtlasBuildToolkitPreBuild"/> to be passed.
/// </summary>
PreBuild,
/// <summary>
/// Called after calling <see cref="ImFontAtlasPtr.Build"/>.<br />
/// Expect <see cref="IFontAtlasBuildToolkitPostBuild"/> to be passed.<br />
/// <br />
/// This callback is not guaranteed to happen after <see cref="PreBuild"/>,
/// but it will never happen on its own.
/// </summary>
PostBuild,
/// <summary>
/// Called after promoting staging font atlas to the actual atlas for <see cref="IFontAtlas"/>.<br />
/// Expect <see cref="PostBuild"/> to be passed.<br />
/// <br />
/// This callback is not guaranteed to happen after <see cref="IFontAtlasBuildToolkitPostPromotion"/>,
/// but it will never happen on its own.
/// </summary>
PostPromotion,
}

View file

@ -1,15 +0,0 @@
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Delegate to be called when a font needs to be built.
/// </summary>
/// <param name="toolkit">A toolkit that may help you for font building steps.</param>
/// <remarks>
/// An implementation of <see cref="IFontAtlasBuildToolkit"/> may implement all of
/// <see cref="IFontAtlasBuildToolkitPreBuild"/>, <see cref="IFontAtlasBuildToolkitPostBuild"/>, and
/// <see cref="IFontAtlasBuildToolkitPostPromotion"/>.<br />
/// Either use <see cref="IFontAtlasBuildToolkit.BuildStep"/> to identify the build step, or use
/// <see cref="FontAtlasBuildToolkitUtilities.OnPreBuild"/>, <see cref="FontAtlasBuildToolkitUtilities.OnPostBuild"/>,
/// and <see cref="FontAtlasBuildToolkitUtilities.OnPostPromotion"/> for routing.
/// </remarks>
public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit);

View file

@ -1,133 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Convenience function for building fonts through <see cref="IFontAtlas"/>.
/// </summary>
public static class FontAtlasBuildToolkitUtilities
{
/// <summary>
/// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges.
/// </summary>
/// <param name="enumerable">The chars.</param>
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns>
public static ushort[] ToGlyphRange(
this IEnumerable<char> enumerable,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true)
{
using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
foreach (var c in enumerable)
builder.AddChar(c);
return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
}
/// <summary>
/// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges.
/// </summary>
/// <param name="span">The chars.</param>
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns>
public static ushort[] ToGlyphRange(
this ReadOnlySpan<char> span,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true)
{
using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
foreach (var c in span)
builder.AddChar(c);
return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
}
/// <summary>
/// Compiles given string into an array of <see cref="ushort"/> containing ImGui glyph ranges.
/// </summary>
/// <param name="string">The string.</param>
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns>
public static ushort[] ToGlyphRange(
this string @string,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true) =>
@string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints);
/// <summary>
/// Finds the corresponding <see cref="ImFontConfigPtr"/> in
/// <see cref="IFontAtlasBuildToolkit.NewImAtlas"/>.<see cref="ImFontAtlasPtr.ConfigData"/> that corresponds to the
/// specified font <paramref name="fontPtr"/>.
/// </summary>
/// <param name="toolkit">The toolkit.</param>
/// <param name="fontPtr">The font.</param>
/// <returns>The relevant config pointer, or empty config pointer if not found.</returns>
public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr)
{
foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan)
{
if (c.DstFont == fontPtr.NativePtr)
return new((nint)Unsafe.AsPointer(ref c));
}
return default;
}
/// <summary>
/// Invokes <paramref name="action"/>
/// if <see cref="IFontAtlasBuildToolkit.BuildStep"/> of <paramref name="toolkit"/>
/// is <see cref="FontAtlasBuildStep.PreBuild"/>.
/// </summary>
/// <param name="toolkit">The toolkit.</param>
/// <param name="action">The action.</param>
/// <returns>This, for method chaining.</returns>
public static IFontAtlasBuildToolkit OnPreBuild(
this IFontAtlasBuildToolkit toolkit,
Action<IFontAtlasBuildToolkitPreBuild> action)
{
if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild)
action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit);
return toolkit;
}
/// <summary>
/// Invokes <paramref name="action"/>
/// if <see cref="IFontAtlasBuildToolkit.BuildStep"/> of <paramref name="toolkit"/>
/// is <see cref="FontAtlasBuildStep.PostBuild"/>.
/// </summary>
/// <param name="toolkit">The toolkit.</param>
/// <param name="action">The action.</param>
/// <returns>toolkit, for method chaining.</returns>
public static IFontAtlasBuildToolkit OnPostBuild(
this IFontAtlasBuildToolkit toolkit,
Action<IFontAtlasBuildToolkitPostBuild> action)
{
if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild)
action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit);
return toolkit;
}
/// <summary>
/// Invokes <paramref name="action"/>
/// if <see cref="IFontAtlasBuildToolkit.BuildStep"/> of <paramref name="toolkit"/>
/// is <see cref="FontAtlasBuildStep.PostPromotion"/>.
/// </summary>
/// <param name="toolkit">The toolkit.</param>
/// <param name="action">The action.</param>
/// <returns>toolkit, for method chaining.</returns>
public static IFontAtlasBuildToolkit OnPostPromotion(
this IFontAtlasBuildToolkit toolkit,
Action<IFontAtlasBuildToolkitPostPromotion> action)
{
if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion)
action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit);
return toolkit;
}
}

View file

@ -1,141 +0,0 @@
using System.Threading.Tasks;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Wrapper for <see cref="ImFontAtlasPtr"/>.
/// </summary>
public interface IFontAtlas : IDisposable
{
/// <summary>
/// Event to be called on build step changes.<br />
/// <see cref="IFontAtlasBuildToolkit.Font"/> is meaningless for this event.
/// </summary>
event FontAtlasBuildStepDelegate? BuildStepChange;
/// <summary>
/// Event fired when a font rebuild operation is recommended.<br />
/// This event will be invoked from the main thread.<br />
/// <br />
/// Reasons for the event include changes in <see cref="ImGuiHelpers.GlobalScale"/> and
/// initialization of new associated font handles.
/// </summary>
/// <remarks>
/// You should call <see cref="BuildFontsAsync"/> or <see cref="BuildFontsOnNextFrame"/>
/// if <see cref="AutoRebuildMode"/> is not set to <c>true</c>.<br />
/// Avoid calling <see cref="BuildFontsImmediately"/> here; it will block the main thread.
/// </remarks>
event Action? RebuildRecommend;
/// <summary>
/// Gets the name of the atlas. For logging and debugging purposes.
/// </summary>
string Name { get; }
/// <summary>
/// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes.
/// </summary>
FontAtlasAutoRebuildMode AutoRebuildMode { get; }
/// <summary>
/// Gets the font atlas. Might be empty.
/// </summary>
ImFontAtlasPtr ImAtlas { get; }
/// <summary>
/// Gets the task that represents the current font rebuild state.
/// </summary>
Task BuildTask { get; }
/// <summary>
/// Gets a value indicating whether there exists any built atlas, regardless of <see cref="BuildTask"/>.
/// </summary>
bool HasBuiltAtlas { get; }
/// <summary>
/// Gets a value indicating whether this font atlas is under the effect of global scale.
/// </summary>
bool IsGlobalScaled { get; }
/// <summary>
/// Suppresses automatically rebuilding fonts for the scope.
/// </summary>
/// <returns>An instance of <see cref="IDisposable"/> that will release the suppression.</returns>
/// <remarks>
/// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so.
/// This function will effectively do nothing, if <see cref="AutoRebuildMode"/> is set to
/// <see cref="FontAtlasAutoRebuildMode.Disable"/>.
/// </remarks>
/// <example>
/// <code>
/// using (atlas.SuppressBuild()) {
/// this.font1 = atlas.NewGameFontHandle(...);
/// this.font2 = atlas.NewDelegateFontHandle(...);
/// }
/// </code>
/// </example>
public IDisposable SuppressAutoRebuild();
/// <summary>
/// Creates a new <see cref="IFontHandle"/> from game's built-in fonts.
/// </summary>
/// <param name="style">Font to use.</param>
/// <returns>Handle to a font that may or may not be ready yet.</returns>
public IFontHandle NewGameFontHandle(GameFontStyle style);
/// <summary>
/// Creates a new IFontHandle using your own callbacks.
/// </summary>
/// <param name="buildStepDelegate">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
/// <returns>Handle to a font that may or may not be ready yet.</returns>
/// <example>
/// <b>On initialization</b>:
/// <code>
/// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => {
/// var config = new SafeFontConfig { SizePx = 16 };
/// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config);
/// tk.AddGameSymbol(config);
/// tk.AddExtraGlyphsForDalamudLanguage(config);
/// // optionally do the following if you have to add more than one font here,
/// // to specify which font added during this delegate is the final font to use.
/// tk.Font = config.MergeFont;
/// }));
/// // or
/// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36)));
/// </code>
/// <br />
/// <b>On use</b>:
/// <code>
/// using (this.fontHandle.Push())
/// ImGui.TextUnformatted("Example");
/// </code>
/// </example>
public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate);
/// <summary>
/// Queues rebuilding fonts, on the main thread.<br />
/// Note that <see cref="BuildTask"/> would not necessarily get changed from calling this function.
/// </summary>
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.Async"/>.</exception>
void BuildFontsOnNextFrame();
/// <summary>
/// Rebuilds fonts immediately, on the current thread.<br />
/// Even the callback for <see cref="FontAtlasBuildStep.PostPromotion"/> will be called on the same thread.
/// </summary>
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.Async"/>.</exception>
void BuildFontsImmediately();
/// <summary>
/// Rebuilds fonts asynchronously, on any thread.
/// </summary>
/// <param name="callPostPromotionOnMainThread">Call <see cref="FontAtlasBuildStep.PostPromotion"/> on the main thread.</param>
/// <returns>The task.</returns>
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.OnNewFrame"/>.</exception>
Task BuildFontsAsync(bool callPostPromotionOnMainThread = true);
}

View file

@ -1,67 +0,0 @@
using System.Runtime.InteropServices;
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.
/// </summary>
public interface IFontAtlasBuildToolkit
{
/// <summary>
/// Gets or sets the font relevant to the call.
/// </summary>
ImFontPtr Font { get; set; }
/// <summary>
/// Gets the current scale this font atlas is being built with.
/// </summary>
float Scale { get; }
/// <summary>
/// Gets a value indicating whether the current build operation is asynchronous.
/// </summary>
bool IsAsyncBuildOperation { get; }
/// <summary>
/// Gets the current build step.
/// </summary>
FontAtlasBuildStep BuildStep { get; }
/// <summary>
/// Gets the font atlas being built.
/// </summary>
ImFontAtlasPtr NewImAtlas { get; }
/// <summary>
/// Gets the wrapper for <see cref="ImFontAtlas.Fonts"/> of <see cref="NewImAtlas"/>.<br />
/// This does not need to be disposed. Calling <see cref="IDisposable.Dispose"/> does nothing.-
/// <br />
/// Modification of this vector may result in undefined behaviors.
/// </summary>
ImVectorWrapper<ImFontPtr> Fonts { get; }
/// <summary>
/// Queues an item to be disposed after the native atlas gets disposed, successful or not.
/// </summary>
/// <typeparam name="T">Disposable type.</typeparam>
/// <param name="disposable">The disposable.</param>
/// <returns>The same <paramref name="disposable"/>.</returns>
T DisposeWithAtlas<T>(T disposable) where T : IDisposable;
/// <summary>
/// Queues an item to be disposed after the native atlas gets disposed, successful or not.
/// </summary>
/// <param name="gcHandle">The gc handle.</param>
/// <returns>The same <paramref name="gcHandle"/>.</returns>
GCHandle DisposeWithAtlas(GCHandle gcHandle);
/// <summary>
/// Queues an item to be disposed after the native atlas gets disposed, successful or not.
/// </summary>
/// <param name="action">The action to run on dispose.</param>
void DisposeWithAtlas(Action action);
}

View file

@ -1,26 +0,0 @@
using Dalamud.Interface.Internal;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.
/// </summary>
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
{
/// <summary>
/// Gets whether global scaling is ignored for the given font.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>True if ignored.</returns>
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
/// <summary>
/// Stores a texture to be managed with the atlas.
/// </summary>
/// <param name="textureWrap">The texture wrap.</param>
/// <param name="disposeOnError">Dispose the wrap on error.</param>
/// <returns>The texture index.</returns>
int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError);
}

View file

@ -1,33 +0,0 @@
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostPromotion"/>.
/// </summary>
public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit
{
/// <summary>
/// Copies glyphs across fonts, in a safer way.<br />
/// If the font does not belong to the current atlas, this function is a no-op.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
void CopyGlyphsAcrossFonts(
ImFontPtr source,
ImFontPtr target,
bool missingOnly,
bool rebuildLookupTable = true,
char rangeLow = ' ',
char rangeHigh = '\uFFFE');
/// <summary>
/// Calls <see cref="ImFontPtr.BuildLookupTable"/>, with some fixups.
/// </summary>
/// <param name="font">The font.</param>
void BuildLookupTable(ImFontPtr font);
}

View file

@ -1,186 +0,0 @@
using System.IO;
using System.Runtime.InteropServices;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PreBuild"/>.<br />
/// <br />
/// After <see cref="FontAtlasBuildStepDelegate"/> returns,
/// either <see cref="IFontAtlasBuildToolkit.Font"/> must be set,
/// or at least one font must have been added to the atlas using one of AddFont... functions.
/// </summary>
public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
{
/// <summary>
/// Queues an item to be disposed after the whole build process gets complete, successful or not.
/// </summary>
/// <typeparam name="T">Disposable type.</typeparam>
/// <param name="disposable">The disposable.</param>
/// <returns>The same <paramref name="disposable"/>.</returns>
T DisposeAfterBuild<T>(T disposable) where T : IDisposable;
/// <summary>
/// Queues an item to be disposed after the whole build process gets complete, successful or not.
/// </summary>
/// <param name="gcHandle">The gc handle.</param>
/// <returns>The same <paramref name="gcHandle"/>.</returns>
GCHandle DisposeAfterBuild(GCHandle gcHandle);
/// <summary>
/// Queues an item to be disposed after the whole build process gets complete, successful or not.
/// </summary>
/// <param name="action">The action to run on dispose.</param>
void DisposeAfterBuild(Action action);
/// <summary>
/// Excludes given font from global scaling.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>Same <see cref="ImFontPtr"/> with <paramref name="fontPtr"/>.</returns>
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr);
/// <summary>
/// Gets whether global scaling is ignored for the given font.
/// </summary>
/// <param name="fontPtr">The font.</param>
/// <returns>True if ignored.</returns>
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
/// <summary>
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
/// <strong>It WILL crash if you try to use a memory pointer allocated in some other way.</strong><br />
/// <strong>
/// Do NOT call <see cref="ImGuiNative.igMemFree"/> on the <paramref name="dataPointer"/> once this function has
/// been called, unless <paramref name="freeOnException"/> is set and the function has thrown an error.
/// </strong>
/// </summary>
/// <param name="dataPointer">Memory address for the data allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.</param>
/// <param name="dataSize">The size of the font file..</param>
/// <param name="fontConfig">The font config.</param>
/// <param name="freeOnException">Free <paramref name="dataPointer"/> if an exception happens.</param>
/// <param name="debugTag">A debug tag.</param>
/// <returns>The newly added font.</returns>
unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
nint dataPointer,
int dataSize,
in SafeFontConfig fontConfig,
bool freeOnException,
string debugTag)
=> this.AddFontFromImGuiHeapAllocatedMemory(
(void*)dataPointer,
dataSize,
fontConfig,
freeOnException,
debugTag);
/// <summary>
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
/// <strong>It WILL crash if you try to use a memory pointer allocated in some other way.</strong><br />
/// <strong>Do NOT call <see cref="ImGuiNative.igMemFree"/> on the <paramref name="dataPointer"/> once this
/// function has been called.</strong>
/// </summary>
/// <param name="dataPointer">Memory address for the data allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.</param>
/// <param name="dataSize">The size of the font file..</param>
/// <param name="fontConfig">The font config.</param>
/// <param name="freeOnException">Free <paramref name="dataPointer"/> if an exception happens.</param>
/// <param name="debugTag">A debug tag.</param>
/// <returns>The newly added font.</returns>
unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
void* dataPointer,
int dataSize,
in SafeFontConfig fontConfig,
bool freeOnException,
string debugTag);
/// <summary>
/// Adds a font from a file.
/// </summary>
/// <param name="path">The file path to create a new font from.</param>
/// <param name="fontConfig">The font config.</param>
/// <returns>The newly added font.</returns>
ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig);
/// <summary>
/// Adds a font from a stream.
/// </summary>
/// <param name="stream">The stream to create a new font from.</param>
/// <param name="fontConfig">The font config.</param>
/// <param name="leaveOpen">Dispose when this function returns or throws.</param>
/// <param name="debugTag">A debug tag.</param>
/// <returns>The newly added font.</returns>
ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag);
/// <summary>
/// Adds a font from memory.
/// </summary>
/// <param name="span">The span to create from.</param>
/// <param name="fontConfig">The font config.</param>
/// <param name="debugTag">A debug tag.</param>
/// <returns>The newly added font.</returns>
ImFontPtr AddFontFromMemory(ReadOnlySpan<byte> span, in SafeFontConfig fontConfig, string debugTag);
/// <summary>
/// Adds the default font known to the current font atlas.<br />
/// <br />
/// Includes <see cref="AddFontAwesomeIconFont"/> and <see cref="AttachExtraGlyphsForDalamudLanguage"/>.<br />
/// As this involves adding multiple fonts, calling this function will set <see cref="IFontAtlasBuildToolkit.Font"/>
/// as the return value of this function, if it was empty before.
/// </summary>
/// <param name="sizePx">Font size in pixels.</param>
/// <param name="glyphRanges">The glyph ranges. Use <see cref="FontAtlasBuildToolkitUtilities"/>.ToGlyphRange to build.</param>
/// <returns>A font returned from <see cref="ImFontAtlasPtr.AddFont"/>.</returns>
ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null);
/// <summary>
/// Adds a font that is shipped with Dalamud.<br />
/// <br />
/// Note: if game symbols font file is requested but is unavailable,
/// then it will take the glyphs from game's built-in fonts, and everything in <paramref name="fontConfig"/>
/// will be ignored but <see cref="SafeFontConfig.SizePx"/>, <see cref="SafeFontConfig.MergeFont"/>,
/// and <see cref="SafeFontConfig.GlyphRanges"/>.
/// </summary>
/// <param name="asset">The font type.</param>
/// <param name="fontConfig">The font config.</param>
/// <returns>The added font.</returns>
ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig);
/// <summary>
/// Same with <see cref="AddDalamudAssetFont"/>(<see cref="DalamudAsset.FontAwesomeFreeSolid"/>, ...),
/// but using only FontAwesome icon ranges.<br />
/// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored.
/// </summary>
/// <param name="fontConfig">The font config.</param>
/// <returns>The added font.</returns>
ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig);
/// <summary>
/// Adds the game's symbols into the provided font.<br />
/// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored.<br />
/// If the game symbol font file is unavailable, only <see cref="SafeFontConfig.SizePx"/> will be honored.
/// </summary>
/// <param name="fontConfig">The font config.</param>
/// <returns>The added font.</returns>
ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig);
/// <summary>
/// Adds the game glyphs to the font.
/// </summary>
/// <param name="gameFontStyle">The font style.</param>
/// <param name="glyphRanges">The glyph ranges.</param>
/// <param name="mergeFont">The font to merge to. If empty, then a new font will be created.</param>
/// <returns>The added font.</returns>
ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont);
/// <summary>
/// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.<br />
/// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored.
/// </summary>
/// <param name="fontConfig">The font config.</param>
void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig);
}

View file

@ -1,42 +0,0 @@
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Represents a reference counting handle for fonts.
/// </summary>
public interface IFontHandle : IDisposable
{
/// <summary>
/// Represents a reference counting handle for fonts. Dalamud internal use only.
/// </summary>
internal interface IInternal : IFontHandle
{
/// <summary>
/// Gets the font.<br />
/// Use of this properly is safe only from the UI thread.<br />
/// Use <see cref="IFontHandle.Push"/> if the intended purpose of this property is <see cref="ImGui.PushFont"/>.<br />
/// Futures changes may make simple <see cref="ImGui.PushFont"/> not enough.
/// </summary>
ImFontPtr ImFont { get; }
}
/// <summary>
/// Gets the load exception, if it failed to load. Otherwise, it is null.
/// </summary>
Exception? LoadException { get; }
/// <summary>
/// Gets a value indicating whether this font is ready for use.<br />
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.
/// </summary>
bool Available { get; }
/// <summary>
/// Pushes the current font into ImGui font stack using <see cref="ImGui.PushFont"/>, if available.<br />
/// Use <see cref="ImGui.GetFont"/> to access the current font.<br />
/// You may not access the font once you dispose this object.
/// </summary>
/// <returns>A disposable object that will call <see cref="ImGui.PopFont"/>(1) on dispose.</returns>
IDisposable Push();
}

View file

@ -1,334 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Logging.Internal;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// A font handle representing a user-callback generated font.
/// </summary>
internal class DelegateFontHandle : IFontHandle.IInternal
{
private IFontHandleManager? manager;
/// <summary>
/// Initializes a new instance of the <see cref="DelegateFontHandle"/> class.
/// </summary>
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
/// <param name="callOnBuildStepChange">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange)
{
this.manager = manager;
this.CallOnBuildStepChange = callOnBuildStepChange;
}
/// <summary>
/// Gets the function to be called on build step changes.
/// </summary>
public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; }
/// <inheritdoc/>
public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this);
/// <inheritdoc/>
public bool Available => this.ImFont.IsNotNullAndLoaded();
/// <inheritdoc/>
public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default;
private IFontHandleManager ManagerNotDisposed =>
this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle));
/// <inheritdoc/>
public void Dispose()
{
this.manager?.FreeFontHandle(this);
this.manager = null;
}
/// <inheritdoc/>
public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available);
/// <summary>
/// Manager for <see cref="DelegateFontHandle"/>s.
/// </summary>
internal sealed class HandleManager : IFontHandleManager
{
private readonly HashSet<DelegateFontHandle> handles = new();
private readonly object syncRoot = new();
/// <summary>
/// Initializes a new instance of the <see cref="HandleManager"/> class.
/// </summary>
/// <param name="atlasName">The name of the owner atlas.</param>
public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager";
/// <inheritdoc/>
public event Action? RebuildRecommend;
/// <inheritdoc/>
public string Name { get; }
/// <inheritdoc/>
public IFontHandleSubstance? Substance { get; set; }
/// <inheritdoc/>
public void Dispose()
{
lock (this.syncRoot)
{
this.handles.Clear();
this.Substance?.Dispose();
this.Substance = null;
}
}
/// <inheritdoc cref="IFontAtlas.NewDelegateFontHandle"/>
public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate)
{
var key = new DelegateFontHandle(this, buildStepDelegate);
lock (this.syncRoot)
this.handles.Add(key);
this.RebuildRecommend?.Invoke();
return key;
}
/// <inheritdoc/>
public void FreeFontHandle(IFontHandle handle)
{
if (handle is not DelegateFontHandle cgfh)
return;
lock (this.syncRoot)
this.handles.Remove(cgfh);
}
/// <inheritdoc/>
public IFontHandleSubstance NewSubstance()
{
lock (this.syncRoot)
return new HandleSubstance(this, this.handles.ToArray());
}
}
/// <summary>
/// Substance from <see cref="HandleManager"/>.
/// </summary>
internal sealed class HandleSubstance : IFontHandleSubstance
{
private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}");
// Not owned by this class. Do not dispose.
private readonly DelegateFontHandle[] relevantHandles;
// Owned by this class, but ImFontPtr values still do not belong to this.
private readonly Dictionary<DelegateFontHandle, ImFontPtr> fonts = new();
private readonly Dictionary<DelegateFontHandle, Exception?> buildExceptions = new();
/// <summary>
/// Initializes a new instance of the <see cref="HandleSubstance"/> class.
/// </summary>
/// <param name="manager">The manager.</param>
/// <param name="relevantHandles">The relevant handles.</param>
public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles)
{
this.Manager = manager;
this.relevantHandles = relevantHandles;
}
/// <inheritdoc/>
public IFontHandleManager Manager { get; }
/// <inheritdoc/>
public void Dispose()
{
this.fonts.Clear();
this.buildExceptions.Clear();
}
/// <inheritdoc/>
public ImFontPtr GetFontPtr(IFontHandle handle) =>
handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default;
/// <inheritdoc/>
public Exception? GetBuildException(IFontHandle handle) =>
handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default;
/// <inheritdoc/>
public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{
var fontsVector = toolkitPreBuild.Fonts;
foreach (var k in this.relevantHandles)
{
var fontCountPrevious = fontsVector.Length;
try
{
toolkitPreBuild.Font = default;
k.CallOnBuildStepChange(toolkitPreBuild);
if (toolkitPreBuild.Font.IsNull())
{
if (fontCountPrevious == fontsVector.Length)
{
throw new InvalidOperationException(
$"{nameof(FontAtlasBuildStepDelegate)} must either set the " +
$"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font.");
}
toolkitPreBuild.Font = fontsVector[^1];
}
else
{
var found = false;
unsafe
{
for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++)
{
if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr)
found = true;
}
}
if (!found)
{
throw new InvalidOperationException(
"The font does not exist in the atlas' font array. If you need an empty font, try" +
"adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" +
"glyph range.");
}
}
if (fontsVector.Length - fontCountPrevious != 1)
{
Log.Warning(
"[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " +
"Using the most recently added font. " +
"Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?",
this.Manager.Name,
fontsVector.Length - fontCountPrevious,
nameof(FontAtlasBuildStepDelegate),
nameof(SafeFontConfig),
nameof(SafeFontConfig.MergeFont),
nameof(ImFontConfigPtr),
nameof(ImFontConfigPtr.MergeMode));
}
for (var i = fontCountPrevious; i < fontsVector.Length; i++)
{
if (fontsVector[i].ValidateUnsafe() is { } ex)
{
throw new InvalidOperationException(
"One of the newly added fonts seem to be pointing to an invalid memory address.",
ex);
}
}
// Check for duplicate entries; duplicates will result in free-after-free
for (var i = 0; i < fontCountPrevious; i++)
{
for (var j = fontCountPrevious; j < fontsVector.Length; j++)
{
unsafe
{
if (fontsVector[i].NativePtr == fontsVector[j].NativePtr)
throw new InvalidOperationException("An already added font has been added again.");
}
}
}
this.fonts[k] = toolkitPreBuild.Font;
}
catch (Exception e)
{
this.fonts[k] = default;
this.buildExceptions[k] = e;
Log.Error(
e,
"[{name}:Substance] An error has occurred while during {delegate} PreBuild call.",
this.Manager.Name,
nameof(FontAtlasBuildStepDelegate));
// Sanitization, in a futile attempt to prevent crashes on invalid parameters
unsafe
{
var distinct =
fontsVector
.DistinctBy(x => (nint)x.NativePtr) // Remove duplicates
.Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them
.ToArray();
// We're adding the contents back; do not destroy the contents
fontsVector.Clear(true);
fontsVector.AddRange(distinct.AsSpan());
}
}
}
}
/// <inheritdoc/>
public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{
// irrelevant
}
/// <inheritdoc/>
public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
{
foreach (var k in this.relevantHandles)
{
if (!this.fonts[k].IsNotNullAndLoaded())
continue;
try
{
toolkitPostBuild.Font = this.fonts[k];
k.CallOnBuildStepChange.Invoke(toolkitPostBuild);
}
catch (Exception e)
{
this.fonts[k] = default;
this.buildExceptions[k] = e;
Log.Error(
e,
"[{name}] An error has occurred while during {delegate} PostBuild call.",
this.Manager.Name,
nameof(FontAtlasBuildStepDelegate));
}
}
}
/// <inheritdoc/>
public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion)
{
foreach (var k in this.relevantHandles)
{
if (!this.fonts[k].IsNotNullAndLoaded())
continue;
try
{
toolkitPostPromotion.Font = this.fonts[k];
k.CallOnBuildStepChange.Invoke(toolkitPostPromotion);
}
catch (Exception e)
{
this.fonts[k] = default;
this.buildExceptions[k] = e;
Log.Error(
e,
"[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.",
this.Manager.Name,
nameof(FontAtlasBuildStepDelegate));
}
}
}
}
}

View file

@ -1,682 +0,0 @@
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Unicode;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
using SharpDX.DXGI;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Standalone font atlas.
/// </summary>
internal sealed partial class FontAtlasFactory
{
private static readonly Dictionary<ulong, List<(char Left, char Right, float Distance)>> PairAdjustmentsCache =
new();
/// <summary>
/// Implementations for <see cref="IFontAtlasBuildToolkitPreBuild"/> and
/// <see cref="IFontAtlasBuildToolkitPostBuild"/>.
/// </summary>
private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable
{
private static readonly ushort FontAwesomeIconMin =
(ushort)Enum.GetValues<FontAwesomeIcon>().Where(x => x > 0).Min();
private static readonly ushort FontAwesomeIconMax =
(ushort)Enum.GetValues<FontAwesomeIcon>().Where(x => x > 0).Max();
private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new();
private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance;
private readonly FontAtlasFactory factory;
private readonly FontAtlasBuiltData data;
/// <summary>
/// Initializes a new instance of the <see cref="BuildToolkit"/> class.
/// </summary>
/// <param name="factory">An instance of <see cref="FontAtlasFactory"/>.</param>
/// <param name="data">New atlas.</param>
/// <param name="gameFontHandleSubstance">An instance of <see cref="GamePrebakedFontHandle.HandleSubstance"/>.</param>
/// <param name="isAsync">Specify whether the current build operation is an asynchronous one.</param>
public BuildToolkit(
FontAtlasFactory factory,
FontAtlasBuiltData data,
GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance,
bool isAsync)
{
this.data = data;
this.gameFontHandleSubstance = gameFontHandleSubstance;
this.IsAsyncBuildOperation = isAsync;
this.factory = factory;
}
/// <inheritdoc cref="IFontAtlasBuildToolkit.Font"/>
public ImFontPtr Font { get; set; }
/// <inheritdoc cref="IFontAtlasBuildToolkit.Font"/>
public float Scale => this.data.Scale;
/// <inheritdoc/>
public bool IsAsyncBuildOperation { get; }
/// <inheritdoc/>
public FontAtlasBuildStep BuildStep { get; set; }
/// <inheritdoc/>
public ImFontAtlasPtr NewImAtlas => this.data.Atlas;
/// <inheritdoc/>
public ImVectorWrapper<ImFontPtr> Fonts => this.data.Fonts;
/// <summary>
/// Gets the list of fonts to ignore global scale.
/// </summary>
public List<ImFontPtr> GlobalScaleExclusions { get; } = new();
/// <inheritdoc/>
public void Dispose() => this.disposeAfterBuild.Dispose();
/// <inheritdoc/>
public T2 DisposeAfterBuild<T2>(T2 disposable) where T2 : IDisposable =>
this.disposeAfterBuild.Add(disposable);
/// <inheritdoc/>
public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle);
/// <inheritdoc/>
public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action);
/// <inheritdoc/>
public T DisposeWithAtlas<T>(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable);
/// <inheritdoc/>
public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle);
/// <inheritdoc/>
public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action);
/// <inheritdoc/>
public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr)
{
this.GlobalScaleExclusions.Add(fontPtr);
return fontPtr;
}
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) =>
this.GlobalScaleExclusions.Contains(fontPtr);
/// <inheritdoc/>
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
this.data.AddNewTexture(textureWrap, disposeOnError);
/// <inheritdoc/>
public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
void* dataPointer,
int dataSize,
in SafeFontConfig fontConfig,
bool freeOnException,
string debugTag)
{
Log.Verbose(
"[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}",
this.data.Owner?.Name ?? "(error)",
(nint)this.NewImAtlas.NativePtr,
nameof(this.AddFontFromImGuiHeapAllocatedMemory),
(nint)dataPointer,
dataSize,
debugTag);
try
{
fontConfig.ThrowOnInvalidValues();
var raw = fontConfig.Raw with
{
FontData = dataPointer,
FontDataSize = dataSize,
};
if (fontConfig.GlyphRanges is not { Length: > 0 } ranges)
ranges = new ushort[] { 1, 0xFFFE, 0 };
raw.GlyphRanges = (ushort*)this.DisposeAfterBuild(
GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject();
TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw);
var font = this.NewImAtlas.AddFont(&raw);
var dataHash = default(HashCode);
dataHash.AddBytes(new(dataPointer, dataSize));
var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32);
List<(char Left, char Right, float Distance)> pairAdjustments;
lock (PairAdjustmentsCache)
{
if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments))
{
PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new());
try
{
pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray());
}
catch
{
// don't care
}
}
}
foreach (var pair in pairAdjustments)
{
if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges))
continue;
if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges))
continue;
font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels);
}
return font;
}
catch
{
if (freeOnException)
ImGuiNative.igMemFree(dataPointer);
throw;
}
}
/// <inheritdoc/>
public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig)
{
return this.AddFontFromStream(
File.OpenRead(path),
fontConfig,
false,
$"{nameof(this.AddFontFromFile)}({path})");
}
/// <inheritdoc/>
public unsafe ImFontPtr AddFontFromStream(
Stream stream,
in SafeFontConfig fontConfig,
bool leaveOpen,
string debugTag)
{
using var streamCloser = leaveOpen ? null : stream;
if (!stream.CanSeek)
{
// There is no need to dispose a MemoryStream.
var ms = new MemoryStream();
stream.CopyTo(ms);
stream = ms;
}
var length = checked((int)(uint)stream.Length);
var memory = ImGuiHelpers.AllocateMemory(length);
try
{
stream.ReadExactly(new(memory, length));
return this.AddFontFromImGuiHeapAllocatedMemory(
memory,
length,
fontConfig,
false,
$"{nameof(this.AddFontFromStream)}({debugTag})");
}
catch
{
ImGuiNative.igMemFree(memory);
throw;
}
}
/// <inheritdoc/>
public unsafe ImFontPtr AddFontFromMemory(
ReadOnlySpan<byte> span,
in SafeFontConfig fontConfig,
string debugTag)
{
var length = span.Length;
var memory = ImGuiHelpers.AllocateMemory(length);
try
{
span.CopyTo(new(memory, length));
return this.AddFontFromImGuiHeapAllocatedMemory(
memory,
length,
fontConfig,
false,
$"{nameof(this.AddFontFromMemory)}({debugTag})");
}
catch
{
ImGuiNative.igMemFree(memory);
throw;
}
}
/// <inheritdoc/>
public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges)
{
ImFontPtr font;
glyphRanges ??= this.factory.DefaultGlyphRanges;
if (this.factory.UseAxis)
{
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
}
else
{
font = this.AddDalamudAssetFont(
DalamudAsset.NotoSansJpMedium,
new() { SizePx = sizePx, GlyphRanges = glyphRanges });
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
}
this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font });
if (this.Font.IsNull())
this.Font = font;
return font;
}
/// <inheritdoc/>
public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig)
{
if (asset.GetPurpose() != DalamudAssetPurpose.Font)
throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font.");
switch (asset)
{
case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile:
return this.factory.AddFont(
this,
asset,
fontConfig with
{
FontNo = 0,
SizePx = (fontConfig.SizePx * 3) / 2,
});
case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile:
{
return this.AddGameGlyphs(
new(GameFontFamily.Axis, fontConfig.SizePx),
fontConfig.GlyphRanges,
fontConfig.MergeFont);
}
default:
return this.factory.AddFont(
this,
asset,
fontConfig with
{
FontNo = 0,
});
}
}
/// <inheritdoc/>
public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont(
DalamudAsset.FontAwesomeFreeSolid,
fontConfig with
{
GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 },
});
/// <inheritdoc/>
public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) =>
this.AddDalamudAssetFont(
DalamudAsset.LodestoneGameSymbol,
fontConfig with
{
GlyphRanges = new ushort[]
{
GamePrebakedFontHandle.SeIconCharMin,
GamePrebakedFontHandle.SeIconCharMax,
0,
},
});
/// <inheritdoc/>
public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) =>
this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges);
/// <inheritdoc/>
public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig)
{
var dalamudConfiguration = Service<DalamudConfiguration>.Get();
if (dalamudConfiguration.EffectiveLanguage == "ko"
|| Service<DalamudIme>.GetNullable()?.EncounteredHangul is true)
{
this.AddDalamudAssetFont(
DalamudAsset.NotoSansKrRegular,
fontConfig with
{
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
UnicodeRanges.HangulJamo,
UnicodeRanges.HangulCompatibilityJamo,
UnicodeRanges.HangulSyllables,
UnicodeRanges.HangulJamoExtendedA,
UnicodeRanges.HangulJamoExtendedB),
});
}
var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc");
if (!File.Exists(fontPathChs))
fontPathChs = null;
var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc");
if (!File.Exists(fontPathCht))
fontPathCht = null;
if (fontPathCht != null && Service<DalamudConfiguration>.Get().EffectiveLanguage == "tw")
{
this.AddFontFromFile(fontPathCht, fontConfig with
{
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkUnifiedIdeographsExtensionA),
});
}
else if (fontPathChs != null && (Service<DalamudConfiguration>.Get().EffectiveLanguage == "zh"
|| Service<DalamudIme>.GetNullable()?.EncounteredHan is true))
{
this.AddFontFromFile(fontPathChs, fontConfig with
{
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkUnifiedIdeographsExtensionA),
});
}
}
public void PreBuildSubstances()
{
foreach (var substance in this.data.Substances)
substance.OnPreBuild(this);
foreach (var substance in this.data.Substances)
substance.OnPreBuildCleanup(this);
}
public unsafe void PreBuild()
{
var configData = this.data.ConfigData;
foreach (ref var config in configData.DataSpan)
{
if (this.GlobalScaleExclusions.Contains(new(config.DstFont)))
continue;
config.SizePixels *= this.Scale;
config.GlyphMaxAdvanceX *= this.Scale;
if (float.IsInfinity(config.GlyphMaxAdvanceX))
config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
config.GlyphMinAdvanceX *= this.Scale;
if (float.IsInfinity(config.GlyphMinAdvanceX))
config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
config.GlyphOffset *= this.Scale;
}
}
public void DoBuild()
{
// ImGui will call AddFontDefault() on Build() call.
// AddFontDefault() will reliably crash, when invoked multithreaded.
// We add a dummy font to prevent that.
if (this.data.ConfigData.Length == 0)
{
this.AddDalamudAssetFont(
DalamudAsset.NotoSansJpMedium,
new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 });
}
if (!this.NewImAtlas.Build())
throw new InvalidOperationException("ImFontAtlas.Build failed");
this.BuildStep = FontAtlasBuildStep.PostBuild;
}
public unsafe void PostBuild()
{
var scale = this.Scale;
foreach (ref var font in this.Fonts.DataSpan)
{
if (!this.GlobalScaleExclusions.Contains(font))
font.AdjustGlyphMetrics(1 / scale, 1 / scale);
foreach (var c in FallbackCodepoints)
{
var g = font.FindGlyphNoFallback(c);
if (g.NativePtr == null)
continue;
font.UpdateFallbackChar(c);
break;
}
foreach (var c in EllipsisCodepoints)
{
var g = font.FindGlyphNoFallback(c);
if (g.NativePtr == null)
continue;
font.EllipsisChar = c;
break;
}
}
}
public void PostBuildSubstances()
{
foreach (var substance in this.data.Substances)
substance.OnPostBuild(this);
}
public unsafe void UploadTextures()
{
var buf = Array.Empty<byte>();
try
{
var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm);
var bpp = use4 ? 2 : 4;
var width = this.NewImAtlas.TexWidth;
var height = this.NewImAtlas.TexHeight;
foreach (ref var texture in this.data.ImTextures.DataSpan)
{
if (texture.TexID != 0)
{
// Nothing to do
}
else if (texture.TexPixelsRGBA32 is not null)
{
var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat(
new(texture.TexPixelsRGBA32, width * height * 4),
width * 4,
width,
height,
use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm);
this.data.AddExistingTexture(wrap);
texture.TexID = wrap.ImGuiHandle;
}
else if (texture.TexPixelsAlpha8 is not null)
{
var numPixels = width * height;
if (buf.Length < numPixels * bpp)
{
ArrayPool<byte>.Shared.Return(buf);
buf = ArrayPool<byte>.Shared.Rent(numPixels * bpp);
}
fixed (void* pBuf = buf)
{
var sourcePtr = texture.TexPixelsAlpha8;
if (use4)
{
var target = (ushort*)pBuf;
while (numPixels-- > 0)
{
*target = (ushort)((*sourcePtr << 8) | 0x0FFF);
target++;
sourcePtr++;
}
}
else
{
var target = (uint*)pBuf;
while (numPixels-- > 0)
{
*target = (uint)((*sourcePtr << 24) | 0x00FFFFFF);
target++;
sourcePtr++;
}
}
}
var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat(
buf,
width * bpp,
width,
height,
use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm);
this.data.AddExistingTexture(wrap);
texture.TexID = wrap.ImGuiHandle;
continue;
}
else
{
Log.Warning(
"[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null",
this.data.Owner?.Name ?? "(error)");
}
if (texture.TexPixelsRGBA32 is not null)
ImGuiNative.igMemFree(texture.TexPixelsRGBA32);
if (texture.TexPixelsAlpha8 is not null)
ImGuiNative.igMemFree(texture.TexPixelsAlpha8);
texture.TexPixelsRGBA32 = null;
texture.TexPixelsAlpha8 = null;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
}
/// <summary>
/// Implementations for <see cref="IFontAtlasBuildToolkitPostPromotion"/>.
/// </summary>
private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion
{
private readonly FontAtlasBuiltData builtData;
/// <summary>
/// Initializes a new instance of the <see cref="BuildToolkitPostPromotion"/> class.
/// </summary>
/// <param name="builtData">The built data.</param>
public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData;
/// <inheritdoc/>
public ImFontPtr Font { get; set; }
/// <inheritdoc/>
public float Scale => this.builtData.Scale;
/// <inheritdoc/>
public bool IsAsyncBuildOperation => true;
/// <inheritdoc/>
public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion;
/// <inheritdoc/>
public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas;
/// <inheritdoc/>
public unsafe ImVectorWrapper<ImFontPtr> Fonts => new(
&this.NewImAtlas.NativePtr->Fonts,
x => ImGuiNative.ImFont_destroy(x->NativePtr));
/// <inheritdoc/>
public T DisposeWithAtlas<T>(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable);
/// <inheritdoc/>
public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle);
/// <inheritdoc/>
public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action);
/// <inheritdoc/>
public unsafe void CopyGlyphsAcrossFonts(
ImFontPtr source,
ImFontPtr target,
bool missingOnly,
bool rebuildLookupTable = true,
char rangeLow = ' ',
char rangeHigh = '\uFFFE')
{
var sourceFound = false;
var targetFound = false;
foreach (var f in this.Fonts)
{
sourceFound |= f.NativePtr == source.NativePtr;
targetFound |= f.NativePtr == target.NativePtr;
}
if (sourceFound && targetFound)
{
ImGuiHelpers.CopyGlyphsAcrossFonts(
source,
target,
missingOnly,
false,
rangeLow,
rangeHigh);
if (rebuildLookupTable)
this.BuildLookupTable(target);
}
}
/// <inheritdoc/>
public unsafe void BuildLookupTable(ImFontPtr font)
{
// Need to clear previous Fallback pointers before BuildLookupTable, or it may crash
font.NativePtr->FallbackGlyph = null;
font.NativePtr->FallbackHotData = null;
font.BuildLookupTable();
// Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking
// Codepoint < FallbackHotData.size always means that it's not fallback char.
// Otherwise, having a fallback character in ImGui.InputText gets strange.
var indexedHotData = font.IndexedHotDataWrapped();
var indexLookup = font.IndexLookupWrapped();
ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData;
for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++)
{
if (indexLookup[codepoint] == ushort.MaxValue)
{
indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX;
indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth;
}
}
}
}
}

View file

@ -1,726 +0,0 @@
// #define VeryVerboseLog
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using ImGuiNET;
using JetBrains.Annotations;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Standalone font atlas.
/// </summary>
internal sealed partial class FontAtlasFactory
{
/// <summary>
/// Fallback codepoints for ImFont.
/// </summary>
public const string FallbackCodepoints = "\u3013\uFFFD?-";
/// <summary>
/// Ellipsis codepoints for ImFont.
/// </summary>
public const string EllipsisCodepoints = "\u2026\u0085";
/// <summary>
/// If set, disables concurrent font build operation.
/// </summary>
private static readonly object? NoConcurrentBuildOperationLock = null; // new();
private static readonly ModuleLog Log = new(nameof(FontAtlasFactory));
private static readonly Task<FontAtlasBuiltData> EmptyTask = Task.FromResult(default(FontAtlasBuiltData));
private struct FontAtlasBuiltData : IDisposable
{
public readonly DalamudFontAtlas? Owner;
public readonly ImFontAtlasPtr Atlas;
public readonly float Scale;
public bool IsBuildInProgress;
private readonly List<IDalamudTextureWrap>? wraps;
private readonly List<IFontHandleSubstance>? substances;
private readonly DisposeSafety.ScopedFinalizer? garbage;
public unsafe FontAtlasBuiltData(
DalamudFontAtlas owner,
IEnumerable<IFontHandleSubstance> substances,
float scale)
{
this.Owner = owner;
this.Scale = scale;
this.garbage = new();
try
{
var substancesList = this.substances = new();
foreach (var s in substances)
substancesList.Add(this.garbage.Add(s));
this.garbage.Add(() => substancesList.Clear());
var wrapsCopy = this.wraps = new();
this.garbage.Add(() => wrapsCopy.Clear());
var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas();
this.Atlas = atlasPtr;
if (this.Atlas.NativePtr is null)
throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}.");
this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr));
this.IsBuildInProgress = true;
}
catch
{
this.garbage.Dispose();
throw;
}
}
public readonly DisposeSafety.ScopedFinalizer Garbage =>
this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
public readonly ImVectorWrapper<ImFontPtr> Fonts => this.Atlas.FontsWrapped();
public readonly ImVectorWrapper<ImFontConfig> ConfigData => this.Atlas.ConfigDataWrapped();
public readonly ImVectorWrapper<ImFontAtlasTexture> ImTextures => this.Atlas.TexturesWrapped();
public readonly IReadOnlyList<IDalamudTextureWrap> Wraps =>
(IReadOnlyList<IDalamudTextureWrap>?)this.wraps ?? Array.Empty<IDalamudTextureWrap>();
public readonly IReadOnlyList<IFontHandleSubstance> Substances =>
(IReadOnlyList<IFontHandleSubstance>?)this.substances ?? Array.Empty<IFontHandleSubstance>();
public readonly void AddExistingTexture(IDalamudTextureWrap wrap)
{
if (this.wraps is null)
throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
this.wraps.Add(this.Garbage.Add(wrap));
}
public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError)
{
if (this.wraps is null)
throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
var handle = wrap.ImGuiHandle;
var index = this.ImTextures.IndexOf(x => x.TexID == handle);
if (index == -1)
{
try
{
this.wraps.EnsureCapacity(this.wraps.Count + 1);
this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1);
index = this.ImTextures.Length;
this.wraps.Add(this.Garbage.Add(wrap));
this.ImTextures.Add(new() { TexID = handle });
}
catch (Exception e)
{
if (disposeOnError)
wrap.Dispose();
if (this.wraps.Count != this.ImTextures.Length)
{
Log.Error(
e,
"{name} failed, and {wraps} and {imtextures} have different number of items",
nameof(this.AddNewTexture),
nameof(this.Wraps),
nameof(this.ImTextures));
if (this.wraps.Count > 0 && this.wraps[^1] == wrap)
this.wraps.RemoveAt(this.wraps.Count - 1);
if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle)
this.ImTextures.RemoveAt(this.ImTextures.Length - 1);
if (this.wraps.Count != this.ImTextures.Length)
Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash");
}
throw;
}
}
return index;
}
public unsafe void Dispose()
{
if (this.garbage is null)
return;
if (this.IsBuildInProgress)
{
Log.Error(
"[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" +
"Stack:\n{trace}",
this.Owner?.Name ?? "<?>",
(nint)this.Atlas.NativePtr,
new StackTrace());
while (this.IsBuildInProgress)
Thread.Sleep(100);
}
#if VeryVerboseLog
Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "<?>", (nint)this.Atlas.NativePtr);
#endif
this.garbage.Dispose();
}
public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync)
{
var axisSubstance = this.Substances.OfType<GamePrebakedFontHandle.HandleSubstance>().Single();
return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild };
}
}
private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback
{
private readonly DisposeSafety.ScopedFinalizer disposables = new();
private readonly FontAtlasFactory factory;
private readonly DelegateFontHandle.HandleManager delegateFontHandleManager;
private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager;
private readonly IFontHandleManager[] fontHandleManagers;
private readonly object syncRootPostPromotion = new();
private readonly object syncRoot = new();
private Task<FontAtlasBuiltData> buildTask = EmptyTask;
private FontAtlasBuiltData builtData;
private int buildSuppressionCounter;
private bool buildSuppressionSuppressed;
private int buildIndex;
private bool buildQueued;
private bool disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="DalamudFontAtlas"/> class.
/// </summary>
/// <param name="factory">The factory.</param>
/// <param name="atlasName">Name of atlas, for debugging and logging purposes.</param>
/// <param name="autoRebuildMode">Specify how to auto rebuild.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas are under the effect of global scale.</param>
public DalamudFontAtlas(
FontAtlasFactory factory,
string atlasName,
FontAtlasAutoRebuildMode autoRebuildMode,
bool isGlobalScaled)
{
this.IsGlobalScaled = isGlobalScaled;
try
{
this.factory = factory;
this.AutoRebuildMode = autoRebuildMode;
this.Name = atlasName;
this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend;
this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend);
this.fontHandleManagers = new IFontHandleManager[]
{
this.delegateFontHandleManager = this.disposables.Add(
new DelegateFontHandle.HandleManager(atlasName)),
this.gameFontHandleManager = this.disposables.Add(
new GamePrebakedFontHandle.HandleManager(atlasName, factory)),
};
foreach (var fhm in this.fontHandleManagers)
fhm.RebuildRecommend += this.OnRebuildRecommend;
}
catch
{
this.disposables.Dispose();
throw;
}
this.factory.SceneTask.ContinueWith(
r =>
{
lock (this.syncRoot)
{
if (this.disposed)
return;
r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame;
this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame);
}
if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame)
this.BuildFontsOnNextFrame();
});
}
/// <summary>
/// Finalizes an instance of the <see cref="DalamudFontAtlas"/> class.
/// </summary>
~DalamudFontAtlas()
{
lock (this.syncRoot)
{
this.buildTask.ToDisposableIgnoreExceptions().Dispose();
this.builtData.Dispose();
}
}
/// <inheritdoc/>
public event FontAtlasBuildStepDelegate? BuildStepChange;
/// <inheritdoc/>
public event Action? RebuildRecommend;
/// <inheritdoc/>
public event Action<DisposeSafety.IDisposeCallback>? BeforeDispose;
/// <inheritdoc/>
public event Action<DisposeSafety.IDisposeCallback, Exception?>? AfterDispose;
/// <inheritdoc/>
public string Name { get; }
/// <inheritdoc/>
public FontAtlasAutoRebuildMode AutoRebuildMode { get; }
/// <inheritdoc/>
public ImFontAtlasPtr ImAtlas
{
get
{
lock (this.syncRoot)
return this.builtData.Atlas;
}
}
/// <inheritdoc/>
public Task BuildTask => this.buildTask;
/// <inheritdoc/>
public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull();
/// <inheritdoc/>
public bool IsGlobalScaled { get; }
/// <inheritdoc/>
public void Dispose()
{
if (this.disposed)
return;
this.BeforeDispose?.InvokeSafely(this);
try
{
lock (this.syncRoot)
{
this.disposed = true;
this.buildTask.ToDisposableIgnoreExceptions().Dispose();
this.buildTask = EmptyTask;
this.disposables.Add(this.builtData);
this.builtData = default;
this.disposables.Dispose();
}
try
{
this.AfterDispose?.Invoke(this, null);
}
catch
{
// ignore
}
}
catch (Exception e)
{
try
{
this.AfterDispose?.Invoke(this, e);
}
catch
{
// ignore
}
}
GC.SuppressFinalize(this);
}
/// <inheritdoc/>
public IDisposable SuppressAutoRebuild()
{
this.buildSuppressionCounter++;
return Disposable.Create(
() =>
{
this.buildSuppressionCounter--;
if (this.buildSuppressionSuppressed)
this.OnRebuildRecommend();
});
}
/// <inheritdoc/>
public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style);
/// <inheritdoc/>
public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) =>
this.delegateFontHandleManager.NewFontHandle(buildStepDelegate);
/// <inheritdoc/>
public void BuildFontsOnNextFrame()
{
if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async)
{
throw new InvalidOperationException(
$"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " +
$"{nameof(this.AutoRebuildMode)} is set to " +
$"{nameof(FontAtlasAutoRebuildMode.Async)}.");
}
if (!this.buildTask.IsCompleted || this.buildQueued)
return;
#if VeryVerboseLog
Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame));
#endif
this.buildQueued = true;
}
/// <inheritdoc/>
public void BuildFontsImmediately()
{
#if VeryVerboseLog
Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately));
#endif
if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async)
{
throw new InvalidOperationException(
$"{nameof(this.BuildFontsImmediately)} cannot be used when " +
$"{nameof(this.AutoRebuildMode)} is set to " +
$"{nameof(FontAtlasAutoRebuildMode.Async)}.");
}
var tcs = new TaskCompletionSource<FontAtlasBuiltData>();
int rebuildIndex;
try
{
rebuildIndex = ++this.buildIndex;
lock (this.syncRoot)
{
if (!this.buildTask.IsCompleted)
throw new InvalidOperationException("Font rebuild is already in progress.");
this.buildTask = tcs.Task;
}
#if VeryVerboseLog
Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately));
#endif
var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f;
var r = this.RebuildFontsPrivate(false, scale);
r.Wait();
if (r.IsCompletedSuccessfully)
tcs.SetResult(r.Result);
else if (r.Exception is not null)
tcs.SetException(r.Exception);
else
tcs.SetCanceled();
}
catch (Exception e)
{
tcs.SetException(e);
Log.Error(e, "[{name}] Failed to build fonts.", this.Name);
throw;
}
this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately));
}
/// <inheritdoc/>
public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true)
{
#if VeryVerboseLog
Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync));
#endif
if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame)
{
throw new InvalidOperationException(
$"{nameof(this.BuildFontsAsync)} cannot be used when " +
$"{nameof(this.AutoRebuildMode)} is set to " +
$"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}.");
}
lock (this.syncRoot)
{
var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f;
var rebuildIndex = ++this.buildIndex;
return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap();
async Task<FontAtlasBuiltData> BuildInner(Task<FontAtlasBuiltData> unused)
{
Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync));
lock (this.syncRoot)
{
if (this.buildIndex != rebuildIndex)
return default;
}
var res = await this.RebuildFontsPrivate(true, scale);
if (res.Atlas.IsNull())
return res;
if (callPostPromotionOnMainThread)
{
await this.factory.Framework.RunOnFrameworkThread(
() => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)));
}
else
{
this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync));
}
return res;
}
}
}
private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source)
{
lock (this.syncRoot)
{
if (this.buildIndex != rebuildIndex)
{
data.ExplicitDisposeIgnoreExceptions();
return;
}
this.builtData.ExplicitDisposeIgnoreExceptions();
this.builtData = data;
this.buildTask = EmptyTask;
foreach (var substance in data.Substances)
substance.Manager.Substance = substance;
}
lock (this.syncRootPostPromotion)
{
if (this.buildIndex != rebuildIndex)
{
data.ExplicitDisposeIgnoreExceptions();
return;
}
var toolkit = new BuildToolkitPostPromotion(data);
try
{
this.BuildStepChange?.Invoke(toolkit);
}
catch (Exception e)
{
Log.Error(
e,
"[{name}] {delegateName} PostPromotion error",
this.Name,
nameof(FontAtlasBuildStepDelegate));
}
foreach (var substance in data.Substances)
{
try
{
substance.OnPostPromotion(toolkit);
}
catch (Exception e)
{
Log.Error(
e,
"[{name}] {substance} PostPromotion error",
this.Name,
substance.GetType().FullName ?? substance.GetType().Name);
}
}
foreach (var font in toolkit.Fonts)
{
try
{
toolkit.BuildLookupTable(font);
}
catch (Exception e)
{
Log.Error(e, "[{name}] BuildLookupTable error", this.Name);
}
}
#if VeryVerboseLog
Log.Verbose("[{name}] Built from {source}.", this.Name, source);
#endif
}
}
private void ImGuiSceneOnNewRenderFrame()
{
if (!this.buildQueued)
return;
try
{
if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async)
this.BuildFontsImmediately();
}
finally
{
this.buildQueued = false;
}
}
private Task<FontAtlasBuiltData> RebuildFontsPrivate(bool isAsync, float scale)
{
if (NoConcurrentBuildOperationLock is null)
return this.RebuildFontsPrivateReal(isAsync, scale);
lock (NoConcurrentBuildOperationLock)
return this.RebuildFontsPrivateReal(isAsync, scale);
}
private async Task<FontAtlasBuiltData> RebuildFontsPrivateReal(bool isAsync, float scale)
{
lock (this.syncRoot)
{
// this lock ensures that this.buildTask is properly set.
}
var sw = new Stopwatch();
sw.Start();
var res = default(FontAtlasBuiltData);
nint atlasPtr = 0;
try
{
res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale);
unsafe
{
atlasPtr = (nint)res.Atlas.NativePtr;
}
Log.Verbose(
"[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)",
this.Name,
nameof(this.RebuildFontsPrivateReal),
atlasPtr,
sw.ElapsedMilliseconds);
using var toolkit = res.CreateToolkit(this.factory, isAsync);
this.BuildStepChange?.Invoke(toolkit);
toolkit.PreBuildSubstances();
toolkit.PreBuild();
#if VeryVerboseLog
Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds);
#endif
toolkit.DoBuild();
#if VeryVerboseLog
Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds);
#endif
toolkit.PostBuild();
toolkit.PostBuildSubstances();
this.BuildStepChange?.Invoke(toolkit);
if (this.factory.SceneTask is { IsCompleted: false } sceneTask)
{
Log.Verbose(
"[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)",
this.Name,
nameof(this.RebuildFontsPrivateReal),
atlasPtr,
sw.ElapsedMilliseconds);
await sceneTask.ConfigureAwait(!isAsync);
}
#if VeryVerboseLog
Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds);
#endif
toolkit.UploadTextures();
Log.Verbose(
"[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)",
this.Name,
nameof(this.RebuildFontsPrivateReal),
atlasPtr,
sw.ElapsedMilliseconds);
res.IsBuildInProgress = false;
return res;
}
catch (Exception e)
{
Log.Error(
e,
"[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)",
this.Name,
nameof(this.RebuildFontsPrivateReal),
atlasPtr,
sw.ElapsedMilliseconds);
res.IsBuildInProgress = false;
res.Dispose();
throw;
}
finally
{
this.buildQueued = false;
}
}
private void OnRebuildRecommend()
{
if (this.disposed)
return;
if (this.buildSuppressionCounter > 0)
{
this.buildSuppressionSuppressed = true;
return;
}
this.buildSuppressionSuppressed = false;
this.factory.Framework.RunOnFrameworkThread(
() =>
{
this.RebuildRecommend?.InvokeSafely();
switch (this.AutoRebuildMode)
{
case FontAtlasAutoRebuildMode.Async:
_ = this.BuildFontsAsync();
break;
case FontAtlasAutoRebuildMode.OnNewFrame:
this.BuildFontsOnNextFrame();
break;
case FontAtlasAutoRebuildMode.Disable:
default:
break;
}
});
}
}
}

View file

@ -1,368 +0,0 @@
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
using ImGuiScene;
using Lumina.Data.Files;
using SharpDX;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Factory for the implementation of <see cref="IFontAtlas"/>.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal sealed partial class FontAtlasFactory
: IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable
{
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
private readonly CancellationTokenSource cancellationTokenSource = new();
private readonly IReadOnlyDictionary<GameFontFamilyAndSize, Task<byte[]>> fdtFiles;
private readonly IReadOnlyDictionary<string, Task<Task<TexFile>[]>> texFiles;
private readonly IReadOnlyDictionary<string, Task<IDalamudTextureWrap?[]>> prebakedTextureWraps;
private readonly Task<ushort[]> defaultGlyphRanges;
private readonly DalamudAssetManager dalamudAssetManager;
[ServiceManager.ServiceConstructor]
private FontAtlasFactory(
DataManager dataManager,
Framework framework,
InterfaceManager interfaceManager,
DalamudAssetManager dalamudAssetManager)
{
this.Framework = framework;
this.InterfaceManager = interfaceManager;
this.dalamudAssetManager = dalamudAssetManager;
this.SceneTask = Service<InterfaceManager.InterfaceManagerWithScene>
.GetAsync()
.ContinueWith(r => r.Result.Manager.Scene);
var gffasInfo = Enum.GetValues<GameFontFamilyAndSize>()
.Select(
x =>
(
Font: x,
Attr: x.GetAttribute<GameFontFamilyAndSizeAttribute>()))
.Where(x => x.Attr is not null)
.ToArray();
var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray();
this.fdtFiles = gffasInfo.ToImmutableDictionary(
x => x.Font,
x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data));
var channelCountsTask = texPaths.ToImmutableDictionary(
x => x,
x => Task.WhenAll(
gffasInfo.Where(y => y.Attr.TexPathFormat == x)
.Select(y => this.fdtFiles[y.Font]))
.ContinueWith(
files => 1 + files.Result.Max(
file =>
{
unsafe
{
using var pin = file.AsMemory().Pin();
var fdt = new FdtFileView(pin.Pointer, file.Length);
return fdt.MaxTextureIndex;
}
})));
this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary(
x => x.Key,
x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result]));
this.texFiles = channelCountsTask.ToImmutableDictionary(
x => x.Key,
x => x.Value.ContinueWith(
y => Enumerable
.Range(1, 1 + ((y.Result - 1) / 4))
.Select(z => Task.Run(() => dataManager.GetFile<TexFile>(string.Format(x.Key, z))!))
.ToArray()));
this.defaultGlyphRanges =
this.fdtFiles[GameFontFamilyAndSize.Axis12]
.ContinueWith(
file =>
{
unsafe
{
using var pin = file.Result.AsMemory().Pin();
var fdt = new FdtFileView(pin.Pointer, file.Result.Length);
return fdt.ToGlyphRanges();
}
});
}
/// <summary>
/// Gets or sets a value indicating whether to override configuration for UseAxis.
/// </summary>
public bool? UseAxisOverride { get; set; } = null;
/// <summary>
/// Gets a value indicating whether to use AXIS fonts.
/// </summary>
public bool UseAxis => this.UseAxisOverride ?? Service<DalamudConfiguration>.Get().UseAxisFontsFromGame;
/// <summary>
/// Gets the service instance of <see cref="Framework"/>.
/// </summary>
public Framework Framework { get; }
/// <summary>
/// Gets the service instance of <see cref="InterfaceManager"/>.<br />
/// <see cref="Internal.InterfaceManager.Scene"/> may not yet be available.
/// </summary>
public InterfaceManager InterfaceManager { get; }
/// <summary>
/// Gets the async task for <see cref="RawDX11Scene"/> inside <see cref="InterfaceManager"/>.
/// </summary>
public Task<RawDX11Scene> SceneTask { get; }
/// <summary>
/// Gets the default glyph ranges (glyph ranges of <see cref="GameFontFamilyAndSize.Axis12"/>).
/// </summary>
public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges);
/// <summary>
/// Gets a value indicating whether game symbol font file is available.
/// </summary>
public bool HasGameSymbolsFontFile =>
this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol);
/// <inheritdoc/>
public void Dispose()
{
this.cancellationTokenSource.Cancel();
this.scopedFinalizer.Dispose();
this.cancellationTokenSource.Dispose();
}
/// <summary>
/// Creates a new instance of a class that implements the <see cref="IFontAtlas"/> interface.
/// </summary>
/// <param name="atlasName">Name of atlas, for debugging and logging purposes.</param>
/// <param name="autoRebuildMode">Specify how to auto rebuild.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
/// <returns>The new font atlas.</returns>
public IFontAtlas CreateFontAtlas(
string atlasName,
FontAtlasAutoRebuildMode autoRebuildMode,
bool isGlobalScaled = true) =>
new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled);
/// <summary>
/// Adds the font from Dalamud Assets.
/// </summary>
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
/// <param name="asset">The font.</param>
/// <param name="fontConfig">The font config.</param>
/// <returns>The address and size.</returns>
public ImFontPtr AddFont(
IFontAtlasBuildToolkitPreBuild toolkitPreBuild,
DalamudAsset asset,
in SafeFontConfig fontConfig) =>
toolkitPreBuild.AddFontFromStream(
this.dalamudAssetManager.CreateStream(asset),
fontConfig,
false,
$"Asset({asset})");
/// <summary>
/// Gets the <see cref="FdtReader"/> for the <see cref="GameFontFamilyAndSize"/>.
/// </summary>
/// <param name="gffas">The font family and size.</param>
/// <returns>The <see cref="FdtReader"/>.</returns>
public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]));
/// <inheritdoc/>
public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView)
{
var arr = ExtractResult(this.fdtFiles[gffas]);
var handle = arr.AsMemory().Pin();
try
{
fdtFileView = new(handle.Pointer, arr.Length);
return handle;
}
catch
{
handle.Dispose();
throw;
}
}
/// <inheritdoc/>
public int GetFontTextureCount(string texPathFormat) =>
ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length;
/// <inheritdoc/>
public TexFile GetTexFile(string texPathFormat, int index) =>
ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]);
/// <inheritdoc/>
public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex)
{
lock (this.prebakedTextureWraps[texPathFormat])
{
var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]);
var fileIndex = textureIndex / 4;
var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4];
wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex);
return CloneTextureWrap(wraps[textureIndex]);
}
}
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
private static unsafe void ExtractChannelFromB8G8R8A8(
Span<byte> target,
ReadOnlySpan<byte> source,
int channelIndex,
bool targetIsB4G4R4A4)
{
var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4));
fixed (byte* sourcePtrImmutable = source)
{
var rptr = sourcePtrImmutable + channelIndex;
fixed (void* targetPtr = target)
{
if (targetIsB4G4R4A4)
{
var wptr = (ushort*)targetPtr;
while (numPixels-- > 0)
{
*wptr = (ushort)((*rptr << 8) | 0x0FFF);
wptr++;
rptr += 4;
}
}
else
{
var wptr = (uint*)targetPtr;
while (numPixels-- > 0)
{
*wptr = (uint)((*rptr << 24) | 0x00FFFFFF);
wptr++;
rptr += 4;
}
}
}
}
}
/// <summary>
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
/// texture behind.
/// </summary>
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
{
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
using var res = srv.Resource;
using var tex2D = res.QueryInterface<Texture2D>();
var description = tex2D.Description;
return new DalamudTextureWrap(
new D3DTextureWrap(
srv.QueryInterface<ShaderResourceView>(),
description.Width,
description.Height));
}
private static unsafe void ExtractChannelFromB4G4R4A4(
Span<byte> target,
ReadOnlySpan<byte> source,
int channelIndex,
bool targetIsB4G4R4A4)
{
var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4));
fixed (byte* sourcePtrImmutable = source)
{
var rptr = sourcePtrImmutable + (channelIndex / 2);
var rshift = (channelIndex & 1) == 0 ? 0 : 4;
fixed (void* targetPtr = target)
{
if (targetIsB4G4R4A4)
{
var wptr = (ushort*)targetPtr;
while (numPixels-- > 0)
{
*wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF);
wptr++;
rptr += 2;
}
}
else
{
var wptr = (uint*)targetPtr;
while (numPixels-- > 0)
{
var v = (*rptr >> rshift) & 0xF;
v |= v << 4;
*wptr = (uint)((v << 24) | 0x00FFFFFF);
wptr++;
rptr += 4;
}
}
}
}
}
private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex)
{
var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]);
var numPixels = texFile.Header.Width * texFile.Header.Height;
_ = Service<InterfaceManager.InterfaceManagerWithScene>.Get();
var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm);
var bpp = targetIsB4G4R4A4 ? 2 : 4;
var buffer = ArrayPool<byte>.Shared.Rent(numPixels * bpp);
try
{
var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _);
switch (texFile.Header.Format)
{
case TexFile.TextureFormat.B4G4R4A4:
// Game ships with this format.
ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4);
break;
case TexFile.TextureFormat.B8G8R8A8:
// In case of modded font textures.
ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4);
break;
default:
// Unlikely.
ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4);
break;
}
return this.scopedFinalizer.Add(
this.InterfaceManager.LoadImageFromDxgiFormat(
buffer,
texFile.Header.Width * bpp,
texFile.Header.Width,
texFile.Header.Height,
targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}

View file

@ -1,857 +0,0 @@
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Disposables;
using Dalamud.Game.Text;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using ImGuiNET;
using Lumina.Data.Files;
using Vector4 = System.Numerics.Vector4;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// A font handle that uses the game's built-in fonts, optionally with some styling.
/// </summary>
internal class GamePrebakedFontHandle : IFontHandle.IInternal
{
/// <summary>
/// The smallest value of <see cref="SeIconChar"/>.
/// </summary>
public static readonly char SeIconCharMin = (char)Enum.GetValues<SeIconChar>().Min();
/// <summary>
/// The largest value of <see cref="SeIconChar"/>.
/// </summary>
public static readonly char SeIconCharMax = (char)Enum.GetValues<SeIconChar>().Max();
private IFontHandleManager? manager;
/// <summary>
/// Initializes a new instance of the <see cref="GamePrebakedFontHandle"/> class.
/// </summary>
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
/// <param name="style">Font to use.</param>
public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style)
{
if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined)
throw new ArgumentOutOfRangeException(nameof(style), style, null);
if (style.SizePt <= 0)
throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style));
this.manager = manager;
this.FontStyle = style;
}
/// <summary>
/// Provider for <see cref="IDalamudTextureWrap"/> for `common/font/fontNN.tex`.
/// </summary>
public interface IGameFontTextureProvider
{
/// <summary>
/// Creates the <see cref="FdtFileView"/> for the <see cref="GameFontFamilyAndSize"/>.<br />
/// <strong>Dispose after use.</strong>
/// </summary>
/// <param name="gffas">The font family and size.</param>
/// <param name="fdtFileView">The view.</param>
/// <returns>Dispose this after use..</returns>
public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView);
/// <summary>
/// Gets the number of font textures.
/// </summary>
/// <param name="texPathFormat">Format of .tex path.</param>
/// <returns>The number of textures.</returns>
public int GetFontTextureCount(string texPathFormat);
/// <summary>
/// Gets the <see cref="TexFile"/> for the given index of a font.
/// </summary>
/// <param name="texPathFormat">Format of .tex path.</param>
/// <param name="index">The index of .tex file.</param>
/// <returns>The <see cref="TexFile"/>.</returns>
public TexFile GetTexFile(string texPathFormat, int index);
/// <summary>
/// Gets a new reference of the font texture.
/// </summary>
/// <param name="texPathFormat">Format of .tex path.</param>
/// <param name="textureIndex">Texture index.</param>
/// <returns>The texture.</returns>
public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex);
}
/// <summary>
/// Gets the font style.
/// </summary>
public GameFontStyle FontStyle { get; }
/// <inheritdoc/>
public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this);
/// <inheritdoc/>
public bool Available => this.ImFont.IsNotNullAndLoaded();
/// <inheritdoc/>
public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default;
private IFontHandleManager ManagerNotDisposed =>
this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle));
/// <inheritdoc/>
public void Dispose()
{
this.manager?.FreeFontHandle(this);
this.manager = null;
}
/// <inheritdoc/>
public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available);
/// <summary>
/// Manager for <see cref="GamePrebakedFontHandle"/>s.
/// </summary>
internal sealed class HandleManager : IFontHandleManager
{
private readonly Dictionary<GameFontStyle, int> gameFontsRc = new();
private readonly object syncRoot = new();
/// <summary>
/// Initializes a new instance of the <see cref="HandleManager"/> class.
/// </summary>
/// <param name="atlasName">The name of the owner atlas.</param>
/// <param name="gameFontTextureProvider">An instance of <see cref="IGameFontTextureProvider"/>.</param>
public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider)
{
this.GameFontTextureProvider = gameFontTextureProvider;
this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager";
}
/// <inheritdoc/>
public event Action? RebuildRecommend;
/// <inheritdoc/>
public string Name { get; }
/// <inheritdoc/>
public IFontHandleSubstance? Substance { get; set; }
/// <summary>
/// Gets an instance of <see cref="IGameFontTextureProvider"/>.
/// </summary>
public IGameFontTextureProvider GameFontTextureProvider { get; }
/// <inheritdoc/>
public void Dispose()
{
this.Substance?.Dispose();
this.Substance = null;
}
/// <inheritdoc cref="IFontAtlas.NewGameFontHandle"/>
public IFontHandle NewFontHandle(GameFontStyle style)
{
var handle = new GamePrebakedFontHandle(this, style);
bool suggestRebuild;
lock (this.syncRoot)
{
this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1;
suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true;
}
if (suggestRebuild)
this.RebuildRecommend?.Invoke();
return handle;
}
/// <inheritdoc/>
public void FreeFontHandle(IFontHandle handle)
{
if (handle is not GamePrebakedFontHandle ggfh)
return;
lock (this.syncRoot)
{
if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle))
return;
if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0)
this.gameFontsRc.Remove(ggfh.FontStyle);
}
}
/// <inheritdoc/>
public IFontHandleSubstance NewSubstance()
{
lock (this.syncRoot)
return new HandleSubstance(this, this.gameFontsRc.Keys);
}
}
/// <summary>
/// Substance from <see cref="HandleManager"/>.
/// </summary>
internal sealed class HandleSubstance : IFontHandleSubstance
{
private readonly HandleManager handleManager;
private readonly HashSet<GameFontStyle> gameFontStyles;
// Owned by this class, but ImFontPtr values still do not belong to this.
private readonly Dictionary<GameFontStyle, FontDrawPlan> fonts = new();
private readonly Dictionary<GameFontStyle, Exception?> buildExceptions = new();
private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new();
private readonly HashSet<ImFontPtr> templatedFonts = new();
/// <summary>
/// Initializes a new instance of the <see cref="HandleSubstance"/> class.
/// </summary>
/// <param name="manager">The manager.</param>
/// <param name="gameFontStyles">The game font styles.</param>
public HandleSubstance(HandleManager manager, IEnumerable<GameFontStyle> gameFontStyles)
{
this.handleManager = manager;
Service<InterfaceManager>.Get();
this.gameFontStyles = new(gameFontStyles);
}
/// <inheritdoc/>
public IFontHandleManager Manager => this.handleManager;
/// <inheritdoc/>
public void Dispose()
{
}
/// <summary>
/// Attaches game symbols to the given font. If font is null, it will be created.
/// </summary>
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
/// <param name="font">The font to attach to.</param>
/// <param name="style">The game font style.</param>
/// <param name="glyphRanges">The intended glyph ranges.</param>
/// <returns><paramref name="font"/> if it is not empty; otherwise a new font.</returns>
public ImFontPtr AttachGameGlyphs(
IFontAtlasBuildToolkitPreBuild toolkitPreBuild,
ImFontPtr font,
GameFontStyle style,
ushort[]? glyphRanges = null)
{
if (font.IsNull())
font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx);
this.attachments.Add((font, style, glyphRanges));
return font;
}
/// <summary>
/// Creates or gets a relevant <see cref="ImFontPtr"/> for the given <see cref="GameFontStyle"/>.
/// </summary>
/// <param name="style">The game font style.</param>
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
/// <returns>The font.</returns>
public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{
try
{
if (!this.fonts.TryGetValue(style, out var plan))
{
plan = new(
style,
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
this.fonts[style] = plan;
}
plan.AttachFont(plan.FullRangeFont);
return plan.FullRangeFont;
}
catch (Exception e)
{
this.buildExceptions[style] = e;
throw;
}
}
/// <inheritdoc/>
public ImFontPtr GetFontPtr(IFontHandle handle) =>
handle is GamePrebakedFontHandle ggfh
? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default
: default;
/// <inheritdoc/>
public Exception? GetBuildException(IFontHandle handle) =>
handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default;
/// <inheritdoc/>
public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{
foreach (var style in this.gameFontStyles)
{
if (this.fonts.ContainsKey(style))
continue;
try
{
_ = this.GetOrCreateFont(style, toolkitPreBuild);
}
catch
{
// ignore; it should have been recorded from the call
}
}
}
/// <inheritdoc/>
public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
{
foreach (var (font, style, ranges) in this.attachments)
{
var effectiveStyle =
toolkitPreBuild.IsGlobalScaleIgnored(font)
? style.Scale(1 / toolkitPreBuild.Scale)
: style;
if (!this.fonts.TryGetValue(style, out var plan))
{
plan = new(
effectiveStyle,
toolkitPreBuild.Scale,
this.handleManager.GameFontTextureProvider,
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
this.fonts[style] = plan;
}
plan.AttachFont(font, ranges);
}
foreach (var plan in this.fonts.Values)
{
plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas);
}
}
/// <inheritdoc/>
public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
{
var allTextureIndices = new Dictionary<string, int[]>();
var allTexFiles = new Dictionary<string, TexFile[]>();
using var rentReturn = Disposable.Create(
() =>
{
foreach (var x in allTextureIndices.Values)
ArrayPool<int>.Shared.Return(x);
foreach (var x in allTexFiles.Values)
ArrayPool<TexFile>.Shared.Return(x);
});
var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size];
var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size];
for (var i = 0; i < pixels8Array.Length; i++)
toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _);
foreach (var (style, plan) in this.fonts)
{
try
{
foreach (var font in plan.Ranges.Keys)
this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale);
plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths);
plan.CopyGlyphsToRanges(toolkitPostBuild);
plan.PostProcessFullRangeFont(toolkitPostBuild.Scale);
}
catch (Exception e)
{
this.buildExceptions[style] = e;
this.fonts[style] = default;
}
}
}
/// <inheritdoc/>
public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion)
{
// Irrelevant
}
/// <summary>
/// Creates a new template font.
/// </summary>
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
/// <param name="sizePx">The size of the font.</param>
/// <returns>The font.</returns>
private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx)
{
var font = toolkitPreBuild.AddDalamudAssetFont(
DalamudAsset.NotoSansJpMedium,
new()
{
GlyphRanges = new ushort[] { ' ', ' ', '\0' },
SizePx = sizePx,
});
this.templatedFonts.Add(font);
return font;
}
private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale)
{
if (!this.templatedFonts.Contains(font))
return;
var fas = style.Scale(atlasScale).FamilyAndSize;
using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt);
ref var fdtFontHeader = ref fdt.FontHeader;
var fontPtr = font.NativePtr;
var scale = style.SizePt / fdtFontHeader.Size;
fontPtr->Ascent = fdtFontHeader.Ascent * scale;
fontPtr->Descent = fdtFontHeader.Descent * scale;
fontPtr->EllipsisChar = '…';
}
}
[SuppressMessage(
"StyleCop.CSharp.MaintainabilityRules",
"SA1401:Fields should be private",
Justification = "Internal")]
private sealed class FontDrawPlan : IDisposable
{
public readonly GameFontStyle Style;
public readonly GameFontStyle BaseStyle;
public readonly GameFontFamilyAndSizeAttribute BaseAttr;
public readonly int TexCount;
public readonly Dictionary<ImFontPtr, BitArray> Ranges = new();
public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new();
public readonly ushort[] RectLookup = new ushort[0x10000];
public readonly FdtFileView Fdt;
public readonly ImFontPtr FullRangeFont;
private readonly IDisposable fdtHandle;
private readonly IGameFontTextureProvider gftp;
public FontDrawPlan(
GameFontStyle style,
float scale,
IGameFontTextureProvider gameFontTextureProvider,
ImFontPtr fullRangeFont)
{
this.Style = style;
this.BaseStyle = style.Scale(scale);
this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute<GameFontFamilyAndSizeAttribute>()!;
this.gftp = gameFontTextureProvider;
this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat);
this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt);
this.RectLookup.AsSpan().Fill(ushort.MaxValue);
this.FullRangeFont = fullRangeFont;
this.Ranges[fullRangeFont] = new(0x10000);
}
public void Dispose()
{
this.fdtHandle.Dispose();
}
public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null)
{
if (!this.Ranges.TryGetValue(font, out var rangeBitArray))
rangeBitArray = this.Ranges[font] = new(0x10000);
if (glyphRanges is null)
{
foreach (ref var g in this.Fdt.Glyphs)
{
var c = g.CharInt;
if (c is >= 0x20 and <= 0xFFFE)
rangeBitArray[c] = true;
}
return;
}
for (var i = 0; i < glyphRanges.Length - 1; i += 2)
{
if (glyphRanges[i] == 0)
break;
var from = (int)glyphRanges[i];
var to = (int)glyphRanges[i + 1];
for (var j = from; j <= to; j++)
rangeBitArray[j] = true;
}
}
public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas)
{
var glyphs = this.Fdt.Glyphs;
var ranges = this.Ranges[this.FullRangeFont];
foreach (var (font, extraRange) in this.Ranges)
{
if (font.NativePtr != this.FullRangeFont.NativePtr)
ranges.Or(extraRange);
}
if (this.Style is not { Weight: 0, SkewStrength: 0 })
{
for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++)
{
ref var glyph = ref glyphs[fdtGlyphIndex];
var cint = glyph.CharInt;
if (cint > char.MaxValue)
continue;
if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue)
continue;
var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph);
this.RectLookup[cint] = (ushort)this.Rects.Count;
this.Rects.Add(
(
atlas.AddCustomRectFontGlyph(
this.FullRangeFont,
(char)cint,
glyph.BoundingWidth + widthAdjustment,
glyph.BoundingHeight,
glyph.AdvanceWidth,
new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)),
fdtGlyphIndex));
}
}
else
{
for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++)
{
ref var glyph = ref glyphs[fdtGlyphIndex];
var cint = glyph.CharInt;
if (cint > char.MaxValue)
continue;
if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue)
continue;
this.RectLookup[cint] = (ushort)this.Rects.Count;
this.Rects.Add((-1, fdtGlyphIndex));
}
}
}
public unsafe void PostProcessFullRangeFont(float atlasScale)
{
var round = 1 / atlasScale;
var pfrf = this.FullRangeFont.NativePtr;
ref var frf = ref *pfrf;
frf.FontSize = MathF.Round(frf.FontSize / round) * round;
frf.Ascent = MathF.Round(frf.Ascent / round) * round;
frf.Descent = MathF.Round(frf.Descent / round) * round;
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan)
{
var w = (g.X1 - g.X0) * scale;
var h = (g.Y1 - g.Y0) * scale;
g.X0 = MathF.Round((g.X0 * scale) / round) * round;
g.Y0 = MathF.Round((g.Y0 * scale) / round) * round;
g.X1 = g.X0 + w;
g.Y1 = g.Y0 + h;
g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round;
}
var fullRange = this.Ranges[this.FullRangeFont];
foreach (ref var k in this.Fdt.PairAdjustments)
{
var (leftInt, rightInt) = (k.LeftInt, k.RightInt);
if (leftInt > char.MaxValue || rightInt > char.MaxValue)
continue;
if (!fullRange[leftInt] || !fullRange[rightInt])
continue;
ImGuiNative.ImFont_AddKerningPair(
pfrf,
(ushort)leftInt,
(ushort)rightInt,
MathF.Round((k.RightOffset * scale) / round) * round);
}
pfrf->FallbackGlyph = null;
ImGuiNative.ImFont_BuildLookupTable(pfrf);
foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints)
{
var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate);
if ((nint)glyph == IntPtr.Zero)
continue;
frf.FallbackChar = fallbackCharCandidate;
frf.FallbackGlyph = glyph;
frf.FallbackHotData =
(ImFontGlyphHotData*)frf.IndexedHotData.Address<ImGuiHelpers.ImFontGlyphHotDataReal>(
fallbackCharCandidate);
break;
}
}
public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
{
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
var atlasScale = toolkitPostBuild.Scale;
var round = 1 / atlasScale;
foreach (var (font, rangeBits) in this.Ranges)
{
if (font.NativePtr == this.FullRangeFont.NativePtr)
continue;
var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font);
var lookup = font.IndexLookupWrapped();
var glyphs = font.GlyphsWrapped();
foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan)
{
if (!rangeBits[sourceGlyph.Codepoint])
continue;
var glyphIndex = ushort.MaxValue;
if (sourceGlyph.Codepoint < lookup.Length)
glyphIndex = lookup[sourceGlyph.Codepoint];
if (glyphIndex == ushort.MaxValue)
{
glyphIndex = (ushort)glyphs.Length;
glyphs.Add(default);
}
ref var g = ref glyphs[glyphIndex];
g = sourceGlyph;
if (noGlobalScale)
{
g.XY *= scale;
g.AdvanceX *= scale;
}
else
{
var w = (g.X1 - g.X0) * scale;
var h = (g.Y1 - g.Y0) * scale;
g.X0 = MathF.Round((g.X0 * scale) / round) * round;
g.Y0 = MathF.Round((g.Y0 * scale) / round) * round;
g.X1 = g.X0 + w;
g.Y1 = g.Y0 + h;
g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round;
}
}
foreach (ref var k in this.Fdt.PairAdjustments)
{
var (leftInt, rightInt) = (k.LeftInt, k.RightInt);
if (leftInt > char.MaxValue || rightInt > char.MaxValue)
continue;
if (!rangeBits[leftInt] || !rangeBits[rightInt])
continue;
if (noGlobalScale)
{
font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale);
}
else
{
font.AddKerningPair(
(ushort)leftInt,
(ushort)rightInt,
MathF.Round((k.RightOffset * scale) / round) * round);
}
}
font.NativePtr->FallbackGlyph = null;
font.BuildLookupTable();
foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints)
{
var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr;
if ((nint)glyph == IntPtr.Zero)
continue;
ref var frf = ref *font.NativePtr;
frf.FallbackChar = fallbackCharCandidate;
frf.FallbackGlyph = glyph;
frf.FallbackHotData =
(ImFontGlyphHotData*)frf.IndexedHotData.Address<ImGuiHelpers.ImFontGlyphHotDataReal>(
fallbackCharCandidate);
break;
}
}
}
public unsafe void SetFullRangeFontGlyphs(
IFontAtlasBuildToolkitPostBuild toolkitPostBuild,
Dictionary<string, TexFile[]> allTexFiles,
Dictionary<string, int[]> allTextureIndices,
byte*[] pixels8Array,
int[] widths)
{
var glyphs = this.FullRangeFont.GlyphsWrapped();
var lookups = this.FullRangeFont.IndexLookupWrapped();
ref var fdtFontHeader = ref this.Fdt.FontHeader;
var fdtGlyphs = this.Fdt.Glyphs;
var fdtTexSize = new Vector4(
this.Fdt.FontHeader.TextureWidth,
this.Fdt.FontHeader.TextureHeight,
this.Fdt.FontHeader.TextureWidth,
this.Fdt.FontHeader.TextureHeight);
if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles))
{
allTexFiles.Add(
this.BaseAttr.TexPathFormat,
texFiles = ArrayPool<TexFile>.Shared.Rent(this.TexCount));
}
if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices))
{
allTextureIndices.Add(
this.BaseAttr.TexPathFormat,
textureIndices = ArrayPool<int>.Shared.Rent(this.TexCount));
textureIndices.AsSpan(0, this.TexCount).Fill(-1);
}
var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1));
var pixelStrength = stackalloc byte[pixelWidth];
for (var i = 0; i < pixelWidth; i++)
pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i));
var minGlyphY = 0;
var maxGlyphY = 0;
foreach (ref var g in fdtGlyphs)
{
minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY);
maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY);
}
var horzShift = stackalloc int[maxGlyphY - minGlyphY];
var horzBlend = stackalloc byte[maxGlyphY - minGlyphY];
horzShift -= minGlyphY;
horzBlend -= minGlyphY;
if (this.BaseStyle.BaseSkewStrength != 0)
{
for (var i = minGlyphY; i < maxGlyphY; i++)
{
float blend = this.BaseStyle.BaseSkewStrength switch
{
> 0 => fdtFontHeader.LineHeight - i,
< 0 => -i,
_ => throw new InvalidOperationException(),
};
blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight;
horzShift[i] = (int)MathF.Floor(blend);
horzBlend[i] = (byte)(255 * (blend - horzShift[i]));
}
}
foreach (var (rectId, fdtGlyphIndex) in this.Rects)
{
ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex];
if (rectId == -1)
{
ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex];
if (textureIndex == -1)
{
textureIndex = toolkitPostBuild.StoreTexture(
this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex),
true);
}
var glyph = new ImGuiHelpers.ImFontGlyphReal
{
AdvanceX = fdtGlyph.AdvanceWidth,
Codepoint = fdtGlyph.Char,
Colored = false,
TextureIndex = textureIndex,
Visible = true,
X0 = this.BaseAttr.HorizontalOffset,
Y0 = fdtGlyph.CurrentOffsetY,
U0 = fdtGlyph.TextureOffsetX,
V0 = fdtGlyph.TextureOffsetY,
U1 = fdtGlyph.BoundingWidth,
V1 = fdtGlyph.BoundingHeight,
};
glyph.XY1 = glyph.XY0 + glyph.UV1;
glyph.UV1 += glyph.UV0;
glyph.UV /= fdtTexSize;
glyphs.Add(glyph);
}
else
{
ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas
.GetCustomRectByIndex(rectId)
.NativePtr;
var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph);
// Glyph is scaled at this point; undo that.
ref var glyph = ref glyphs[lookups[rc.GlyphId]];
glyph.X0 = this.BaseAttr.HorizontalOffset;
glyph.Y0 = fdtGlyph.CurrentOffsetY;
glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment;
glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight;
glyph.AdvanceX = fdtGlyph.AdvanceWidth;
var pixels8 = pixels8Array[rc.TextureIndex];
var width = widths[rc.TextureIndex];
texFiles[fdtGlyph.TextureFileIndex] ??=
this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex);
var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData;
var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex;
for (var y = 0; y < fdtGlyph.BoundingHeight; y++)
{
var sourcePixelIndex =
((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX;
sourcePixelIndex *= 4;
sourcePixelIndex += sourceBufferDelta;
var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y];
var targetOffset = ((rc.Y + y) * width) + rc.X;
for (var x = 0; x < rc.Width; x++)
pixels8[targetOffset + x] = 0;
targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y];
if (blend1 == 0)
{
for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++)
{
var n = sourceBuffer[sourcePixelIndex + 4];
for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++)
{
ref var p = ref pixels8[targetOffset + boldOffset];
p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255));
}
}
}
else
{
var blend2 = 255 - blend1;
for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++)
{
var a1 = sourceBuffer[sourcePixelIndex];
var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4];
var n = (a1 * blend1) + (a2 * blend2);
for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++)
{
ref var p = ref pixels8[targetOffset + boldOffset];
p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255));
}
}
}
}
}
}
}
}
}

View file

@ -1,32 +0,0 @@
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Manager for <see cref="IFontHandle"/>.
/// </summary>
internal interface IFontHandleManager : IDisposable
{
/// <inheritdoc cref="IFontAtlas.RebuildRecommend"/>
event Action? RebuildRecommend;
/// <summary>
/// Gets the name of the font handle manager. For logging and debugging purposes.
/// </summary>
string Name { get; }
/// <summary>
/// Gets or sets the active font handle substance.
/// </summary>
IFontHandleSubstance? Substance { get; set; }
/// <summary>
/// Decrease font reference counter.
/// </summary>
/// <param name="handle">Handle being released.</param>
void FreeFontHandle(IFontHandle handle);
/// <summary>
/// Creates a new substance of the font atlas.
/// </summary>
/// <returns>The new substance.</returns>
IFontHandleSubstance NewSubstance();
}

View file

@ -1,54 +0,0 @@
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Substance of a font.
/// </summary>
internal interface IFontHandleSubstance : IDisposable
{
/// <summary>
/// Gets the manager relevant to this instance of <see cref="IFontHandleSubstance"/>.
/// </summary>
IFontHandleManager Manager { get; }
/// <summary>
/// Gets the font.
/// </summary>
/// <param name="handle">The handle to get from.</param>
/// <returns>Corresponding font or null.</returns>
ImFontPtr GetFontPtr(IFontHandle handle);
/// <summary>
/// Gets the exception happened while loading for the font.
/// </summary>
/// <param name="handle">The handle to get from.</param>
/// <returns>Corresponding font or null.</returns>
Exception? GetBuildException(IFontHandle handle);
/// <summary>
/// Called before <see cref="ImFontAtlasPtr.Build"/> call.
/// </summary>
/// <param name="toolkitPreBuild">The toolkit.</param>
void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild);
/// <summary>
/// Called between <see cref="OnPreBuild"/> and <see cref="ImFontAtlasPtr.Build"/> calls.<br />
/// Any further modification to <see cref="IFontAtlasBuildToolkit.Fonts"/> will result in undefined behavior.
/// </summary>
/// <param name="toolkitPreBuild">The toolkit.</param>
void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild);
/// <summary>
/// Called after <see cref="ImFontAtlasPtr.Build"/> call.
/// </summary>
/// <param name="toolkitPostBuild">The toolkit.</param>
void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild);
/// <summary>
/// Called on the specific thread depending on <see cref="IFontAtlasBuildToolkit.IsAsyncBuildOperation"/> after
/// promoting the staging atlas to direct use with <see cref="IFontAtlas"/>.
/// </summary>
/// <param name="toolkitPostPromotion">The toolkit.</param>
void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion);
}

View file

@ -1,203 +0,0 @@
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Text;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Deals with TrueType.
/// </summary>
internal static partial class TrueTypeUtils
{
private struct Fixed : IComparable<Fixed>
{
public ushort Major;
public ushort Minor;
public Fixed(ushort major, ushort minor)
{
this.Major = major;
this.Minor = minor;
}
public Fixed(PointerSpan<byte> span)
{
var offset = 0;
span.ReadBig(ref offset, out this.Major);
span.ReadBig(ref offset, out this.Minor);
}
public int CompareTo(Fixed other)
{
var majorComparison = this.Major.CompareTo(other.Major);
return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor);
}
}
private struct KerningPair : IEquatable<KerningPair>
{
public ushort Left;
public ushort Right;
public short Value;
public KerningPair(PointerSpan<byte> span)
{
var offset = 0;
span.ReadBig(ref offset, out this.Left);
span.ReadBig(ref offset, out this.Right);
span.ReadBig(ref offset, out this.Value);
}
public KerningPair(ushort left, ushort right, short value)
{
this.Left = left;
this.Right = right;
this.Value = value;
}
public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right);
public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right);
public static KerningPair ReverseEndianness(KerningPair pair) => new()
{
Left = BinaryPrimitives.ReverseEndianness(pair.Left),
Right = BinaryPrimitives.ReverseEndianness(pair.Right),
Value = BinaryPrimitives.ReverseEndianness(pair.Value),
};
public bool Equals(KerningPair other) =>
this.Left == other.Left && this.Right == other.Right && this.Value == other.Value;
public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other);
public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value);
public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}";
}
[StructLayout(LayoutKind.Explicit, Size = 4)]
private struct PlatformAndEncoding
{
[FieldOffset(0)]
public PlatformId Platform;
[FieldOffset(2)]
public UnicodeEncodingId UnicodeEncoding;
[FieldOffset(2)]
public MacintoshEncodingId MacintoshEncoding;
[FieldOffset(2)]
public IsoEncodingId IsoEncoding;
[FieldOffset(2)]
public WindowsEncodingId WindowsEncoding;
public PlatformAndEncoding(PointerSpan<byte> source)
{
var offset = 0;
source.ReadBig(ref offset, out this.Platform);
source.ReadBig(ref offset, out this.UnicodeEncoding);
}
public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new()
{
Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform),
UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding),
};
public readonly string Decode(Span<byte> data)
{
switch (this.Platform)
{
case PlatformId.Unicode:
switch (this.UnicodeEncoding)
{
case UnicodeEncodingId.Unicode_2_0_Bmp:
case UnicodeEncodingId.Unicode_2_0_Full:
return Encoding.BigEndianUnicode.GetString(data);
}
break;
case PlatformId.Macintosh:
switch (this.MacintoshEncoding)
{
case MacintoshEncodingId.Roman:
return Encoding.ASCII.GetString(data);
}
break;
case PlatformId.Windows:
switch (this.WindowsEncoding)
{
case WindowsEncodingId.Symbol:
case WindowsEncodingId.UnicodeBmp:
case WindowsEncodingId.UnicodeFullRepertoire:
return Encoding.BigEndianUnicode.GetString(data);
}
break;
}
throw new NotSupportedException();
}
}
[StructLayout(LayoutKind.Explicit)]
private struct TagStruct : IEquatable<TagStruct>, IComparable<TagStruct>
{
[FieldOffset(0)]
public unsafe fixed byte Tag[4];
[FieldOffset(0)]
public uint NativeValue;
public unsafe TagStruct(char c1, char c2, char c3, char c4)
{
this.Tag[0] = checked((byte)c1);
this.Tag[1] = checked((byte)c2);
this.Tag[2] = checked((byte)c3);
this.Tag[3] = checked((byte)c4);
}
public unsafe TagStruct(PointerSpan<byte> span)
{
this.Tag[0] = span[0];
this.Tag[1] = span[1];
this.Tag[2] = span[2];
this.Tag[3] = span[3];
}
public unsafe TagStruct(ReadOnlySpan<byte> span)
{
this.Tag[0] = span[0];
this.Tag[1] = span[1];
this.Tag[2] = span[2];
this.Tag[3] = span[3];
}
public unsafe byte this[int index]
{
get => this.Tag[index];
set => this.Tag[index] = value;
}
public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right);
public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right);
public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue;
public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other);
public override int GetHashCode() => (int)this.NativeValue;
public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue);
public override unsafe string ToString() =>
$"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\"";
}
}

View file

@ -1,84 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Deals with TrueType.
/// </summary>
internal static partial class TrueTypeUtils
{
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")]
private enum IsoEncodingId : ushort
{
Ascii = 0,
Iso_10646 = 1,
Iso_8859_1 = 2,
}
private enum MacintoshEncodingId : ushort
{
Roman = 0,
}
private enum NameId : ushort
{
CopyrightNotice = 0,
FamilyName = 1,
SubfamilyName = 2,
UniqueId = 3,
FullFontName = 4,
VersionString = 5,
PostScriptName = 6,
Trademark = 7,
Manufacturer = 8,
Designer = 9,
Description = 10,
UrlVendor = 11,
UrlDesigner = 12,
LicenseDescription = 13,
LicenseInfoUrl = 14,
TypographicFamilyName = 16,
TypographicSubfamilyName = 17,
CompatibleFullMac = 18,
SampleText = 19,
PoscSriptCidFindFontName = 20,
WwsFamilyName = 21,
WwsSubfamilyName = 22,
LightBackgroundPalette = 23,
DarkBackgroundPalette = 24,
VariationPostScriptNamePrefix = 25,
}
private enum PlatformId : ushort
{
Unicode = 0,
Macintosh = 1, // discouraged
Iso = 2, // deprecated
Windows = 3,
Custom = 4, // OTF Windows NT compatibility mapping
}
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")]
private enum UnicodeEncodingId : ushort
{
Unicode_1_0 = 0, // deprecated
Unicode_1_1 = 1, // deprecated
IsoIec_10646 = 2, // deprecated
Unicode_2_0_Bmp = 3,
Unicode_2_0_Full = 4,
UnicodeVariationSequences = 5,
UnicodeFullRepertoire = 6,
}
private enum WindowsEncodingId : ushort
{
Symbol = 0,
UnicodeBmp = 1,
ShiftJis = 2,
Prc = 3,
Big5 = 4,
Wansung = 5,
Johab = 6,
UnicodeFullRepertoire = 10,
}
}

View file

@ -1,148 +0,0 @@
using System.Buffers.Binary;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Deals with TrueType.
/// </summary>
[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")]
[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")]
[SuppressMessage(
"StyleCop.CSharp.NamingRules",
"SA1310:Field names should not contain underscore",
Justification = "Version name")]
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")]
internal static partial class TrueTypeUtils
{
private readonly struct SfntFile : IReadOnlyDictionary<TagStruct, PointerSpan<byte>>
{
// http://formats.kaitai.io/ttf/ttf.svg
public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0');
public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1');
public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O');
public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0');
public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e');
public readonly PointerSpan<byte> Memory;
public readonly int OffsetInCollection;
public readonly ushort TableCount;
public SfntFile(PointerSpan<byte> memory, int offsetInCollection = 0)
{
var span = memory.Span;
this.Memory = memory;
this.OffsetInCollection = offsetInCollection;
this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]);
}
public int Count => this.TableCount;
public IEnumerable<TagStruct> Keys => this.Select(x => x.Key);
public IEnumerable<PointerSpan<byte>> Values => this.Select(x => x.Value);
public PointerSpan<byte> this[TagStruct key] => this.First(x => x.Key == key).Value;
public IEnumerator<KeyValuePair<TagStruct, PointerSpan<byte>>> GetEnumerator()
{
var offset = 12;
for (var i = 0; i < this.TableCount; i++)
{
var dte = new DirectoryTableEntry(this.Memory[offset..]);
yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length));
offset += Unsafe.SizeOf<DirectoryTableEntry>();
}
}
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key);
public bool TryGetValue(TagStruct key, out PointerSpan<byte> value)
{
foreach (var (k, v) in this)
{
if (k == key)
{
value = v;
return true;
}
}
value = default;
return false;
}
public readonly struct DirectoryTableEntry
{
public readonly PointerSpan<byte> Memory;
public DirectoryTableEntry(PointerSpan<byte> span) => this.Memory = span;
public TagStruct Tag => new(this.Memory);
public uint Checksum => this.Memory.ReadU32Big(4);
public int Offset => this.Memory.ReadI32Big(8);
public int Length => this.Memory.ReadI32Big(12);
}
}
private readonly struct TtcFile : IReadOnlyList<SfntFile>
{
public static readonly TagStruct FileTag = new('t', 't', 'c', 'f');
public readonly PointerSpan<byte> Memory;
public readonly TagStruct Tag;
public readonly ushort MajorVersion;
public readonly ushort MinorVersion;
public readonly int FontCount;
public TtcFile(PointerSpan<byte> memory)
{
var span = memory.Span;
this.Memory = memory;
this.Tag = new(span);
if (this.Tag != FileTag)
throw new InvalidOperationException();
this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]);
this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]);
this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]);
}
public int Count => this.FontCount;
public SfntFile this[int index]
{
get
{
if (index < 0 || index >= this.FontCount)
{
throw new IndexOutOfRangeException(
$"The requested font #{index} does not exist in this .ttc file.");
}
var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]);
return new(this.Memory[offset..], offset);
}
}
public IEnumerator<SfntFile> GetEnumerator()
{
for (var i = 0; i < this.FontCount; i++)
yield return this[i];
}
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
}

View file

@ -1,259 +0,0 @@
using System.Buffers.Binary;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Deals with TrueType.
/// </summary>
internal static partial class TrueTypeUtils
{
[Flags]
private enum LookupFlags : byte
{
RightToLeft = 1 << 0,
IgnoreBaseGlyphs = 1 << 1,
IgnoreLigatures = 1 << 2,
IgnoreMarks = 1 << 3,
UseMarkFilteringSet = 1 << 4,
}
private enum LookupType : ushort
{
SingleAdjustment = 1,
PairAdjustment = 2,
CursiveAttachment = 3,
MarkToBaseAttachment = 4,
MarkToLigatureAttachment = 5,
MarkToMarkAttachment = 6,
ContextPositioning = 7,
ChainedContextPositioning = 8,
ExtensionPositioning = 9,
}
private readonly struct ClassDefTable
{
public readonly PointerSpan<byte> Memory;
public ClassDefTable(PointerSpan<byte> memory) => this.Memory = memory;
public ushort Format => this.Memory.ReadU16Big(0);
public Format1ClassArray Format1 => new(this.Memory);
public Format2ClassRanges Format2 => new(this.Memory);
public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate()
{
switch (this.Format)
{
case 1:
{
var format1 = this.Format1;
var startId = format1.StartGlyphId;
var count = format1.GlyphCount;
var classes = format1.ClassValueArray;
for (var i = 0; i < count; i++)
yield return (classes[i], (ushort)(i + startId));
break;
}
case 2:
{
foreach (var range in this.Format2.ClassValueArray)
{
var @class = range.Class;
var startId = range.StartGlyphId;
var count = range.EndGlyphId - startId + 1;
for (var i = 0; i < count; i++)
yield return (@class, (ushort)(startId + i));
}
break;
}
}
}
[Pure]
public ushort GetClass(ushort glyphId)
{
switch (this.Format)
{
case 1:
{
var format1 = this.Format1;
var startId = format1.StartGlyphId;
if (startId <= glyphId && glyphId < startId + format1.GlyphCount)
return this.Format1.ClassValueArray[glyphId - startId];
break;
}
case 2:
{
var rangeSpan = this.Format2.ClassValueArray;
var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId });
if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId))
return rangeSpan[i].Class;
break;
}
}
return 0;
}
public readonly struct Format1ClassArray
{
public readonly PointerSpan<byte> Memory;
public Format1ClassArray(PointerSpan<byte> memory) => this.Memory = memory;
public ushort Format => this.Memory.ReadU16Big(0);
public ushort StartGlyphId => this.Memory.ReadU16Big(2);
public ushort GlyphCount => this.Memory.ReadU16Big(4);
public BigEndianPointerSpan<ushort> ClassValueArray => new(
this.Memory[6..].As<ushort>(this.GlyphCount),
BinaryPrimitives.ReverseEndianness);
}
public readonly struct Format2ClassRanges
{
public readonly PointerSpan<byte> Memory;
public Format2ClassRanges(PointerSpan<byte> memory) => this.Memory = memory;
public ushort ClassRangeCount => this.Memory.ReadU16Big(2);
public BigEndianPointerSpan<ClassRangeRecord> ClassValueArray => new(
this.Memory[4..].As<ClassRangeRecord>(this.ClassRangeCount),
ClassRangeRecord.ReverseEndianness);
public struct ClassRangeRecord : IComparable<ClassRangeRecord>
{
public ushort StartGlyphId;
public ushort EndGlyphId;
public ushort Class;
public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new()
{
StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId),
EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId),
Class = BinaryPrimitives.ReverseEndianness(value.Class),
};
public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId);
public bool ContainsGlyph(ushort glyphId) =>
this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId;
}
}
}
private readonly struct CoverageTable
{
public readonly PointerSpan<byte> Memory;
public CoverageTable(PointerSpan<byte> memory) => this.Memory = memory;
public enum CoverageFormat : ushort
{
Glyphs = 1,
RangeRecords = 2,
}
public CoverageFormat Format => this.Memory.ReadEnumBig<CoverageFormat>(0);
public ushort Count => this.Memory.ReadU16Big(2);
public BigEndianPointerSpan<ushort> Glyphs =>
this.Format == CoverageFormat.Glyphs
? new(this.Memory[4..].As<ushort>(this.Count), BinaryPrimitives.ReverseEndianness)
: default(BigEndianPointerSpan<ushort>);
public BigEndianPointerSpan<RangeRecord> RangeRecords =>
this.Format == CoverageFormat.RangeRecords
? new(this.Memory[4..].As<RangeRecord>(this.Count), RangeRecord.ReverseEndianness)
: default(BigEndianPointerSpan<RangeRecord>);
public int GetCoverageIndex(ushort glyphId)
{
switch (this.Format)
{
case CoverageFormat.Glyphs:
return this.Glyphs.BinarySearch(glyphId);
case CoverageFormat.RangeRecords:
{
var index = this.RangeRecords.BinarySearch(
(in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId));
if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId))
return index;
return -1;
}
default:
return -1;
}
}
public struct RangeRecord
{
public ushort StartGlyphId;
public ushort EndGlyphId;
public ushort StartCoverageIndex;
public static RangeRecord ReverseEndianness(RangeRecord value) => new()
{
StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId),
EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId),
StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex),
};
public bool ContainsGlyph(ushort glyphId) =>
this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId;
}
}
private readonly struct LookupTable : IEnumerable<PointerSpan<byte>>
{
public readonly PointerSpan<byte> Memory;
public LookupTable(PointerSpan<byte> memory) => this.Memory = memory;
public LookupType Type => this.Memory.ReadEnumBig<LookupType>(0);
public byte MarkAttachmentType => this.Memory[2];
public LookupFlags Flags => (LookupFlags)this.Memory[3];
public ushort SubtableCount => this.Memory.ReadU16Big(4);
public BigEndianPointerSpan<ushort> SubtableOffsets => new(
this.Memory[6..].As<ushort>(this.SubtableCount),
BinaryPrimitives.ReverseEndianness);
public PointerSpan<byte> this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..];
public IEnumerator<PointerSpan<byte>> GetEnumerator()
{
foreach (var i in Enumerable.Range(0, this.SubtableCount))
yield return this.Memory[this.SubtableOffsets[i] ..];
}
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount
? index
: throw new IndexOutOfRangeException();
}
}

View file

@ -1,443 +0,0 @@
using System.Buffers.Binary;
using System.Collections;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Deals with TrueType.
/// </summary>
internal static partial class TrueTypeUtils
{
private delegate int BinarySearchComparer<T>(in T value);
private static IDisposable CreatePointerSpan<T>(this T[] data, out PointerSpan<T> pointerSpan)
where T : unmanaged
{
var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned);
pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length);
return Disposable.Create(() => gchandle.Free());
}
private static int BinarySearch<T>(this IReadOnlyList<T> span, in T value)
where T : unmanaged, IComparable<T>
{
var l = 0;
var r = span.Count - 1;
while (l <= r)
{
var i = (int)(((uint)r + (uint)l) >> 1);
var c = value.CompareTo(span[i]);
switch (c)
{
case 0:
return i;
case > 0:
l = i + 1;
break;
default:
r = i - 1;
break;
}
}
return ~l;
}
private static int BinarySearch<T>(this IReadOnlyList<T> span, BinarySearchComparer<T> comparer)
where T : unmanaged
{
var l = 0;
var r = span.Count - 1;
while (l <= r)
{
var i = (int)(((uint)r + (uint)l) >> 1);
var c = comparer(span[i]);
switch (c)
{
case 0:
return i;
case > 0:
l = i + 1;
break;
default:
r = i - 1;
break;
}
}
return ~l;
}
private static short ReadI16Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]);
private static int ReadI32Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]);
private static long ReadI64Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]);
private static ushort ReadU16Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]);
private static uint ReadU32Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]);
private static ulong ReadU64Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]);
private static Half ReadF16Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]);
private static float ReadF32Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]);
private static double ReadF64Big(this PointerSpan<byte> ps, int offset) =>
BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out short value) =>
value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out int value) =>
value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out long value) =>
value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out ushort value) =>
value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out uint value) =>
value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out ulong value) =>
value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out Half value) =>
value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out float value) =>
value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, int offset, out double value) =>
value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]);
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out short value)
{
ps.ReadBig(offset, out value);
offset += 2;
}
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out int value)
{
ps.ReadBig(offset, out value);
offset += 4;
}
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out long value)
{
ps.ReadBig(offset, out value);
offset += 8;
}
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out ushort value)
{
ps.ReadBig(offset, out value);
offset += 2;
}
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out uint value)
{
ps.ReadBig(offset, out value);
offset += 4;
}
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out ulong value)
{
ps.ReadBig(offset, out value);
offset += 8;
}
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out Half value)
{
ps.ReadBig(offset, out value);
offset += 2;
}
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out float value)
{
ps.ReadBig(offset, out value);
offset += 4;
}
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out double value)
{
ps.ReadBig(offset, out value);
offset += 8;
}
private static unsafe T ReadEnumBig<T>(this PointerSpan<byte> ps, int offset) where T : unmanaged, Enum
{
switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T))))
{
case 1:
var b1 = ps.Span[offset];
return *(T*)&b1;
case 2:
var b2 = ps.ReadU16Big(offset);
return *(T*)&b2;
case 4:
var b4 = ps.ReadU32Big(offset);
return *(T*)&b4;
case 8:
var b8 = ps.ReadU64Big(offset);
return *(T*)&b8;
default:
throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null);
}
}
private static void ReadBig<T>(this PointerSpan<byte> ps, int offset, out T value) where T : unmanaged, Enum =>
value = ps.ReadEnumBig<T>(offset);
private static void ReadBig<T>(this PointerSpan<byte> ps, ref int offset, out T value) where T : unmanaged, Enum
{
value = ps.ReadEnumBig<T>(offset);
offset += Unsafe.SizeOf<T>();
}
private readonly unsafe struct PointerSpan<T> : IList<T>, IReadOnlyList<T>, ICollection
where T : unmanaged
{
public readonly T* Pointer;
public PointerSpan(T* pointer, int count)
{
this.Pointer = pointer;
this.Count = count;
}
public PointerSpan(nint pointer, int count)
: this((T*)pointer, count)
{
}
public Span<T> Span => new(this.Pointer, this.Count);
public bool IsEmpty => this.Count == 0;
public int Count { get; }
public int Length => this.Count;
public int ByteCount => sizeof(T) * this.Count;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
bool ICollection<T>.IsReadOnly => false;
public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)];
public PointerSpan<T> this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count));
T IList<T>.this[int index]
{
get => this.Pointer[this.EnsureIndex(index)];
set => this.Pointer[this.EnsureIndex(index)] = value;
}
T IReadOnlyList<T>.this[int index] => this.Pointer[this.EnsureIndex(index)];
public bool ContainsPointer<T2>(T2* obj) where T2 : unmanaged =>
(T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count;
public PointerSpan<T> Slice(int offset, int count) => new(this.Pointer + offset, count);
public PointerSpan<T> Slice((int Offset, int Count) offsetAndCount)
=> this.Slice(offsetAndCount.Offset, offsetAndCount.Count);
public PointerSpan<T2> As<T2>(int count)
where T2 : unmanaged =>
count > this.Count / sizeof(T2)
? throw new ArgumentOutOfRangeException(
nameof(count),
count,
$"Wanted {count} items; had {this.Count / sizeof(T2)} items")
: new((T2*)this.Pointer, count);
public PointerSpan<T2> As<T2>()
where T2 : unmanaged =>
new((T2*)this.Pointer, this.Count / sizeof(T2));
public IEnumerator<T> GetEnumerator()
{
for (var i = 0; i < this.Count; i++)
yield return this[i];
}
void ICollection<T>.Add(T item) => throw new NotSupportedException();
void ICollection<T>.Clear() => throw new NotSupportedException();
bool ICollection<T>.Contains(T item)
{
for (var i = 0; i < this.Count; i++)
{
if (Equals(this.Pointer[i], item))
return true;
}
return false;
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
if (array.Length < this.Count)
throw new ArgumentException(null, nameof(array));
if (array.Length < arrayIndex + this.Count)
throw new ArgumentException(null, nameof(arrayIndex));
for (var i = 0; i < this.Count; i++)
array[arrayIndex + i] = this.Pointer[i];
}
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
int IList<T>.IndexOf(T item)
{
for (var i = 0; i < this.Count; i++)
{
if (Equals(this.Pointer[i], item))
return i;
}
return -1;
}
void IList<T>.Insert(int index, T item) => throw new NotSupportedException();
void IList<T>.RemoveAt(int index) => throw new NotSupportedException();
void ICollection.CopyTo(Array array, int arrayIndex)
{
if (array.Length < this.Count)
throw new ArgumentException(null, nameof(array));
if (array.Length < arrayIndex + this.Count)
throw new ArgumentException(null, nameof(arrayIndex));
for (var i = 0; i < this.Count; i++)
array.SetValue(this.Pointer[i], arrayIndex + i);
}
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private int EnsureIndex(int index) =>
index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException();
}
private readonly unsafe struct BigEndianPointerSpan<T>
: IList<T>, IReadOnlyList<T>, ICollection
where T : unmanaged
{
public readonly T* Pointer;
private readonly Func<T, T> reverseEndianness;
public BigEndianPointerSpan(PointerSpan<T> pointerSpan, Func<T, T> reverseEndianness)
{
this.reverseEndianness = reverseEndianness;
this.Pointer = pointerSpan.Pointer;
this.Count = pointerSpan.Count;
}
public int Count { get; }
public int Length => this.Count;
public int ByteCount => sizeof(T) * this.Count;
public bool IsSynchronized => true;
public object SyncRoot => this;
public bool IsReadOnly => true;
public T this[int index]
{
get =>
BitConverter.IsLittleEndian
? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)])
: this.Pointer[this.EnsureIndex(index)];
set => this.Pointer[this.EnsureIndex(index)] =
BitConverter.IsLittleEndian
? this.reverseEndianness(value)
: value;
}
public IEnumerator<T> GetEnumerator()
{
for (var i = 0; i < this.Count; i++)
yield return this[i];
}
void ICollection<T>.Add(T item) => throw new NotSupportedException();
void ICollection<T>.Clear() => throw new NotSupportedException();
bool ICollection<T>.Contains(T item) => throw new NotSupportedException();
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
if (array.Length < this.Count)
throw new ArgumentException(null, nameof(array));
if (array.Length < arrayIndex + this.Count)
throw new ArgumentException(null, nameof(arrayIndex));
for (var i = 0; i < this.Count; i++)
array[arrayIndex + i] = this[i];
}
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
int IList<T>.IndexOf(T item)
{
for (var i = 0; i < this.Count; i++)
{
if (Equals(this[i], item))
return i;
}
return -1;
}
void IList<T>.Insert(int index, T item) => throw new NotSupportedException();
void IList<T>.RemoveAt(int index) => throw new NotSupportedException();
void ICollection.CopyTo(Array array, int arrayIndex)
{
if (array.Length < this.Count)
throw new ArgumentException(null, nameof(array));
if (array.Length < arrayIndex + this.Count)
throw new ArgumentException(null, nameof(arrayIndex));
for (var i = 0; i < this.Count; i++)
array.SetValue(this[i], arrayIndex + i);
}
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private int EnsureIndex(int index) =>
index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException();
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,135 +0,0 @@
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary>
/// Deals with TrueType.
/// </summary>
internal static partial class TrueTypeUtils
{
/// <summary>
/// Checks whether the given <paramref name="fontConfig"/> will fail in <see cref="ImFontAtlasPtr.Build"/>,
/// and throws an appropriate exception if it is the case.
/// </summary>
/// <param name="fontConfig">The font config.</param>
public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig)
{
var ranges = fontConfig.GlyphRanges;
var sfnt = AsSfntFile(fontConfig);
var cmap = new Cmap(sfnt);
if (cmap.UnicodeTable is not { } unicodeTable)
throw new NotSupportedException("The font does not have a compatible Unicode character mapping table.");
if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges)))
throw new NotSupportedException("The font does not have any glyph that falls under the requested range.");
}
/// <summary>
/// Enumerates through horizontal pair adjustments of a kern and gpos tables.
/// </summary>
/// <param name="fontConfig">The font config.</param>
/// <returns>The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels.</returns>
public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments(
ImFontConfig fontConfig)
{
float multiplier;
Dictionary<ushort, char[]> glyphToCodepoints;
Gpos gpos = default;
Kern kern = default;
try
{
var sfnt = AsSfntFile(fontConfig);
var head = new Head(sfnt);
multiplier = 3f / 4 / head.UnitsPerEm;
if (new Cmap(sfnt).UnicodeTable is not { } table)
yield break;
if (sfnt.ContainsKey(Kern.DirectoryTableTag))
kern = new(sfnt);
else if (sfnt.ContainsKey(Gpos.DirectoryTableTag))
gpos = new(sfnt);
else
yield break;
glyphToCodepoints = table
.GroupBy(x => x.Value, x => x.Key)
.OrderBy(x => x.Key)
.ToDictionary(
x => x.Key,
x => x.Where(y => y <= ushort.MaxValue)
.Select(y => (char)y)
.ToArray());
}
catch
{
// don't care; give up
yield break;
}
if (kern.Memory.Count != 0)
{
foreach (var pair in kern.EnumerateHorizontalPairs())
{
if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars))
continue;
if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars))
continue;
foreach (var l in leftChars)
{
foreach (var r in rightChars)
yield return (l, r, pair.Value * multiplier);
}
}
}
else if (gpos.Memory.Count != 0)
{
foreach (var pair in gpos.ExtractAdvanceX())
{
if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars))
continue;
if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars))
continue;
foreach (var l in leftChars)
{
foreach (var r in rightChars)
yield return (l, r, pair.Value * multiplier);
}
}
}
}
private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig)
{
var memory = new PointerSpan<byte>((byte*)fontConfig.FontData, fontConfig.FontDataSize);
if (memory.Length < 4)
throw new NotSupportedException("File is too short to even have a magic.");
var magic = memory.ReadU32Big(0);
if (BitConverter.IsLittleEndian)
magic = BinaryPrimitives.ReverseEndianness(magic);
if (magic == SfntFile.FileTagTrueType1.NativeValue)
return new(memory);
if (magic == SfntFile.FileTagType1.NativeValue)
return new(memory);
if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue)
return new(memory);
if (magic == SfntFile.FileTagOpenType1_0.NativeValue)
return new(memory);
if (magic == SfntFile.FileTagTrueTypeApple.NativeValue)
return new(memory);
if (magic == TtcFile.FileTag.NativeValue)
return new TtcFile(memory)[fontConfig.FontNo];
throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported.");
}
}

View file

@ -1,306 +0,0 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Managed version of <see cref="ImFontConfig"/>, to avoid unnecessary heap allocation and use of unsafe blocks.
/// </summary>
public struct SafeFontConfig
{
/// <summary>
/// The raw config.
/// </summary>
public ImFontConfig Raw;
/// <summary>
/// Initializes a new instance of the <see cref="SafeFontConfig"/> struct.
/// </summary>
public SafeFontConfig()
{
this.OversampleH = 1;
this.OversampleV = 1;
this.PixelSnapH = true;
this.GlyphMaxAdvanceX = float.MaxValue;
this.RasterizerMultiply = 1f;
this.RasterizerGamma = 1.4f;
this.EllipsisChar = unchecked((char)-1);
this.Raw.FontDataOwnedByAtlas = 1;
}
/// <summary>
/// Initializes a new instance of the <see cref="SafeFontConfig"/> struct,
/// copying applicable values from an existing instance of <see cref="ImFontConfigPtr"/>.
/// </summary>
/// <param name="config">Config to copy from.</param>
public unsafe SafeFontConfig(ImFontConfigPtr config)
: this()
{
if (config.NativePtr is not null)
{
this.Raw = *config.NativePtr;
this.Raw.GlyphRanges = null;
}
}
/// <summary>
/// Gets or sets the index of font within a TTF/OTF file.
/// </summary>
public int FontNo
{
get => this.Raw.FontNo;
set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue);
}
/// <summary>
/// Gets or sets the desired size of the new font, in pixels.<br />
/// Effectively, this is the line height.<br />
/// Value is tied with <see cref="SizePt"/>.
/// </summary>
public float SizePx
{
get => this.Raw.SizePixels;
set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue);
}
/// <summary>
/// Gets or sets the desired size of the new font, in points.<br />
/// Effectively, this is the line height.<br />
/// Value is tied with <see cref="SizePx"/>.
/// </summary>
public float SizePt
{
get => (this.Raw.SizePixels * 3) / 4;
set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue);
}
/// <summary>
/// Gets or sets the horizontal oversampling pixel count.<br />
/// Rasterize at higher quality for sub-pixel positioning.<br />
/// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.<br />
/// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details.
/// </summary>
public int OversampleH
{
get => this.Raw.OversampleH;
set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue);
}
/// <summary>
/// Gets or sets the vertical oversampling pixel count.<br />
/// Rasterize at higher quality for sub-pixel positioning.<br />
/// This is not really useful as we don't use sub-pixel positions on the Y axis.
/// </summary>
public int OversampleV
{
get => this.Raw.OversampleV;
set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue);
}
/// <summary>
/// Gets or sets a value indicating whether to align every glyph to pixel boundary.<br />
/// Useful e.g. if you are merging a non-pixel aligned font with the default font.<br />
/// If enabled, you can set <see cref="OversampleH"/> and <see cref="OversampleV"/> to 1.
/// </summary>
public bool PixelSnapH
{
get => this.Raw.PixelSnapH != 0;
set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0;
}
/// <summary>
/// Gets or sets the extra spacing (in pixels) between glyphs.<br />
/// Only X axis is supported for now.<br />
/// Effectively, it is the letter spacing.
/// </summary>
public Vector2 GlyphExtraSpacing
{
get => this.Raw.GlyphExtraSpacing;
set => this.Raw.GlyphExtraSpacing = new(
EnsureRange(value.X, float.MinValue, float.MaxValue),
EnsureRange(value.Y, float.MinValue, float.MaxValue));
}
/// <summary>
/// Gets or sets the offset all glyphs from this font input.<br />
/// Use this to offset fonts vertically when merging multiple fonts.
/// </summary>
public Vector2 GlyphOffset
{
get => this.Raw.GlyphOffset;
set => this.Raw.GlyphOffset = new(
EnsureRange(value.X, float.MinValue, float.MaxValue),
EnsureRange(value.Y, float.MinValue, float.MaxValue));
}
/// <summary>
/// Gets or sets the glyph ranges, which is a user-provided list of Unicode range.
/// Each range has 2 values, and values are inclusive.<br />
/// The list must be zero-terminated.<br />
/// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added.
/// </summary>
public ushort[]? GlyphRanges { get; set; }
/// <summary>
/// Gets or sets the minimum AdvanceX for glyphs.<br />
/// Set only <see cref="GlyphMinAdvanceX"/> to align font icons.<br />
/// Set both <see cref="GlyphMinAdvanceX"/>/<see cref="GlyphMaxAdvanceX"/> to enforce mono-space font.
/// </summary>
public float GlyphMinAdvanceX
{
get => this.Raw.GlyphMinAdvanceX;
set => this.Raw.GlyphMinAdvanceX =
float.IsFinite(value)
? value
: throw new ArgumentOutOfRangeException(
nameof(value),
value,
$"{nameof(this.GlyphMinAdvanceX)} must be a finite number.");
}
/// <summary>
/// Gets or sets the maximum AdvanceX for glyphs.
/// </summary>
public float GlyphMaxAdvanceX
{
get => this.Raw.GlyphMaxAdvanceX;
set => this.Raw.GlyphMaxAdvanceX =
float.IsFinite(value)
? value
: throw new ArgumentOutOfRangeException(
nameof(value),
value,
$"{nameof(this.GlyphMaxAdvanceX)} must be a finite number.");
}
/// <summary>
/// Gets or sets a value that either brightens (&gt;1.0f) or darkens (&lt;1.0f) the font output.<br />
/// Brightening small fonts may be a good workaround to make them more readable.
/// </summary>
public float RasterizerMultiply
{
get => this.Raw.RasterizerMultiply;
set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue);
}
/// <summary>
/// Gets or sets the gamma value for fonts.
/// </summary>
public float RasterizerGamma
{
get => this.Raw.RasterizerGamma;
set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue);
}
/// <summary>
/// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.<br />
/// When fonts are being merged first specified ellipsis will be used.
/// </summary>
public char EllipsisChar
{
get => (char)this.Raw.EllipsisChar;
set => this.Raw.EllipsisChar = value;
}
/// <summary>
/// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost.
/// </summary>
public unsafe string Name
{
get
{
fixed (void* pName = this.Raw.Name)
{
var span = new ReadOnlySpan<byte>(pName, 40);
var firstNull = span.IndexOf((byte)0);
if (firstNull != -1)
span = span[..firstNull];
return Encoding.UTF8.GetString(span);
}
}
set
{
fixed (void* pName = this.Raw.Name)
{
var span = new Span<byte>(pName, 40);
Encoding.UTF8.GetBytes(value, span);
}
}
}
/// <summary>
/// Gets or sets the desired font to merge with, if set.
/// </summary>
public unsafe ImFontPtr MergeFont
{
get => this.Raw.DstFont is not null ? this.Raw.DstFont : default;
set
{
this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1;
this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr;
}
}
/// <summary>
/// Throws <see cref="ArgumentException"/> with appropriate messages,
/// if this <see cref="SafeFontConfig"/> has invalid values.
/// </summary>
public readonly void ThrowOnInvalidValues()
{
if (!(this.Raw.FontNo >= 0))
throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number.");
if (!(this.Raw.SizePixels > 0))
throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number.");
if (!(this.Raw.OversampleH >= 1))
throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number.");
if (!(this.Raw.OversampleV >= 1))
throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number.");
if (!float.IsFinite(this.Raw.GlyphMinAdvanceX))
throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number.");
if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX))
throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number.");
if (!(this.Raw.RasterizerMultiply > 0))
throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number.");
if (!(this.Raw.RasterizerGamma > 0))
throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number.");
if (this.GlyphRanges is { Length: > 0 } ranges)
{
if (ranges[0] == 0)
{
throw new ArgumentException(
"Font ranges cannot start with 0.",
nameof(this.GlyphRanges));
}
if (ranges[(ranges.Length - 1) & ~1] != 0)
{
throw new ArgumentException(
"Font ranges must terminate with a zero at even indices.",
nameof(this.GlyphRanges));
}
}
}
private static T EnsureRange<T>(T value, T min, T max, [CallerMemberName] string callerName = "")
where T : INumber<T>
{
if (value < min)
throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}.");
if (value > max)
throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}.");
return value;
}
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
@ -11,8 +12,6 @@ using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Utility;
using ImGuiNET;
using ImGuiScene;
@ -31,13 +30,11 @@ public sealed class UiBuilder : IDisposable
private readonly HitchDetector hitchDetector;
private readonly string namespaceName;
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
private readonly Framework framework = Service<Framework>.Get();
private readonly GameFontManager gameFontManager = Service<GameFontManager>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
private bool hasErrorWindow = false;
private bool lastFrameUiHideState = false;
@ -47,33 +44,15 @@ public sealed class UiBuilder : IDisposable
/// </summary>
/// <param name="namespaceName">The plugin namespace.</param>
internal UiBuilder(string namespaceName)
{
try
{
this.stopwatch = new Stopwatch();
this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch);
this.namespaceName = namespaceName;
this.interfaceManager.Draw += this.OnDraw;
this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw);
this.interfaceManager.BuildFonts += this.OnBuildFonts;
this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts;
this.interfaceManager.ResizeBuffers += this.OnResizeBuffers;
this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers);
this.FontAtlas =
this.scopedFinalizer
.Add(
Service<FontAtlasFactory>
.Get()
.CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable));
this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange;
this.FontAtlas.RebuildRecommend += this.RebuildFonts;
}
catch
{
this.scopedFinalizer.Dispose();
throw;
}
}
/// <summary>
@ -101,19 +80,19 @@ public sealed class UiBuilder : IDisposable
/// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.<br/>
/// Any ImFontPtr objects that you store <strong>can be invalidated</strong> when fonts are rebuilt
/// (at any time), so you should both reload your custom fonts and restore those
/// pointers inside this handler.
/// pointers inside this handler.<br/>
/// <strong>PLEASE remove this handler inside Dispose, or when you no longer need your fonts!</strong>
/// </summary>
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)]
public event Action? BuildFonts;
public event Action BuildFonts;
/// <summary>
/// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.<br/>
/// Any ImFontPtr objects that you store <strong>can be invalidated</strong> when fonts are rebuilt
/// (at any time), so you should both reload your custom fonts and restore those
/// pointers inside this handler.
/// pointers inside this handler.<br/>
/// <strong>PLEASE remove this handler inside Dispose, or when you no longer need your fonts!</strong>
/// </summary>
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)]
public event Action? AfterBuildFonts;
public event Action AfterBuildFonts;
/// <summary>
/// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown.
@ -128,57 +107,18 @@ public sealed class UiBuilder : IDisposable
public event Action HideUi;
/// <summary>
/// Gets the default Dalamud font size in points.
/// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons.
/// </summary>
public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt;
/// <summary>
/// Gets the default Dalamud font size in pixels.
/// </summary>
public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx;
/// <summary>
/// Gets the default Dalamud font - supporting all game languages and icons.<br />
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
/// </summary>
/// <remarks>
/// A font handle corresponding to this font can be obtained with:
/// <code>
/// fontAtlas.NewDelegateFontHandle(
/// e => e.OnPreBuild(
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt)));
/// </code>
/// </remarks>
public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont;
/// <summary>
/// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.<br />
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
/// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt.
/// </summary>
/// <remarks>
/// A font handle corresponding to this font can be obtained with:
/// <code>
/// fontAtlas.NewDelegateFontHandle(
/// e => e.OnPreBuild(
/// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt })));
/// </code>
/// </remarks>
public static ImFontPtr IconFont => InterfaceManager.IconFont;
/// <summary>
/// Gets the default Dalamud monospaced font based on Inconsolata Regular.<br />
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
/// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt.
/// </summary>
/// <remarks>
/// A font handle corresponding to this font can be obtained with:
/// <code>
/// fontAtlas.NewDelegateFontHandle(
/// e => e.OnPreBuild(
/// tk => tk.AddDalamudAssetFont(
/// DalamudAsset.InconsolataRegular,
/// new() { SizePt = UiBuilder.DefaultFontSizePt })));
/// </code>
/// </remarks>
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
/// <summary>
@ -250,11 +190,6 @@ public sealed class UiBuilder : IDisposable
/// </summary>
public bool UiPrepared => Service<InterfaceManager.InterfaceManagerWithScene>.GetNullable() != null;
/// <summary>
/// Gets the plugin-private font atlas.
/// </summary>
public IFontAtlas FontAtlas { get; }
/// <summary>
/// Gets or sets a value indicating whether statistics about UI draw time should be collected.
/// </summary>
@ -384,7 +319,7 @@ public sealed class UiBuilder : IDisposable
if (runInFrameworkThread)
{
return this.InterfaceManagerWithSceneAsync
.ContinueWith(_ => this.framework.RunOnFrameworkThread(func))
.ContinueWith(_ => Service<Framework>.Get().RunOnFrameworkThread(func))
.Unwrap();
}
else
@ -406,7 +341,7 @@ public sealed class UiBuilder : IDisposable
if (runInFrameworkThread)
{
return this.InterfaceManagerWithSceneAsync
.ContinueWith(_ => this.framework.RunOnFrameworkThread(func))
.ContinueWith(_ => Service<Framework>.Get().RunOnFrameworkThread(func))
.Unwrap();
}
else
@ -422,49 +357,19 @@ public sealed class UiBuilder : IDisposable
/// </summary>
/// <param name="style">Font to get.</param>
/// <returns>Handle to the game font which may or may not be available for use yet.</returns>
[Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)]
public GameFontHandle GetGameFontHandle(GameFontStyle style) => new(
(IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style),
Service<FontAtlasFactory>.Get());
public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style);
/// <summary>
/// Call this to queue a rebuild of the font atlas.<br/>
/// This will invoke any <see cref="BuildFonts"/> and <see cref="AfterBuildFonts"/> handlers and ensure that any
/// loaded fonts are ready to be used on the next UI frame.
/// This will invoke any <see cref="OnBuildFonts"/> handlers and ensure that any loaded fonts are
/// ready to be used on the next UI frame.
/// </summary>
public void RebuildFonts()
{
Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName);
if (this.AfterBuildFonts is null && this.BuildFonts is null)
this.FontAtlas.BuildFontsAsync();
else
this.FontAtlas.BuildFontsOnNextFrame();
this.interfaceManager.RebuildFonts();
}
/// <summary>
/// Creates an isolated <see cref="IFontAtlas"/>.
/// </summary>
/// <param name="autoRebuildMode">Specify when and how to rebuild this atlas.</param>
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
/// <param name="debugName">Name for debugging purposes.</param>
/// <returns>A new instance of <see cref="IFontAtlas"/>.</returns>
/// <remarks>
/// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all
/// other fonts together.<br />
/// If <paramref name="autoRebuildMode"/> is not <see cref="FontAtlasAutoRebuildMode.OnNewFrame"/>,
/// the font rebuilding functions must be called manually.
/// </remarks>
public IFontAtlas CreateFontAtlas(
FontAtlasAutoRebuildMode autoRebuildMode,
bool isGlobalScaled = true,
string? debugName = null) =>
this.scopedFinalizer.Add(Service<FontAtlasFactory>
.Get()
.CreateFontAtlas(
this.namespaceName + ":" + (debugName ?? "custom"),
autoRebuildMode,
isGlobalScaled));
/// <summary>
/// Add a notification to the notification queue.
/// </summary>
@ -487,7 +392,12 @@ public sealed class UiBuilder : IDisposable
/// <summary>
/// Unregister the UiBuilder. Do not call this in plugin code.
/// </summary>
void IDisposable.Dispose() => this.scopedFinalizer.Dispose();
void IDisposable.Dispose()
{
this.interfaceManager.Draw -= this.OnDraw;
this.interfaceManager.BuildFonts -= this.OnBuildFonts;
this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers;
}
/// <summary>
/// Open the registered configuration UI, if it exists.
@ -553,12 +463,8 @@ public sealed class UiBuilder : IDisposable
this.ShowUi?.InvokeSafely();
}
// just in case, if something goes wrong, prevent drawing; otherwise it probably will crash.
if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully
&& (this.BuildFonts is not null || this.AfterBuildFonts is not null))
{
if (!this.interfaceManager.FontsReady)
return;
}
ImGui.PushID(this.namespaceName);
if (DoStats)
@ -620,28 +526,14 @@ public sealed class UiBuilder : IDisposable
this.hitchDetector.Stop();
}
private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e)
private void OnBuildFonts()
{
if (e.IsAsyncBuildOperation)
return;
e.OnPreBuild(
_ =>
{
var prev = ImGui.GetIO().NativePtr->Fonts;
ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr;
this.BuildFonts?.InvokeSafely();
ImGui.GetIO().NativePtr->Fonts = prev;
});
}
e.OnPostBuild(
_ =>
private void OnAfterBuildFonts()
{
var prev = ImGui.GetIO().NativePtr->Fonts;
ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr;
this.AfterBuildFonts?.InvokeSafely();
ImGui.GetIO().NativePtr->Fonts = prev;
});
}
private void OnResizeBuffers()

View file

@ -1,15 +1,10 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Reactive.Disposables;
using System.Runtime.InteropServices;
using System.Text.Unicode;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using ImGuiScene;
@ -36,7 +31,8 @@ public static class ImGuiHelpers
/// This does not necessarily mean you can call drawing functions.
/// </summary>
public static unsafe bool IsImGuiInitialized =>
ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null;
ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary
&& ImGui.GetIO().NativePtr is not null;
/// <summary>
/// Gets the global Dalamud scale; even available before drawing is ready.<br />
@ -202,7 +198,7 @@ public static class ImGuiHelpers
/// <param name="round">If a positive number is given, numbers will be rounded to this.</param>
public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f)
{
Func<float, float> rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x;
Func<float, float> rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x;
var font = fontPtr.NativePtr;
font->FontSize = rounder(font->FontSize * scale);
@ -314,7 +310,6 @@ public static class ImGuiHelpers
glyph->U1,
glyph->V1,
glyph->AdvanceX * scale);
target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint);
changed = true;
}
else if (!missingOnly)
@ -348,6 +343,14 @@ public static class ImGuiHelpers
}
if (changed && rebuildLookupTable)
target.BuildLookupTableNonstandard();
}
/// <summary>
/// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions.
/// </summary>
/// <param name="font">The font.</param>
public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font)
{
// ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph.
// FallbackGlyph is resolved after resolving ' '.
@ -356,10 +359,9 @@ public static class ImGuiHelpers
// On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null,
// making ImGui attempt to treat whatever was there as a ' '.
// This may cause random glyphs to be sized randomly, if not an access violation exception.
target.NativePtr->FallbackGlyph = null;
font.NativePtr->FallbackGlyph = null;
target.BuildLookupTable();
}
font.BuildLookupTable();
}
/// <summary>
@ -405,103 +407,6 @@ public static class ImGuiHelpers
public static void CenterCursorFor(float itemWidth) =>
ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2));
/// <summary>
/// Allocates memory on the heap using <see cref="ImGuiNative.igMemAlloc"/><br />
/// Memory must be freed using <see cref="ImGuiNative.igMemFree"/>.
/// <br />
/// Note that null is a valid return value when <paramref name="length"/> is 0.
/// </summary>
/// <param name="length">The length of allocated memory.</param>
/// <returns>The allocated memory.</returns>
/// <exception cref="OutOfMemoryException">If <see cref="ImGuiNative.igMemAlloc"/> returns null.</exception>
public static unsafe void* AllocateMemory(int length)
{
// TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint.
// fix that in ImGui.NET.
switch (length)
{
case 0:
return null;
case < 0:
throw new ArgumentOutOfRangeException(
nameof(length),
length,
$"{nameof(length)} cannot be a negative number.");
default:
var memory = ImGuiNative.igMemAlloc((uint)length);
if (memory is null)
{
throw new OutOfMemoryException(
$"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}");
}
return memory;
}
}
/// <summary>
/// Creates a new instance of <see cref="ImFontGlyphRangesBuilderPtr"/> with a natively backed memory.
/// </summary>
/// <param name="builder">The created instance.</param>
/// <returns>Disposable you can call.</returns>
public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder)
{
builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder());
var ptr = builder.NativePtr;
return Disposable.Create(() =>
{
if (ptr != null)
ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr);
ptr = null;
});
}
/// <summary>
/// Builds ImGui Glyph Ranges for use with <see cref="SafeFontConfig.GlyphRanges"/>.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>When disposed, the resource allocated for the range will be freed.</returns>
public static unsafe ushort[] BuildRangesToArray(
this ImFontGlyphRangesBuilderPtr builder,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true)
{
if (addFallbackCodepoints)
builder.AddText(FontAtlasFactory.FallbackCodepoints);
if (addEllipsisCodepoints)
{
builder.AddText(FontAtlasFactory.EllipsisCodepoints);
builder.AddChar('.');
}
builder.BuildRanges(out var vec);
return new ReadOnlySpan<ushort>((void*)vec.Data, vec.Size).ToArray();
}
/// <inheritdoc cref="CreateImGuiRangesFrom(IEnumerable{UnicodeRange})"/>
public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges)
=> CreateImGuiRangesFrom((IEnumerable<UnicodeRange>)ranges);
/// <summary>
/// Creates glyph ranges from <see cref="UnicodeRange"/>.<br />
/// Use values from <see cref="UnicodeRanges"/>.
/// </summary>
/// <param name="ranges">The unicode ranges.</param>
/// <returns>The range array that can be used for <see cref="SafeFontConfig.GlyphRanges"/>.</returns>
public static ushort[] CreateImGuiRangesFrom(IEnumerable<UnicodeRange> ranges) =>
ranges
.Where(x => x.FirstCodePoint <= ushort.MaxValue)
.SelectMany(
x => new[]
{
(ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue),
(ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue),
})
.Append((ushort)0)
.ToArray();
/// <summary>
/// Determines whether <paramref name="ptr"/> is empty.
/// </summary>
@ -510,7 +415,7 @@ public static class ImGuiHelpers
public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null;
/// <summary>
/// Determines whether <paramref name="ptr"/> is empty.
/// Determines whether <paramref name="ptr"/> is not null and loaded.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is empty.</returns>
@ -523,27 +428,6 @@ public static class ImGuiHelpers
/// <returns>Whether it is empty.</returns>
public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null;
/// <summary>
/// If <paramref name="self"/> is default, then returns <paramref name="other"/>.
/// </summary>
/// <param name="self">The self.</param>
/// <param name="other">The other.</param>
/// <returns><paramref name="self"/> if it is not default; otherwise, <paramref name="other"/>.</returns>
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
self.NativePtr is null ? other : self;
/// <summary>
/// Mark 4K page as used, after adding a codepoint to a font.
/// </summary>
/// <param name="font">The font.</param>
/// <param name="codepoint">The codepoint.</param>
internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint)
{
// Mark 4K page as used
var pageIndex = unchecked((ushort)(codepoint / 4096));
font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7)));
}
/// <summary>
/// Finds the corresponding ImGui viewport ID for the given window handle.
/// </summary>
@ -564,89 +448,6 @@ public static class ImGuiHelpers
return -1;
}
/// <summary>
/// Attempts to validate that <paramref name="fontPtr"/> is valid.
/// </summary>
/// <param name="fontPtr">The font pointer.</param>
/// <returns>The exception, if any occurred during validation.</returns>
internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr)
{
try
{
var font = fontPtr.NativePtr;
if (font is null)
throw new NullReferenceException("The font is null.");
_ = Marshal.ReadIntPtr((nint)font);
if (font->IndexedHotData.Data != 0)
_ = Marshal.ReadIntPtr(font->IndexedHotData.Data);
if (font->FrequentKerningPairs.Data != 0)
_ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data);
if (font->IndexLookup.Data != 0)
_ = Marshal.ReadIntPtr(font->IndexLookup.Data);
if (font->Glyphs.Data != 0)
_ = Marshal.ReadIntPtr(font->Glyphs.Data);
if (font->KerningPairs.Data != 0)
_ = Marshal.ReadIntPtr(font->KerningPairs.Data);
if (font->ConfigDataCount == 0 && font->ConfigData is not null)
throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?");
if (font->ConfigDataCount != 0 && font->ConfigData is null)
throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?");
if (font->ConfigData is not null)
_ = Marshal.ReadIntPtr((nint)font->ConfigData);
if (font->FallbackGlyph is not null
&& ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data))
throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data");
if (font->FallbackHotData is not null
&& ((nint)font->FallbackHotData < font->IndexedHotData.Data
|| (nint)font->FallbackHotData >= font->IndexedHotData.Data))
throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data");
if (font->ContainerAtlas is not null)
_ = Marshal.ReadIntPtr((nint)font->ContainerAtlas);
}
catch (Exception e)
{
return e;
}
return null;
}
/// <summary>
/// Updates the fallback char of <paramref name="font"/>.
/// </summary>
/// <param name="font">The font.</param>
/// <param name="c">The fallback character.</param>
internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c)
{
font.FallbackChar = c;
font.NativePtr->FallbackHotData =
(ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar);
}
/// <summary>
/// Determines if the supplied codepoint is inside the given range,
/// in format of <see cref="ImFontConfig.GlyphRanges"/>.
/// </summary>
/// <param name="codepoint">The codepoint.</param>
/// <param name="rangePtr">The ranges.</param>
/// <returns>Whether it is the case.</returns>
internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr)
{
if (codepoint is <= 0 or >= ushort.MaxValue)
return false;
while (*rangePtr != 0)
{
var from = *rangePtr++;
var to = *rangePtr++;
if (from <= codepoint && codepoint <= to)
return true;
}
return false;
}
/// <summary>
/// Get data needed for each new frame.
/// </summary>