diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 66c2745c5..957be12b9 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using Dalamud.Game.Text;
+using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Style;
using Dalamud.IoC.Internal;
@@ -145,7 +146,13 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
///
/// Gets or sets a value indicating whether to use AXIS fonts from the game.
///
- public bool UseAxisFontsFromGame { get; set; } = false;
+ [Obsolete($"See {nameof(DefaultFontSpec)}")]
+ public bool UseAxisFontsFromGame { get; set; } = true;
+
+ ///
+ /// Gets or sets the default font spec.
+ ///
+ public IFontSpec? DefaultFontSpec { get; set; }
///
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
diff --git a/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs
new file mode 100644
index 000000000..a6d40e4b7
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+
+using Dalamud.Interface.ManagedFontAtlas;
+using Dalamud.Storage.Assets;
+
+using ImGuiNET;
+
+using Newtonsoft.Json;
+
+using TerraFX.Interop.DirectX;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents a font from Dalamud assets.
+///
+public sealed class DalamudAssetFontAndFamilyId : IFontFamilyId, IFontId
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The font asset.
+ public DalamudAssetFontAndFamilyId(DalamudAsset asset)
+ {
+ if (asset.GetPurpose() != DalamudAssetPurpose.Font)
+ throw new ArgumentOutOfRangeException(nameof(asset), asset, "The specified asset is not a font asset.");
+ this.Asset = asset;
+ }
+
+ ///
+ /// Gets the font asset.
+ ///
+ [JsonProperty]
+ public DalamudAsset Asset { get; init; }
+
+ ///
+ [JsonIgnore]
+ public string EnglishName => $"Dalamud: {this.Asset}";
+
+ ///
+ [JsonIgnore]
+ public IReadOnlyDictionary? LocaleNames => null;
+
+ ///
+ [JsonIgnore]
+ public IReadOnlyList Fonts => new List { this }.AsReadOnly();
+
+ ///
+ [JsonIgnore]
+ public IFontFamilyId Family => this;
+
+ ///
+ [JsonIgnore]
+ public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
+
+ ///
+ [JsonIgnore]
+ public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
+
+ ///
+ [JsonIgnore]
+ public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
+
+ public static bool operator ==(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) =>
+ Equals(left, right);
+
+ public static bool operator !=(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) =>
+ !Equals(left, right);
+
+ ///
+ public override bool Equals(object? obj) => obj is DalamudAssetFontAndFamilyId other && this.Equals(other);
+
+ ///
+ public override int GetHashCode() => (int)this.Asset;
+
+ ///
+ public override string ToString() => $"{nameof(DalamudAssetFontAndFamilyId)}:{this.Asset}";
+
+ ///
+ public int FindBestMatch(int weight, int stretch, int style) => 0;
+
+ ///
+ public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
+ tk.AddDalamudAssetFont(this.Asset, config);
+
+ private bool Equals(DalamudAssetFontAndFamilyId other) => this.Asset == other.Asset;
+}
diff --git a/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs
new file mode 100644
index 000000000..7c6a69622
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+
+using Dalamud.Interface.ManagedFontAtlas;
+
+using ImGuiNET;
+
+using Newtonsoft.Json;
+
+using TerraFX.Interop.DirectX;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents the default Dalamud font.
+///
+public sealed class DalamudDefaultFontAndFamilyId : IFontId, IFontFamilyId
+{
+ ///
+ /// The shared instance of .
+ ///
+ public static readonly DalamudDefaultFontAndFamilyId Instance = new();
+
+ private DalamudDefaultFontAndFamilyId()
+ {
+ }
+
+ ///
+ [JsonIgnore]
+ public string EnglishName => "(Default)";
+
+ ///
+ [JsonIgnore]
+ public IReadOnlyDictionary? LocaleNames => null;
+
+ ///
+ [JsonIgnore]
+ public IFontFamilyId Family => this;
+
+ ///
+ [JsonIgnore]
+ public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
+
+ ///
+ [JsonIgnore]
+ public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
+
+ ///
+ [JsonIgnore]
+ public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
+
+ ///
+ [JsonIgnore]
+ public IReadOnlyList Fonts => new List { this }.AsReadOnly();
+
+ public static bool operator ==(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) =>
+ left is null == right is null;
+
+ public static bool operator !=(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) =>
+ left is null != right is null;
+
+ ///
+ public override bool Equals(object? obj) => obj is DalamudDefaultFontAndFamilyId;
+
+ ///
+ public override int GetHashCode() => 12345678;
+
+ ///
+ public override string ToString() => nameof(DalamudDefaultFontAndFamilyId);
+
+ ///
+ public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
+ => tk.AddDalamudDefaultFont(config.SizePx, config.GlyphRanges);
+ // TODO: mergeFont
+
+ ///
+ public int FindBestMatch(int weight, int stretch, int style) => 0;
+}
diff --git a/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs
new file mode 100644
index 000000000..dd4ba0d66
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+
+using Dalamud.Interface.GameFonts;
+using Dalamud.Interface.ManagedFontAtlas;
+
+using ImGuiNET;
+
+using Newtonsoft.Json;
+
+using TerraFX.Interop.DirectX;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents a font from the game.
+///
+public sealed class GameFontAndFamilyId : IFontId, IFontFamilyId
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The game font family.
+ public GameFontAndFamilyId(GameFontFamily family) => this.GameFontFamily = family;
+
+ ///
+ /// Gets the game font family.
+ ///
+ [JsonProperty]
+ public GameFontFamily GameFontFamily { get; init; }
+
+ ///
+ [JsonIgnore]
+ public string EnglishName => $"Game: {Enum.GetName(this.GameFontFamily) ?? throw new NotSupportedException()}";
+
+ ///
+ [JsonIgnore]
+ public IReadOnlyDictionary? LocaleNames => null;
+
+ ///
+ [JsonIgnore]
+ public IFontFamilyId Family => this;
+
+ ///
+ [JsonIgnore]
+ public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
+
+ ///
+ [JsonIgnore]
+ public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
+
+ ///
+ [JsonIgnore]
+ public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
+
+ ///
+ [JsonIgnore]
+ public IReadOnlyList Fonts => new List { this }.AsReadOnly();
+
+ public static bool operator ==(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => Equals(left, right);
+
+ public static bool operator !=(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => !Equals(left, right);
+
+ ///
+ public override bool Equals(object? obj) =>
+ ReferenceEquals(this, obj) || (obj is GameFontAndFamilyId other && this.Equals(other));
+
+ ///
+ public override int GetHashCode() => (int)this.GameFontFamily;
+
+ ///
+ public int FindBestMatch(int weight, int stretch, int style) => 0;
+
+ ///
+ public override string ToString() => $"{nameof(GameFontAndFamilyId)}:{this.GameFontFamily}";
+
+ ///
+ public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
+ tk.AddGameGlyphs(new(this.GameFontFamily, config.SizePx), config.GlyphRanges, config.MergeFont);
+
+ private bool Equals(GameFontAndFamilyId other) => this.GameFontFamily == other.GameFontFamily;
+}
diff --git a/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs
new file mode 100644
index 000000000..991716f74
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs
@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+
+using Dalamud.Interface.GameFonts;
+using Dalamud.Utility;
+
+using Newtonsoft.Json;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents a font family identifier.
+/// Not intended for plugins to implement.
+///
+public interface IFontFamilyId : IObjectWithLocalizableName
+{
+ ///
+ /// Gets the list of fonts under this family.
+ ///
+ [JsonIgnore]
+ IReadOnlyList Fonts { get; }
+
+ ///
+ /// Finds the index of the font inside that best matches the given parameters.
+ ///
+ /// The weight of the font.
+ /// The stretch of the font.
+ /// The style of the font.
+ /// The index of the font. Guaranteed to be a valid index.
+ int FindBestMatch(int weight, int stretch, int style);
+
+ ///
+ /// Gets the list of Dalamud-provided fonts.
+ ///
+ /// The list of fonts.
+ public static List ListDalamudFonts() =>
+ new()
+ {
+ new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
+ new DalamudAssetFontAndFamilyId(DalamudAsset.InconsolataRegular),
+ new DalamudAssetFontAndFamilyId(DalamudAsset.FontAwesomeFreeSolid),
+ };
+
+ ///
+ /// Gets the list of Game-provided fonts.
+ ///
+ /// The list of fonts.
+ public static List ListGameFonts() => new()
+ {
+ new GameFontAndFamilyId(GameFontFamily.Axis),
+ new GameFontAndFamilyId(GameFontFamily.Jupiter),
+ new GameFontAndFamilyId(GameFontFamily.JupiterNumeric),
+ new GameFontAndFamilyId(GameFontFamily.Meidinger),
+ new GameFontAndFamilyId(GameFontFamily.MiedingerMid),
+ new GameFontAndFamilyId(GameFontFamily.TrumpGothic),
+ };
+
+ ///
+ /// Gets the list of System-provided fonts.
+ ///
+ /// If true, try to refresh the list.
+ /// The list of fonts.
+ public static unsafe List ListSystemFonts(bool refresh)
+ {
+ using var dwf = default(ComPtr);
+ fixed (Guid* piid = &IID.IID_IDWriteFactory)
+ {
+ DirectX.DWriteCreateFactory(
+ DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
+ piid,
+ (IUnknown**)dwf.GetAddressOf()).ThrowOnError();
+ }
+
+ using var sfc = default(ComPtr);
+ dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), refresh).ThrowOnError();
+
+ var count = (int)sfc.Get()->GetFontFamilyCount();
+ var result = new List(count);
+ for (var i = 0; i < count; i++)
+ {
+ using var ff = default(ComPtr);
+ if (sfc.Get()->GetFontFamily((uint)i, ff.GetAddressOf()).FAILED)
+ {
+ // Ignore errors, if any
+ continue;
+ }
+
+ try
+ {
+ result.Add(SystemFontFamilyId.FromDWriteFamily(ff));
+ }
+ catch
+ {
+ // ignore
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/Dalamud/Interface/FontIdentifier/IFontId.cs b/Dalamud/Interface/FontIdentifier/IFontId.cs
new file mode 100644
index 000000000..4c611edf8
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/IFontId.cs
@@ -0,0 +1,40 @@
+using Dalamud.Interface.ManagedFontAtlas;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents a font identifier.
+/// Not intended for plugins to implement.
+///
+public interface IFontId : IObjectWithLocalizableName
+{
+ ///
+ /// Gets the associated font family.
+ ///
+ IFontFamilyId Family { get; }
+
+ ///
+ /// Gets the font weight, ranging from 1 to 999.
+ ///
+ int Weight { get; }
+
+ ///
+ /// Gets the font stretch, ranging from 1 to 9.
+ ///
+ int Stretch { get; }
+
+ ///
+ /// Gets the font style. Treat as an opaque value.
+ ///
+ int Style { get; }
+
+ ///
+ /// Adds this font to the given font build toolkit.
+ ///
+ /// The font build toolkit.
+ /// The font configuration. Some parameters may be ignored.
+ /// The added font.
+ ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config);
+}
diff --git a/Dalamud/Interface/FontIdentifier/IFontSpec.cs b/Dalamud/Interface/FontIdentifier/IFontSpec.cs
new file mode 100644
index 000000000..e4d931605
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/IFontSpec.cs
@@ -0,0 +1,50 @@
+using Dalamud.Interface.ManagedFontAtlas;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents a user's choice of font(s).
+/// Not intended for plugins to implement.
+///
+public interface IFontSpec
+{
+ ///
+ /// Gets the font size in pixels.
+ ///
+ float SizePx { get; }
+
+ ///
+ /// Gets the font size in points.
+ ///
+ float SizePt { get; }
+
+ ///
+ /// Gets the line height in pixels.
+ ///
+ float LineHeightPx { get; }
+
+ ///
+ /// Creates a font handle corresponding to this font specification.
+ ///
+ /// The atlas to bind this font handle to.
+ /// Optional callback to be called after creating the font handle.
+ /// The new font handle.
+ IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null);
+
+ ///
+ /// Adds this font to the given font build toolkit.
+ ///
+ /// The font build toolkit.
+ /// The font to merge to.
+ /// The added font.
+ ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default);
+
+ ///
+ /// Represents this font specification, preferrably in the requested locale.
+ ///
+ /// The locale code. Must be in lowercase(invariant).
+ /// The value.
+ string ToLocalizedString(string localeCode);
+}
diff --git a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs
new file mode 100644
index 000000000..2b970a5fd
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+
+using Dalamud.Utility;
+
+using TerraFX.Interop.DirectX;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents an object with localizable names.
+///
+public interface IObjectWithLocalizableName
+{
+ ///
+ /// Gets the name, preferrably in English.
+ ///
+ string EnglishName { get; }
+
+ ///
+ /// Gets the names per locales.
+ ///
+ IReadOnlyDictionary? LocaleNames { get; }
+
+ ///
+ /// Gets the name in the requested locale if available; otherwise, .
+ ///
+ /// The locale code. Must be in lowercase(invariant).
+ /// The value.
+ string GetLocalizedName(string localeCode)
+ {
+ if (this.LocaleNames is null)
+ return this.EnglishName;
+ if (this.LocaleNames.TryGetValue(localeCode, out var v))
+ return v;
+ foreach (var (a, b) in this.LocaleNames)
+ {
+ if (a.StartsWith(localeCode))
+ return b;
+ }
+
+ return this.EnglishName;
+ }
+
+ ///
+ /// Resolves all names per locales.
+ ///
+ /// The names.
+ /// A new dictionary mapping from locale code to localized names.
+ internal static unsafe IReadOnlyDictionary GetLocaleNames(IDWriteLocalizedStrings* fn)
+ {
+ var count = fn->GetCount();
+ var maxStrLen = 0u;
+ for (var i = 0u; i < count; i++)
+ {
+ var length = 0u;
+ fn->GetStringLength(i, &length).ThrowOnError();
+ maxStrLen = Math.Max(maxStrLen, length);
+ fn->GetLocaleNameLength(i, &length).ThrowOnError();
+ maxStrLen = Math.Max(maxStrLen, length);
+ }
+
+ maxStrLen++;
+ var buf = stackalloc char[(int)maxStrLen];
+ var result = new Dictionary((int)count);
+ for (var i = 0u; i < count; i++)
+ {
+ fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError();
+ var key = new string(buf);
+ fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError();
+ var value = new string(buf);
+ result[key.ToLowerInvariant()] = value;
+ }
+
+ return result;
+ }
+}
diff --git a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs
new file mode 100644
index 000000000..0604b22ea
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs
@@ -0,0 +1,155 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Text;
+
+using Dalamud.Interface.ManagedFontAtlas;
+using Dalamud.Interface.Utility;
+
+using ImGuiNET;
+
+using Newtonsoft.Json;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents a user's choice of a single font.
+///
+[SuppressMessage(
+ "StyleCop.CSharp.OrderingRules",
+ "SA1206:Declaration keywords should follow order",
+ Justification = "public required")]
+public record SingleFontSpec : IFontSpec
+{
+ ///
+ /// Gets the font id.
+ ///
+ [JsonProperty]
+ public required IFontId FontId { get; init; }
+
+ ///
+ [JsonProperty]
+ public float SizePx { get; init; } = 16;
+
+ ///
+ [JsonIgnore]
+ public float SizePt
+ {
+ get => (this.SizePx * 3) / 4;
+ init => this.SizePx = (value * 4) / 3;
+ }
+
+ ///
+ [JsonIgnore]
+ public float LineHeightPx => MathF.Round(this.SizePx * this.LineHeight);
+
+ ///
+ /// Gets the line height ratio to the font size.
+ ///
+ [JsonProperty]
+ public float LineHeight { get; init; } = 1f;
+
+ ///
+ /// Gets the glyph offset in pixels.
+ ///
+ [JsonProperty]
+ public Vector2 GlyphOffset { get; init; }
+
+ ///
+ /// Gets the letter spacing in pixels.
+ ///
+ [JsonProperty]
+ public float LetterSpacing { get; init; }
+
+ ///
+ /// Gets the glyph ranges.
+ ///
+ [JsonProperty]
+ public ushort[]? GlyphRanges { get; init; }
+
+ ///
+ public string ToLocalizedString(string localeCode)
+ {
+ var sb = new StringBuilder();
+ sb.Append(this.FontId.Family.GetLocalizedName(localeCode));
+ sb.Append($"({this.FontId.GetLocalizedName(localeCode)}, {this.SizePt}pt");
+ if (Math.Abs(this.LineHeight - 1f) > 0.000001f)
+ sb.Append($", LH={this.LineHeight:0.##}");
+ if (this.GlyphOffset != default)
+ sb.Append($", O={this.GlyphOffset.X:0.##},{this.GlyphOffset.Y:0.##}");
+ if (this.LetterSpacing != 0f)
+ sb.Append($", LS={this.LetterSpacing:0.##}");
+ sb.Append(')');
+ return sb.ToString();
+ }
+
+ ///
+ public override string ToString() => this.ToLocalizedString("en");
+
+ ///
+ public IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null) =>
+ atlas.NewDelegateFontHandle(tk =>
+ {
+ tk.OnPreBuild(e => e.Font = this.AddToBuildToolkit(e));
+ callback?.Invoke(tk);
+ });
+
+ ///
+ public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default)
+ {
+ var font = this.FontId.AddToBuildToolkit(
+ tk,
+ new()
+ {
+ SizePx = this.SizePx,
+ GlyphRanges = this.GlyphRanges,
+ MergeFont = mergeFont,
+ });
+
+ tk.RegisterPostBuild(
+ () =>
+ {
+ var roundUnit = tk.IsGlobalScaleIgnored(font) ? 1 : 1 / tk.Scale;
+ var newAscent = MathF.Round((font.Ascent * this.LineHeight) / roundUnit) * roundUnit;
+ var newFontSize = MathF.Round((font.FontSize * this.LineHeight) / roundUnit) * roundUnit;
+ var shiftDown = MathF.Round((newFontSize - font.FontSize) / 2f / roundUnit) * roundUnit;
+
+ font.Ascent = newAscent;
+ font.FontSize = newFontSize;
+ font.Descent = newFontSize - font.Ascent;
+
+ var lookup = new BitArray(ushort.MaxValue + 1, this.GlyphRanges is null);
+ if (this.GlyphRanges is not null)
+ {
+ for (var i = 0; i < this.GlyphRanges.Length && this.GlyphRanges[i] != 0; i += 2)
+ {
+ var to = (int)this.GlyphRanges[i + 1];
+ for (var j = this.GlyphRanges[i]; j <= to; j++)
+ lookup[j] = true;
+ }
+ }
+
+ // `/ roundUnit` = `* scale`
+ var dax = MathF.Round(this.LetterSpacing / roundUnit / roundUnit) * roundUnit;
+ var dxy0 = this.GlyphOffset / roundUnit;
+
+ dxy0 /= roundUnit;
+ dxy0.X = MathF.Round(dxy0.X);
+ dxy0.Y = MathF.Round(dxy0.Y);
+ dxy0 *= roundUnit;
+
+ dxy0.Y += shiftDown;
+ var dxy = new Vector4(dxy0, dxy0.X, dxy0.Y);
+ foreach (ref var glyphReal in font.GlyphsWrapped().DataSpan)
+ {
+ if (!lookup[glyphReal.Codepoint])
+ continue;
+
+ glyphReal.XY += dxy;
+ glyphReal.AdvanceX += dax;
+ }
+ });
+
+ return font;
+ }
+}
diff --git a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
new file mode 100644
index 000000000..420ee77a4
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
@@ -0,0 +1,181 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+using Dalamud.Utility;
+
+using Newtonsoft.Json;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents a font from system.
+///
+public sealed class SystemFontFamilyId : IFontFamilyId
+{
+ [JsonIgnore]
+ private IReadOnlyList? fontsLazy;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The font name in English.
+ /// The localized font name for display purposes.
+ [JsonConstructor]
+ internal SystemFontFamilyId(string englishName, IReadOnlyDictionary localeNames)
+ {
+ this.EnglishName = englishName;
+ this.LocaleNames = localeNames;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The localized font name for display purposes.
+ internal SystemFontFamilyId(IReadOnlyDictionary localeNames)
+ {
+ if (localeNames.TryGetValue("en-us", out var name))
+ this.EnglishName = name;
+ else if (localeNames.TryGetValue("en", out name))
+ this.EnglishName = name;
+ else
+ this.EnglishName = localeNames.Values.First();
+ this.LocaleNames = localeNames;
+ }
+
+ ///
+ [JsonProperty]
+ public string EnglishName { get; init; }
+
+ ///
+ [JsonProperty]
+ public IReadOnlyDictionary? LocaleNames { get; }
+
+ ///
+ [JsonIgnore]
+ public IReadOnlyList Fonts => this.fontsLazy ??= this.GetFonts();
+
+ public static bool operator ==(SystemFontFamilyId? left, SystemFontFamilyId? right) => Equals(left, right);
+
+ public static bool operator !=(SystemFontFamilyId? left, SystemFontFamilyId? right) => !Equals(left, right);
+
+ ///
+ public int FindBestMatch(int weight, int stretch, int style)
+ {
+ using var matchingFont = default(ComPtr);
+
+ var candidates = this.Fonts.ToList();
+ var minGap = int.MaxValue;
+ foreach (var c in candidates)
+ minGap = Math.Min(minGap, Math.Abs(c.Weight - weight));
+ candidates.RemoveAll(c => Math.Abs(c.Weight - weight) != minGap);
+
+ minGap = int.MaxValue;
+ foreach (var c in candidates)
+ minGap = Math.Min(minGap, Math.Abs(c.Stretch - stretch));
+ candidates.RemoveAll(c => Math.Abs(c.Stretch - stretch) != minGap);
+
+ if (candidates.Any(x => x.Style == style))
+ candidates.RemoveAll(x => x.Style != style);
+ else if (candidates.Any(x => x.Style == (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL))
+ candidates.RemoveAll(x => x.Style != (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL);
+
+ if (!candidates.Any())
+ return 0;
+
+ for (var i = 0; i < this.Fonts.Count; i++)
+ {
+ if (Equals(this.Fonts[i], candidates[0]))
+ return i;
+ }
+
+ return 0;
+ }
+
+ ///
+ public override string ToString() => $"{nameof(SystemFontFamilyId)}:{this.EnglishName}";
+
+ ///
+ public override bool Equals(object? obj) =>
+ ReferenceEquals(this, obj) || (obj is SystemFontFamilyId other && this.Equals(other));
+
+ ///
+ public override int GetHashCode() => this.EnglishName.GetHashCode();
+
+ ///
+ /// Create a new instance of from an .
+ ///
+ /// The family.
+ /// The new instance.
+ internal static unsafe SystemFontFamilyId FromDWriteFamily(ComPtr family)
+ {
+ using var fn = default(ComPtr);
+ family.Get()->GetFamilyNames(fn.GetAddressOf()).ThrowOnError();
+ return new(IObjectWithLocalizableName.GetLocaleNames(fn));
+ }
+
+ private unsafe IReadOnlyList GetFonts()
+ {
+ using var dwf = default(ComPtr);
+ fixed (Guid* piid = &IID.IID_IDWriteFactory)
+ {
+ DirectX.DWriteCreateFactory(
+ DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
+ piid,
+ (IUnknown**)dwf.GetAddressOf()).ThrowOnError();
+ }
+
+ using var sfc = default(ComPtr);
+ dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError();
+
+ var familyIndex = 0u;
+ BOOL exists = false;
+ fixed (void* pName = this.EnglishName)
+ sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError();
+ if (!exists)
+ throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");
+
+ using var family = default(ComPtr);
+ sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
+
+ var fontCount = (int)family.Get()->GetFontCount();
+ var fonts = new List(fontCount);
+ for (var i = 0; i < fontCount; i++)
+ {
+ using var font = default(ComPtr);
+ if (family.Get()->GetFont((uint)i, font.GetAddressOf()).FAILED)
+ {
+ // Ignore errors, if any
+ continue;
+ }
+
+ if (font.Get()->GetSimulations() != DWRITE_FONT_SIMULATIONS.DWRITE_FONT_SIMULATIONS_NONE)
+ {
+ // No simulation support
+ continue;
+ }
+
+ fonts.Add(new SystemFontId(this, font));
+ }
+
+ fonts.Sort(
+ (a, b) =>
+ {
+ var comp = a.Weight.CompareTo(b.Weight);
+ if (comp != 0)
+ return comp;
+
+ comp = a.Stretch.CompareTo(b.Stretch);
+ if (comp != 0)
+ return comp;
+
+ return a.Style.CompareTo(b.Style);
+ });
+ return fonts;
+ }
+
+ private bool Equals(SystemFontFamilyId other) => this.EnglishName == other.EnglishName;
+}
diff --git a/Dalamud/Interface/FontIdentifier/SystemFontId.cs b/Dalamud/Interface/FontIdentifier/SystemFontId.cs
new file mode 100644
index 000000000..0a350fc3a
--- /dev/null
+++ b/Dalamud/Interface/FontIdentifier/SystemFontId.cs
@@ -0,0 +1,163 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+using Dalamud.Interface.ManagedFontAtlas;
+using Dalamud.Utility;
+
+using ImGuiNET;
+
+using Newtonsoft.Json;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.FontIdentifier;
+
+///
+/// Represents a font installed in the system.
+///
+public sealed class SystemFontId : IFontId
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent font family.
+ /// The font.
+ internal unsafe SystemFontId(SystemFontFamilyId family, ComPtr font)
+ {
+ this.Family = family;
+ this.Weight = (int)font.Get()->GetWeight();
+ this.Stretch = (int)font.Get()->GetStretch();
+ this.Style = (int)font.Get()->GetStyle();
+
+ using var fn = default(ComPtr);
+ font.Get()->GetFaceNames(fn.GetAddressOf()).ThrowOnError();
+ this.LocaleNames = IObjectWithLocalizableName.GetLocaleNames(fn);
+ if (this.LocaleNames.TryGetValue("en-us", out var name))
+ this.EnglishName = name;
+ else if (this.LocaleNames.TryGetValue("en", out name))
+ this.EnglishName = name;
+ else
+ this.EnglishName = this.LocaleNames.Values.First();
+ }
+
+ [JsonConstructor]
+ private SystemFontId(string englishName, IReadOnlyDictionary localeNames, IFontFamilyId family)
+ {
+ this.EnglishName = englishName;
+ this.LocaleNames = localeNames;
+ this.Family = family;
+ }
+
+ ///
+ [JsonProperty]
+ public string EnglishName { get; init; }
+
+ ///
+ [JsonProperty]
+ public IReadOnlyDictionary? LocaleNames { get; }
+
+ ///
+ [JsonProperty]
+ public IFontFamilyId Family { get; init; }
+
+ ///
+ [JsonProperty]
+ public int Weight { get; init; } = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
+
+ ///
+ [JsonProperty]
+ public int Stretch { get; init; } = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
+
+ ///
+ [JsonProperty]
+ public int Style { get; init; } = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
+
+ public static bool operator ==(SystemFontId? left, SystemFontId? right) => Equals(left, right);
+
+ public static bool operator !=(SystemFontId? left, SystemFontId? right) => !Equals(left, right);
+
+ ///
+ public override bool Equals(object? obj) =>
+ ReferenceEquals(this, obj) || (obj is SystemFontId other && this.Equals(other));
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.Family, this.Weight, this.Stretch, this.Style);
+
+ ///
+ public override string ToString() =>
+ $"{nameof(SystemFontId)}:{this.Weight}:{this.Stretch}:{this.Style}:{this.Family}";
+
+ ///
+ public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
+ {
+ var (path, index) = this.GetFileAndIndex();
+ return tk.AddFontFromFile(path, config with { FontNo = index });
+ }
+
+ ///
+ /// Gets the file containing this font, and the font index within.
+ ///
+ /// The path and index.
+ public unsafe (string Path, int Index) GetFileAndIndex()
+ {
+ using var dwf = default(ComPtr);
+ fixed (Guid* piid = &IID.IID_IDWriteFactory)
+ {
+ DirectX.DWriteCreateFactory(
+ DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
+ piid,
+ (IUnknown**)dwf.GetAddressOf()).ThrowOnError();
+ }
+
+ using var sfc = default(ComPtr);
+ dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError();
+
+ var familyIndex = 0u;
+ BOOL exists = false;
+ fixed (void* name = this.Family.EnglishName)
+ sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError();
+ if (!exists)
+ throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found.");
+
+ using var family = default(ComPtr);
+ sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
+
+ using var font = default(ComPtr);
+ family.Get()->GetFirstMatchingFont(
+ (DWRITE_FONT_WEIGHT)this.Weight,
+ (DWRITE_FONT_STRETCH)this.Stretch,
+ (DWRITE_FONT_STYLE)this.Style,
+ font.GetAddressOf()).ThrowOnError();
+
+ using var fface = default(ComPtr);
+ font.Get()->CreateFontFace(fface.GetAddressOf()).ThrowOnError();
+ var fileCount = 0;
+ fface.Get()->GetFiles((uint*)&fileCount, null).ThrowOnError();
+ if (fileCount != 1)
+ throw new NotSupportedException();
+
+ using var ffile = default(ComPtr);
+ fface.Get()->GetFiles((uint*)&fileCount, ffile.GetAddressOf()).ThrowOnError();
+ void* refKey;
+ var refKeySize = 0u;
+ ffile.Get()->GetReferenceKey(&refKey, &refKeySize).ThrowOnError();
+
+ using var floader = default(ComPtr);
+ ffile.Get()->GetLoader(floader.GetAddressOf()).ThrowOnError();
+
+ using var flocal = default(ComPtr);
+ floader.As(&flocal).ThrowOnError();
+
+ var pathSize = 0u;
+ flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError();
+
+ var path = stackalloc char[(int)pathSize + 1];
+ flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError();
+ return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex());
+ }
+
+ private bool Equals(SystemFontId other) => this.Family.Equals(other.Family) && this.Weight == other.Weight &&
+ this.Stretch == other.Stretch && this.Style == other.Style;
+}
diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
new file mode 100644
index 000000000..410bf7d18
--- /dev/null
+++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
@@ -0,0 +1,1117 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.FontIdentifier;
+using Dalamud.Interface.ManagedFontAtlas;
+using Dalamud.Interface.Utility;
+using Dalamud.Utility;
+
+using ImGuiNET;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.ImGuiFontChooserDialog;
+
+///
+/// A dialog for choosing a font and its size.
+///
+[SuppressMessage(
+ "StyleCop.CSharp.LayoutRules",
+ "SA1519:Braces should not be omitted from multi-line child statement",
+ Justification = "Multiple fixed blocks")]
+public sealed class SingleFontChooserDialog : IDisposable
+{
+ private const float MinFontSizePt = 1;
+
+ private const float MaxFontSizePt = 127;
+
+ private static readonly List EmptyIFontList = new();
+
+ private static readonly (string Name, float Value)[] FontSizeList =
+ {
+ ("9.6", 9.6f),
+ ("10", 10f),
+ ("12", 12f),
+ ("14", 14f),
+ ("16", 16f),
+ ("18", 18f),
+ ("18.4", 18.4f),
+ ("20", 20),
+ ("23", 23),
+ ("34", 34),
+ ("36", 36),
+ ("40", 40),
+ ("45", 45),
+ ("46", 46),
+ ("68", 68),
+ ("90", 90),
+ };
+
+ private static int counterStatic;
+
+ private readonly int counter;
+ private readonly byte[] fontPreviewText = new byte[2048];
+ private readonly TaskCompletionSource tcs = new();
+ private readonly IFontAtlas atlas;
+
+ private string popupImGuiName;
+ private string title;
+
+ private bool firstDraw = true;
+ private bool firstDrawAfterRefresh;
+ private int setFocusOn = -1;
+
+ private bool useAdvancedOptions;
+ private AdvancedOptionsUiState advUiState;
+
+ private Task>? fontFamilies;
+ private int selectedFamilyIndex = -1;
+ private int selectedFontIndex = -1;
+ private int selectedFontWeight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
+ private int selectedFontStretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
+ private int selectedFontStyle = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
+
+ private string familySearch = string.Empty;
+ private string fontSearch = string.Empty;
+ private string fontSizeSearch = "12";
+ private IFontHandle? fontHandle;
+ private SingleFontSpec selectedFont;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A new instance of created using
+ /// as its auto-rebuild mode.
+ public SingleFontChooserDialog(IFontAtlas newAsyncAtlas)
+ {
+ this.counter = Interlocked.Increment(ref counterStatic);
+ this.title = "Choose a font...";
+ this.popupImGuiName = $"{this.title}##{nameof(SingleFontChooserDialog)}[{this.counter}]";
+ this.atlas = newAsyncAtlas;
+ this.selectedFont = new() { FontId = DalamudDefaultFontAndFamilyId.Instance };
+ Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText);
+ }
+
+ ///
+ /// Gets or sets the title of this font chooser dialog popup.
+ ///
+ public string Title
+ {
+ get => this.title;
+ set
+ {
+ this.title = value;
+ this.popupImGuiName = $"{this.title}##{nameof(SingleFontChooserDialog)}[{this.counter}]";
+ }
+ }
+
+ ///
+ /// Gets or sets the preview text. A text too long may be truncated on assignment.
+ ///
+ public string PreviewText
+ {
+ get
+ {
+ var n = this.fontPreviewText.AsSpan().IndexOf((byte)0);
+ return n < 0
+ ? Encoding.UTF8.GetString(this.fontPreviewText)
+ : Encoding.UTF8.GetString(this.fontPreviewText, 0, n);
+ }
+ set => Encoding.UTF8.GetBytes(value, this.fontPreviewText);
+ }
+
+ ///
+ /// Gets the task that resolves upon choosing a font or cancellation.
+ ///
+ public Task ResultTask => this.tcs.Task;
+
+ ///
+ /// Gets or sets the selected family and font.
+ ///
+ public SingleFontSpec SelectedFont
+ {
+ get => this.selectedFont;
+ set
+ {
+ this.selectedFont = value;
+
+ var familyName = value.FontId.Family.ToString() ?? string.Empty;
+ var fontName = value.FontId.ToString() ?? string.Empty;
+ this.familySearch = this.ExtractName(value.FontId.Family);
+ this.fontSearch = this.ExtractName(value.FontId);
+ if (this.fontFamilies?.IsCompletedSuccessfully is true)
+ this.UpdateSelectedFamilyAndFontIndices(this.fontFamilies.Result, familyName, fontName);
+ this.fontSizeSearch = $"{value.SizePt:0.##}";
+ this.advUiState = new(value);
+ this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001;
+ this.useAdvancedOptions |= value.GlyphOffset != default;
+ this.useAdvancedOptions |= value.LetterSpacing != 0f;
+ }
+ }
+
+ ///
+ /// Gets or sets the font family exclusion filter predicate.
+ ///
+ public Predicate? FontFamilyExcludeFilter { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to ignore the global scale on preview text input.
+ ///
+ public bool IgnorePreviewGlobalScale { get; set; }
+
+ ///
+ /// Creates a new instance of that will automatically draw and dispose itself as
+ /// needed.
+ ///
+ /// An instance of .
+ /// The new instance of .
+ public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder)
+ {
+ var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async));
+ uiBuilder.Draw += fcd.Draw;
+ fcd.tcs.Task.ContinueWith(
+ r =>
+ {
+ _ = r.Exception;
+ uiBuilder.Draw -= fcd.Draw;
+ fcd.Dispose();
+ });
+
+ return fcd;
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.fontHandle?.Dispose();
+ this.atlas.Dispose();
+ }
+
+ ///
+ /// Cancels this dialog.
+ ///
+ public void Cancel()
+ {
+ this.tcs.SetCanceled();
+ ImGui.GetIO().WantCaptureKeyboard = false;
+ ImGui.GetIO().WantTextInput = false;
+ }
+
+ ///
+ /// Draws this dialog.
+ ///
+ public void Draw()
+ {
+ if (this.firstDraw)
+ ImGui.OpenPopup(this.popupImGuiName);
+
+ ImGui.GetIO().WantCaptureKeyboard = true;
+ ImGui.GetIO().WantTextInput = true;
+ if (ImGui.IsKeyPressed(ImGuiKey.Escape))
+ {
+ this.Cancel();
+ return;
+ }
+
+ var open = true;
+ ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing);
+ if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open)
+ {
+ this.Cancel();
+ return;
+ }
+
+ var framePad = ImGui.GetStyle().FramePadding;
+ var windowPad = ImGui.GetStyle().WindowPadding;
+ var baseOffset = ImGui.GetCursorPos() - windowPad;
+
+ var actionSize = Vector2.Zero;
+ actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("OK"));
+ actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Cancel"));
+ actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Refresh"));
+ actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Reset"));
+ actionSize += framePad * 2;
+
+ var bodySize = ImGui.GetContentRegionAvail();
+ ImGui.SetCursorPos(baseOffset + windowPad);
+ if (ImGui.BeginChild(
+ "##choicesBlock",
+ bodySize with { X = bodySize.X - windowPad.X - actionSize.X },
+ false,
+ ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
+ {
+ this.DrawChoices();
+ }
+
+ ImGui.EndChild();
+
+ ImGui.SetCursorPos(baseOffset + windowPad + new Vector2(bodySize.X - actionSize.X, 0));
+
+ if (ImGui.BeginChild("##actionsBlock", bodySize with { X = actionSize.X }))
+ {
+ this.DrawActionButtons(actionSize);
+ }
+
+ ImGui.EndChild();
+
+ ImGui.EndPopup();
+
+ this.firstDraw = false;
+ this.firstDrawAfterRefresh = false;
+ }
+
+ private void DrawChoices()
+ {
+ var lineHeight = ImGui.GetTextLineHeight();
+ var previewHeight = (ImGui.GetFrameHeightWithSpacing() - lineHeight) +
+ Math.Max(lineHeight, this.selectedFont.LineHeightPx * 2);
+
+ var advancedOptionsHeight = ImGui.GetFrameHeightWithSpacing() * (this.useAdvancedOptions ? 4 : 1);
+
+ var tableSize = ImGui.GetContentRegionAvail() -
+ new Vector2(0, ImGui.GetStyle().WindowPadding.Y + previewHeight + advancedOptionsHeight);
+ if (ImGui.BeginChild(
+ "##tableContainer",
+ tableSize,
+ false,
+ ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
+ && ImGui.BeginTable("##table", 3, ImGuiTableFlags.None))
+ {
+ ImGui.PushStyleColor(ImGuiCol.TableHeaderBg, Vector4.Zero);
+ ImGui.PushStyleColor(ImGuiCol.HeaderHovered, Vector4.Zero);
+ ImGui.PushStyleColor(ImGuiCol.HeaderActive, Vector4.Zero);
+ ImGui.TableSetupColumn(
+ "Font:##familyColumn",
+ ImGuiTableColumnFlags.WidthStretch,
+ 0.4f);
+ ImGui.TableSetupColumn(
+ "Style:##fontColumn",
+ ImGuiTableColumnFlags.WidthStretch,
+ 0.4f);
+ ImGui.TableSetupColumn(
+ "Size:##sizeColumn",
+ ImGuiTableColumnFlags.WidthStretch,
+ 0.2f);
+ ImGui.TableHeadersRow();
+ ImGui.PopStyleColor(3);
+
+ ImGui.TableNextRow();
+
+ var pad = (int)MathF.Round(8 * ImGuiHelpers.GlobalScale);
+ ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(pad));
+ ImGui.TableNextColumn();
+ var changed = this.DrawFamilyListColumn();
+
+ ImGui.TableNextColumn();
+ changed |= this.DrawFontListColumn(changed);
+
+ ImGui.TableNextColumn();
+ changed |= this.DrawSizeListColumn();
+
+ if (changed)
+ {
+ this.fontHandle?.Dispose();
+ this.fontHandle = null;
+ }
+
+ ImGui.PopStyleVar();
+
+ ImGui.EndTable();
+ }
+
+ ImGui.EndChild();
+
+ ImGui.Checkbox("Show advanced options", ref this.useAdvancedOptions);
+ if (this.useAdvancedOptions)
+ {
+ if (this.DrawAdvancedOptions())
+ {
+ this.fontHandle?.Dispose();
+ this.fontHandle = null;
+ }
+ }
+
+ if (this.IgnorePreviewGlobalScale)
+ {
+ this.fontHandle ??= this.selectedFont.CreateFontHandle(
+ this.atlas,
+ tk =>
+ tk.OnPreBuild(e => e.IgnoreGlobalScale(e.Font))
+ .OnPostBuild(e => e.Font.AdjustGlyphMetrics(1f / e.Scale)));
+ }
+ else
+ {
+ this.fontHandle ??= this.selectedFont.CreateFontHandle(this.atlas);
+ }
+
+ if (this.fontHandle is null)
+ {
+ ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding);
+ ImGui.TextUnformatted("Select a font.");
+ }
+ else if (this.fontHandle.LoadException is { } loadException)
+ {
+ ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding);
+ ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
+ ImGui.TextUnformatted(loadException.Message);
+ ImGui.PopStyleColor();
+ }
+ else if (!this.fontHandle.Available)
+ {
+ ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding);
+ ImGui.TextUnformatted("Loading font...");
+ }
+ else
+ {
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+ using (this.fontHandle?.Push())
+ {
+ unsafe
+ {
+ fixed (byte* buf = this.fontPreviewText)
+ fixed (byte* label = "##fontPreviewText"u8)
+ {
+ ImGuiNative.igInputTextMultiline(
+ label,
+ buf,
+ (uint)this.fontPreviewText.Length,
+ ImGui.GetContentRegionAvail(),
+ ImGuiInputTextFlags.None,
+ null,
+ null);
+ }
+ }
+ }
+ }
+ }
+
+ private unsafe bool DrawFamilyListColumn()
+ {
+ if (this.fontFamilies?.IsCompleted is not true)
+ {
+ ImGui.SetScrollY(0);
+ ImGui.TextUnformatted("Loading...");
+ return false;
+ }
+
+ if (!this.fontFamilies.IsCompletedSuccessfully)
+ {
+ ImGui.SetScrollY(0);
+ ImGui.TextUnformatted("Error: " + this.fontFamilies.Exception);
+ return false;
+ }
+
+ var families = this.fontFamilies.Result;
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+
+ if (this.setFocusOn == 0)
+ {
+ this.setFocusOn = -1;
+ ImGui.SetKeyboardFocusHere();
+ }
+
+ var changed = false;
+ if (ImGui.InputText(
+ "##familySearch",
+ ref this.familySearch,
+ 255,
+ ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory,
+ data =>
+ {
+ if (families.Count == 0)
+ return 0;
+
+ var baseIndex = this.selectedFamilyIndex;
+ if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen)
+ {
+ switch (data->EventKey)
+ {
+ case ImGuiKey.DownArrow:
+ this.selectedFamilyIndex = (this.selectedFamilyIndex + 1) % families.Count;
+ changed = true;
+ break;
+ case ImGuiKey.UpArrow:
+ this.selectedFamilyIndex =
+ (this.selectedFamilyIndex + families.Count - 1) % families.Count;
+ changed = true;
+ break;
+ }
+
+ if (changed)
+ {
+ ImGuiHelpers.SetTextFromCallback(
+ data,
+ this.ExtractName(families[this.selectedFamilyIndex]));
+ }
+ }
+ else
+ {
+ switch (data->EventKey)
+ {
+ case ImGuiKey.DownArrow:
+ this.selectedFamilyIndex = families.FindIndex(
+ baseIndex + 1,
+ x => this.TestName(x, this.familySearch));
+ if (this.selectedFamilyIndex < 0)
+ {
+ this.selectedFamilyIndex = families.FindIndex(
+ 0,
+ baseIndex + 1,
+ x => this.TestName(x, this.familySearch));
+ }
+
+ changed = true;
+ break;
+ case ImGuiKey.UpArrow:
+ if (baseIndex > 0)
+ {
+ this.selectedFamilyIndex = families.FindLastIndex(
+ baseIndex - 1,
+ x => this.TestName(x, this.familySearch));
+ }
+
+ if (this.selectedFamilyIndex < 0)
+ {
+ if (baseIndex < 0)
+ baseIndex = 0;
+ this.selectedFamilyIndex = families.FindLastIndex(
+ families.Count - 1,
+ families.Count - baseIndex,
+ x => this.TestName(x, this.familySearch));
+ }
+
+ changed = true;
+ break;
+ }
+ }
+
+ return 0;
+ }))
+ {
+ if (!string.IsNullOrWhiteSpace(this.familySearch) && !changed)
+ {
+ this.selectedFamilyIndex = families.FindIndex(x => this.TestName(x, this.familySearch));
+ changed = true;
+ }
+ }
+
+ if (ImGui.BeginChild("##familyList", ImGui.GetContentRegionAvail()))
+ {
+ var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
+ var lineHeight = ImGui.GetTextLineHeightWithSpacing();
+
+ if ((changed || this.firstDrawAfterRefresh) && this.selectedFamilyIndex != -1)
+ {
+ ImGui.SetScrollY(
+ (lineHeight * this.selectedFamilyIndex) -
+ ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2));
+ }
+
+ clipper.Begin(families.Count, lineHeight);
+ while (clipper.Step())
+ {
+ for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ {
+ if (i < 0)
+ {
+ ImGui.TextUnformatted(" ");
+ continue;
+ }
+
+ var selected = this.selectedFamilyIndex == i;
+ if (ImGui.Selectable(
+ this.ExtractName(families[i]),
+ ref selected,
+ ImGuiSelectableFlags.DontClosePopups))
+ {
+ this.selectedFamilyIndex = families.IndexOf(families[i]);
+ this.familySearch = this.ExtractName(families[i]);
+ this.setFocusOn = 0;
+ changed = true;
+ }
+ }
+ }
+
+ clipper.Destroy();
+ }
+
+ if (changed && this.selectedFamilyIndex >= 0)
+ {
+ var family = families[this.selectedFamilyIndex];
+ using var matchingFont = default(ComPtr);
+ this.selectedFontIndex = family.FindBestMatch(
+ this.selectedFontWeight,
+ this.selectedFontStretch,
+ this.selectedFontStyle);
+ this.selectedFont = this.selectedFont with { FontId = family.Fonts[this.selectedFontIndex] };
+ }
+
+ ImGui.EndChild();
+ return changed;
+ }
+
+ private unsafe bool DrawFontListColumn(bool changed)
+ {
+ if (this.fontFamilies?.IsCompleted is not true)
+ {
+ ImGui.TextUnformatted("Loading...");
+ return changed;
+ }
+
+ if (!this.fontFamilies.IsCompletedSuccessfully)
+ {
+ ImGui.TextUnformatted("Error: " + this.fontFamilies.Exception);
+ return changed;
+ }
+
+ var families = this.fontFamilies.Result;
+ var family = this.selectedFamilyIndex >= 0
+ && this.selectedFamilyIndex < families.Count
+ ? families[this.selectedFamilyIndex]
+ : null;
+ var fonts = family?.Fonts ?? EmptyIFontList;
+
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+
+ if (this.setFocusOn == 1)
+ {
+ this.setFocusOn = -1;
+ ImGui.SetKeyboardFocusHere();
+ }
+
+ if (ImGui.InputText(
+ "##fontSearch",
+ ref this.fontSearch,
+ 255,
+ ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory,
+ data =>
+ {
+ if (fonts.Count == 0)
+ return 0;
+
+ var baseIndex = this.selectedFontIndex;
+ if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen)
+ {
+ switch (data->EventKey)
+ {
+ case ImGuiKey.DownArrow:
+ this.selectedFontIndex = (this.selectedFontIndex + 1) % fonts.Count;
+ changed = true;
+ break;
+ case ImGuiKey.UpArrow:
+ this.selectedFontIndex = (this.selectedFontIndex + fonts.Count - 1) % fonts.Count;
+ changed = true;
+ break;
+ }
+
+ if (changed)
+ {
+ ImGuiHelpers.SetTextFromCallback(
+ data,
+ this.ExtractName(fonts[this.selectedFontIndex]));
+ }
+ }
+ else
+ {
+ switch (data->EventKey)
+ {
+ case ImGuiKey.DownArrow:
+ this.selectedFontIndex = fonts.FindIndex(
+ baseIndex + 1,
+ x => this.TestName(x, this.fontSearch));
+ if (this.selectedFontIndex < 0)
+ {
+ this.selectedFontIndex = fonts.FindIndex(
+ 0,
+ baseIndex + 1,
+ x => this.TestName(x, this.fontSearch));
+ }
+
+ changed = true;
+ break;
+ case ImGuiKey.UpArrow:
+ if (baseIndex > 0)
+ {
+ this.selectedFontIndex = fonts.FindLastIndex(
+ baseIndex - 1,
+ x => this.TestName(x, this.fontSearch));
+ }
+
+ if (this.selectedFontIndex < 0)
+ {
+ if (baseIndex < 0)
+ baseIndex = 0;
+ this.selectedFontIndex = fonts.FindLastIndex(
+ fonts.Count - 1,
+ fonts.Count - baseIndex,
+ x => this.TestName(x, this.fontSearch));
+ }
+
+ changed = true;
+ break;
+ }
+ }
+
+ return 0;
+ }))
+ {
+ if (!string.IsNullOrWhiteSpace(this.fontSearch) && !changed)
+ {
+ this.selectedFontIndex = fonts.FindIndex(x => this.TestName(x, this.fontSearch));
+ changed = true;
+ }
+ }
+
+ if (ImGui.BeginChild("##fontList"))
+ {
+ var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
+ var lineHeight = ImGui.GetTextLineHeightWithSpacing();
+
+ if ((changed || this.firstDrawAfterRefresh) && this.selectedFontIndex != -1)
+ {
+ ImGui.SetScrollY(
+ (lineHeight * this.selectedFontIndex) -
+ ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2));
+ }
+
+ clipper.Begin(fonts.Count, lineHeight);
+ while (clipper.Step())
+ {
+ for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ {
+ if (i < 0)
+ {
+ ImGui.TextUnformatted(" ");
+ continue;
+ }
+
+ var selected = this.selectedFontIndex == i;
+ if (ImGui.Selectable(
+ this.ExtractName(fonts[i]),
+ ref selected,
+ ImGuiSelectableFlags.DontClosePopups))
+ {
+ this.selectedFontIndex = fonts.IndexOf(fonts[i]);
+ this.fontSearch = this.ExtractName(fonts[i]);
+ this.setFocusOn = 1;
+ changed = true;
+ }
+ }
+ }
+
+ clipper.Destroy();
+ }
+
+ ImGui.EndChild();
+
+ if (changed && family is not null && this.selectedFontIndex >= 0)
+ {
+ var font = family.Fonts[this.selectedFontIndex];
+ this.selectedFontWeight = font.Weight;
+ this.selectedFontStretch = font.Stretch;
+ this.selectedFontStyle = font.Style;
+ this.selectedFont = this.selectedFont with { FontId = font };
+ }
+
+ return changed;
+ }
+
+ private unsafe bool DrawSizeListColumn()
+ {
+ var changed = false;
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+
+ if (this.setFocusOn == 2)
+ {
+ this.setFocusOn = -1;
+ ImGui.SetKeyboardFocusHere();
+ }
+
+ if (ImGui.InputText(
+ "##fontSizeSearch",
+ ref this.fontSizeSearch,
+ 255,
+ ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory |
+ ImGuiInputTextFlags.CharsDecimal,
+ data =>
+ {
+ switch (data->EventKey)
+ {
+ case ImGuiKey.DownArrow:
+ this.selectedFont = this.selectedFont with
+ {
+ SizePt = Math.Min(MaxFontSizePt, MathF.Floor(this.selectedFont.SizePt) + 1),
+ };
+ changed = true;
+ break;
+ case ImGuiKey.UpArrow:
+ this.selectedFont = this.selectedFont with
+ {
+ SizePt = Math.Max(MinFontSizePt, MathF.Ceiling(this.selectedFont.SizePt) - 1),
+ };
+ changed = true;
+ break;
+ }
+
+ if (changed)
+ ImGuiHelpers.SetTextFromCallback(data, $"{this.selectedFont.SizePt:0.##}");
+
+ return 0;
+ }))
+ {
+ if (float.TryParse(this.fontSizeSearch, out var fontSizePt1))
+ {
+ this.selectedFont = this.selectedFont with { SizePt = fontSizePt1 };
+ changed = true;
+ }
+ }
+
+ if (ImGui.BeginChild("##fontSizeList"))
+ {
+ var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
+ var lineHeight = ImGui.GetTextLineHeightWithSpacing();
+
+ if (changed && this.selectedFontIndex != -1)
+ {
+ ImGui.SetScrollY(
+ (lineHeight * this.selectedFontIndex) -
+ ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2));
+ }
+
+ clipper.Begin(FontSizeList.Length, lineHeight);
+ while (clipper.Step())
+ {
+ for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ {
+ if (i < 0)
+ {
+ ImGui.TextUnformatted(" ");
+ continue;
+ }
+
+ var selected = Equals(FontSizeList[i].Value, this.selectedFont.SizePt);
+ if (ImGui.Selectable(
+ FontSizeList[i].Name,
+ ref selected,
+ ImGuiSelectableFlags.DontClosePopups))
+ {
+ this.selectedFont = this.selectedFont with { SizePt = FontSizeList[i].Value };
+ this.setFocusOn = 2;
+ changed = true;
+ }
+ }
+ }
+
+ clipper.Destroy();
+ }
+
+ ImGui.EndChild();
+
+ if (this.selectedFont.SizePt < MinFontSizePt)
+ {
+ this.selectedFont = this.selectedFont with { SizePt = MinFontSizePt };
+ changed = true;
+ }
+
+ if (this.selectedFont.SizePt > MaxFontSizePt)
+ {
+ this.selectedFont = this.selectedFont with { SizePt = MaxFontSizePt };
+ changed = true;
+ }
+
+ if (changed)
+ this.fontSizeSearch = $"{this.selectedFont.SizePt:0.##}";
+
+ return changed;
+ }
+
+ private bool DrawAdvancedOptions()
+ {
+ var changed = false;
+
+ if (!ImGui.BeginTable("##advancedOptions", 4))
+ return changed;
+
+ var labelWidth = ImGui.CalcTextSize("Letter Spacing:").X;
+ labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Offset:").X);
+ labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Line Height:").X);
+ labelWidth += ImGui.GetStyle().FramePadding.X;
+
+ var inputWidth = ImGui.CalcTextSize("000.000").X + (ImGui.GetStyle().FramePadding.X * 2);
+ ImGui.TableSetupColumn(
+ "##inputLabelColumn",
+ ImGuiTableColumnFlags.WidthFixed,
+ labelWidth);
+ ImGui.TableSetupColumn(
+ "##input1Column",
+ ImGuiTableColumnFlags.WidthFixed,
+ inputWidth);
+ ImGui.TableSetupColumn(
+ "##input2Column",
+ ImGuiTableColumnFlags.WidthFixed,
+ inputWidth);
+ ImGui.TableSetupColumn(
+ "##fillerColumn",
+ ImGuiTableColumnFlags.WidthStretch,
+ 1f);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted("Offset:");
+
+ ImGui.TableNextColumn();
+ if (FloatInputText(
+ "##glyphOffsetXInput",
+ ref this.advUiState.OffsetXText,
+ this.selectedFont.GlyphOffset.X) is { } newGlyphOffsetX)
+ {
+ changed = true;
+ this.selectedFont = this.selectedFont with
+ {
+ GlyphOffset = this.selectedFont.GlyphOffset with { X = newGlyphOffsetX },
+ };
+ }
+
+ ImGui.TableNextColumn();
+ if (FloatInputText(
+ "##glyphOffsetYInput",
+ ref this.advUiState.OffsetYText,
+ this.selectedFont.GlyphOffset.Y) is { } newGlyphOffsetY)
+ {
+ changed = true;
+ this.selectedFont = this.selectedFont with
+ {
+ GlyphOffset = this.selectedFont.GlyphOffset with { Y = newGlyphOffsetY },
+ };
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted("Letter Spacing:");
+
+ ImGui.TableNextColumn();
+ if (FloatInputText(
+ "##letterSpacingXInput",
+ ref this.advUiState.LetterSpacingText,
+ this.selectedFont.LetterSpacing) is { } newLetterSpacing)
+ {
+ changed = true;
+ this.selectedFont = this.selectedFont with { LetterSpacing = newLetterSpacing };
+ }
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted("Line Height:");
+
+ ImGui.TableNextColumn();
+ if (FloatInputText(
+ "##lineHeightInput",
+ ref this.advUiState.LineHeightText,
+ this.selectedFont.LineHeight,
+ 0.05f,
+ 0.1f,
+ 3f) is { } newLineHeight)
+ {
+ changed = true;
+ this.selectedFont = this.selectedFont with { LineHeight = newLineHeight };
+ }
+
+ ImGui.EndTable();
+ return changed;
+
+ static unsafe float? FloatInputText(
+ string label, ref string buf, float value, float step = 1f, float min = -127, float max = 127)
+ {
+ var stylePushed = value < min || value > max || !float.TryParse(buf, out _);
+ if (stylePushed)
+ ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
+
+ var changed2 = false;
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+ var changed1 = ImGui.InputText(
+ label,
+ ref buf,
+ 255,
+ ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory |
+ ImGuiInputTextFlags.CharsDecimal,
+ data =>
+ {
+ switch (data->EventKey)
+ {
+ case ImGuiKey.DownArrow:
+ changed2 = true;
+ value = Math.Min(max, (MathF.Round(value / step) * step) + step);
+ ImGuiHelpers.SetTextFromCallback(data, $"{value:0.##}");
+ break;
+ case ImGuiKey.UpArrow:
+ changed2 = true;
+ value = Math.Max(min, (MathF.Round(value / step) * step) - step);
+ ImGuiHelpers.SetTextFromCallback(data, $"{value:0.##}");
+ break;
+ }
+
+ return 0;
+ });
+
+ if (stylePushed)
+ ImGui.PopStyleColor();
+
+ if (!changed1 && !changed2)
+ return null;
+
+ if (!float.TryParse(buf, out var parsed))
+ return null;
+
+ if (min > parsed || parsed > max)
+ return null;
+
+ return parsed;
+ }
+ }
+
+ private void DrawActionButtons(Vector2 buttonSize)
+ {
+ if (this.fontHandle?.Available is not true
+ || this.FontFamilyExcludeFilter?.Invoke(this.selectedFont.FontId.Family) is true)
+ {
+ ImGui.BeginDisabled();
+ ImGui.Button("OK", buttonSize);
+ ImGui.EndDisabled();
+ }
+ else if (ImGui.Button("OK", buttonSize))
+ {
+ this.tcs.SetResult(this.selectedFont);
+ }
+
+ if (ImGui.Button("Cancel", buttonSize))
+ {
+ this.Cancel();
+ }
+
+ var doRefresh = false;
+ var isFirst = false;
+ if (this.fontFamilies?.IsCompleted is not true)
+ {
+ isFirst = doRefresh = this.fontFamilies is null;
+ ImGui.BeginDisabled();
+ ImGui.Button("Refresh", buttonSize);
+ ImGui.EndDisabled();
+ }
+ else if (ImGui.Button("Refresh", buttonSize))
+ {
+ doRefresh = true;
+ }
+
+ if (doRefresh)
+ {
+ this.fontFamilies =
+ this.fontFamilies?.ContinueWith(_ => RefreshBody())
+ ?? Task.Run(RefreshBody);
+ this.fontFamilies.ContinueWith(_ => this.firstDrawAfterRefresh = true);
+
+ List RefreshBody()
+ {
+ var familyName = this.selectedFont.FontId.Family.ToString() ?? string.Empty;
+ var fontName = this.selectedFont.FontId.ToString() ?? string.Empty;
+
+ var newFonts = new List { DalamudDefaultFontAndFamilyId.Instance };
+ newFonts.AddRange(IFontFamilyId.ListDalamudFonts());
+ newFonts.AddRange(IFontFamilyId.ListGameFonts());
+ var systemFonts = IFontFamilyId.ListSystemFonts(!isFirst);
+ systemFonts.Sort(
+ (a, b) => string.Compare(
+ this.ExtractName(a),
+ this.ExtractName(b),
+ StringComparison.CurrentCultureIgnoreCase));
+ newFonts.AddRange(systemFonts);
+ if (this.FontFamilyExcludeFilter is not null)
+ newFonts.RemoveAll(this.FontFamilyExcludeFilter);
+
+ this.UpdateSelectedFamilyAndFontIndices(newFonts, familyName, fontName);
+ return newFonts;
+ }
+ }
+
+ if (this.useAdvancedOptions)
+ {
+ if (ImGui.Button("Reset", buttonSize))
+ {
+ this.selectedFont = this.selectedFont with
+ {
+ LineHeight = 1f,
+ GlyphOffset = default,
+ LetterSpacing = default,
+ };
+
+ this.advUiState = new(this.selectedFont);
+ this.fontHandle?.Dispose();
+ this.fontHandle = null;
+ }
+ }
+ }
+
+ private void UpdateSelectedFamilyAndFontIndices(
+ IReadOnlyList fonts,
+ string familyName,
+ string fontName)
+ {
+ this.selectedFamilyIndex = fonts.FindIndex(x => x.ToString() == familyName);
+ if (this.selectedFamilyIndex == -1)
+ {
+ this.selectedFontIndex = -1;
+ }
+ else
+ {
+ this.selectedFontIndex = -1;
+ var family = fonts[this.selectedFamilyIndex];
+ for (var i = 0; i < family.Fonts.Count; i++)
+ {
+ if (family.Fonts[i].ToString() == fontName)
+ {
+ this.selectedFontIndex = i;
+ break;
+ }
+ }
+
+ if (this.selectedFontIndex == -1)
+ this.selectedFontIndex = 0;
+ this.selectedFont = this.selectedFont with
+ {
+ FontId = fonts[this.selectedFamilyIndex].Fonts[this.selectedFontIndex],
+ };
+ }
+ }
+
+ private string ExtractName(IObjectWithLocalizableName what) =>
+ what.GetLocalizedName(Service.Get().EffectiveLanguage);
+ // Note: EffectiveLanguage can be incorrect but close enough for now
+
+ private bool TestName(IObjectWithLocalizableName what, string search) =>
+ this.ExtractName(what).Contains(search, StringComparison.CurrentCultureIgnoreCase);
+
+ private struct AdvancedOptionsUiState
+ {
+ public string OffsetXText;
+ public string OffsetYText;
+ public string LetterSpacingText;
+ public string LineHeightText;
+
+ public AdvancedOptionsUiState(SingleFontSpec spec)
+ {
+ this.OffsetXText = $"{spec.GlyphOffset.X:0.##}";
+ this.OffsetYText = $"{spec.GlyphOffset.Y:0.##}";
+ this.LetterSpacingText = $"{spec.LetterSpacing:0.##}";
+ this.LineHeightText = $"{spec.LineHeight:0.##}";
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 6cf4a8b90..6d93b4bd7 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -705,13 +705,13 @@ internal class InterfaceManager : IDisposable, IServiceType
using (this.dalamudAtlas.SuppressAutoRebuild())
{
this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
- e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx)));
+ e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(-1)));
this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddFontAwesomeIconFont(
new()
{
- SizePx = DefaultFontSizePx,
+ SizePx = Service.Get().DefaultFontSpec.SizePx,
GlyphMinAdvanceX = DefaultFontSizePx,
GlyphMaxAdvanceX = DefaultFontSizePx,
})));
@@ -719,7 +719,10 @@ internal class InterfaceManager : IDisposable, IServiceType
e => e.OnPreBuild(
tk => tk.AddDalamudAssetFont(
DalamudAsset.InconsolataRegular,
- new() { SizePx = DefaultFontSizePx })));
+ new()
+ {
+ SizePx = Service.Get().DefaultFontSpec.SizePx,
+ })));
this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild(
tk =>
{
diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
index 53821d9df..1b9890a75 100644
--- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
@@ -152,8 +152,11 @@ internal class ConsoleWindow : Window, IDisposable
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f);
ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString);
}
-
- ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
+
+ var sendButtonSize = ImGui.CalcTextSize("Send") +
+ ((new Vector2(16, 0) + (ImGui.GetStyle().FramePadding * 2)) * ImGuiHelpers.GlobalScale);
+ var scrollingHeight = ImGui.GetContentRegionAvail().Y - sendButtonSize.Y;
+ ImGui.BeginChild("scrolling", new Vector2(0, scrollingHeight), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
if (this.clearLog) this.Clear();
@@ -173,9 +176,10 @@ internal class ConsoleWindow : Window, IDisposable
var childDrawList = ImGui.GetWindowDrawList();
var childSize = ImGui.GetWindowSize();
- var cursorDiv = ImGuiHelpers.GlobalScale * 93;
- var cursorLogLevel = ImGuiHelpers.GlobalScale * 100;
- var cursorLogLine = ImGuiHelpers.GlobalScale * 135;
+ var cursorDiv = ImGui.CalcTextSize("00:00:00.000 ").X;
+ var cursorLogLevel = ImGui.CalcTextSize("00:00:00.000 | ").X;
+ var dividerOffset = ImGui.CalcTextSize("00:00:00.000 | AAA ").X + (ImGui.CalcTextSize(" ").X / 2);
+ var cursorLogLine = ImGui.CalcTextSize("00:00:00.000 | AAA | ").X;
lock (this.renderLock)
{
@@ -242,8 +246,7 @@ internal class ConsoleWindow : Window, IDisposable
}
// Draw dividing line
- var offset = ImGuiHelpers.GlobalScale * 127;
- childDrawList.AddLine(new Vector2(childPos.X + offset, childPos.Y), new Vector2(childPos.X + offset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
+ childDrawList.AddLine(new Vector2(childPos.X + dividerOffset, childPos.Y), new Vector2(childPos.X + dividerOffset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
ImGui.EndChild();
@@ -261,7 +264,7 @@ internal class ConsoleWindow : Window, IDisposable
}
}
- ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (80.0f * ImGuiHelpers.GlobalScale) - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - sendButtonSize.X - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
var getFocus = false;
unsafe
@@ -280,7 +283,7 @@ internal class ConsoleWindow : Window, IDisposable
if (hadColor) ImGui.PopStyleColor();
- if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f)))
+ if (ImGui.Button("Send", sendButtonSize))
{
this.ProcessCommand();
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs
index b486cc7d9..84682e7c2 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs
@@ -5,7 +5,10 @@ using System.Numerics;
using System.Text;
using System.Threading.Tasks;
+using Dalamud.Game;
+using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
+using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
@@ -24,6 +27,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
{
private ImVectorWrapper testStringBuffer;
private IFontAtlas? privateAtlas;
+ private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance };
+ private IFontHandle? fontDialogHandle;
private IReadOnlyDictionary Handle)[]>? fontHandles;
private bool useGlobalScale;
private bool useWordWrap;
@@ -111,29 +116,32 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
if (ImGui.Button("Test Lock"))
Task.Run(this.TestLock);
- fixed (byte* labelPtr = "Test Input"u8)
+ ImGui.SameLine();
+ if (ImGui.Button("Choose Editor Font"))
{
- 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;
- }
+ var fcd = new SingleFontChooserDialog(
+ Service.Get().CreateFontAtlas(
+ $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
+ FontAtlasAutoRebuildMode.Async));
+ fcd.SelectedFont = this.fontSpec;
+ fcd.IgnorePreviewGlobalScale = !this.useGlobalScale;
+ Service.Get().Draw += fcd.Draw;
+ fcd.ResultTask.ContinueWith(
+ r => Service.Get().RunOnFrameworkThread(
+ () =>
+ {
+ Service.Get().Draw -= fcd.Draw;
+ fcd.Dispose();
- if (this.useMinimumBuild)
- _ = this.privateAtlas?.BuildFontsAsync();
- }
+ _ = r.Exception;
+ if (!r.IsCompletedSuccessfully)
+ return;
+
+ this.fontSpec = r.Result;
+ Log.Information("Selected font: {font}", this.fontSpec);
+ this.fontDialogHandle?.Dispose();
+ this.fontDialogHandle = null;
+ }));
}
this.privateAtlas ??=
@@ -141,6 +149,41 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
nameof(GamePrebakedFontsTestWidget),
FontAtlasAutoRebuildMode.Async,
this.useGlobalScale);
+ this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(this.privateAtlas);
+
+ fixed (byte* labelPtr = "Test Input"u8)
+ {
+ if (!this.useGlobalScale)
+ ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
+ using (this.fontDialogHandle.Push())
+ {
+ if (ImGuiNative.igInputTextMultiline(
+ labelPtr,
+ this.testStringBuffer.Data,
+ (uint)this.testStringBuffer.Capacity,
+ new(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 3),
+ 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();
+ }
+ }
+
+ if (!this.useGlobalScale)
+ ImGuiNative.igSetWindowFontScale(1);
+ }
+
this.fontHandles ??=
Enum.GetValues()
.Where(x => x.GetAttribute() is not null)
@@ -227,6 +270,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value))
.AggregateToDisposable().Dispose();
this.fontHandles = null;
+ this.fontDialogHandle?.Dispose();
+ this.fontDialogHandle = null;
this.privateAtlas?.Dispose();
this.privateAtlas = null;
}
diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
index c325028e1..47ba2c65f 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
@@ -68,11 +68,11 @@ internal class SettingsWindow : Window
var interfaceManager = Service.Get();
var fontAtlasFactory = Service.Get();
- var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame;
+ var rebuildFont = !Equals(fontAtlasFactory.DefaultFontSpec, configuration.DefaultFontSpec);
rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale);
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
- fontAtlasFactory.UseAxisOverride = null;
+ fontAtlasFactory.DefaultFontSpecOverride = null;
if (rebuildFont)
interfaceManager.RebuildFonts();
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
index 5293e13c4..ea6400121 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
@@ -5,9 +5,14 @@ using System.Text;
using CheapLoc;
using Dalamud.Configuration.Internal;
+using Dalamud.Game;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.FontIdentifier;
+using Dalamud.Interface.GameFonts;
+using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
+using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
@@ -21,31 +26,19 @@ public class SettingsTabLook : SettingsTab
{
private static readonly (string, float)[] GlobalUiScalePresets =
{
- ("9.6pt##DalamudSettingsGlobalUiScaleReset96", 9.6f / InterfaceManager.DefaultFontSizePt),
- ("12pt##DalamudSettingsGlobalUiScaleReset12", 12f / InterfaceManager.DefaultFontSizePt),
- ("14pt##DalamudSettingsGlobalUiScaleReset14", 14f / InterfaceManager.DefaultFontSizePt),
- ("18pt##DalamudSettingsGlobalUiScaleReset18", 18f / InterfaceManager.DefaultFontSizePt),
- ("24pt##DalamudSettingsGlobalUiScaleReset24", 24f / InterfaceManager.DefaultFontSizePt),
- ("36pt##DalamudSettingsGlobalUiScaleReset36", 36f / InterfaceManager.DefaultFontSizePt),
+ ("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f),
+ ("100%##DalamudSettingsGlobalUiScaleReset12", 1f),
+ ("117%##DalamudSettingsGlobalUiScaleReset14", 14 / 12f),
+ ("150%##DalamudSettingsGlobalUiScaleReset18", 1.5f),
+ ("200%##DalamudSettingsGlobalUiScaleReset24", 2f),
+ ("300%##DalamudSettingsGlobalUiScaleReset36", 3f),
};
private float globalUiScale;
+ private IFontSpec defaultFontSpec = null!;
public override SettingsEntry[] Entries { get; } =
{
- new GapSettingsEntry(5),
-
- new SettingsEntry(
- Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"),
- Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font."),
- c => c.UseAxisFontsFromGame,
- (v, c) => c.UseAxisFontsFromGame = v,
- v =>
- {
- Service.Get().UseAxisOverride = v;
- Service.Get().RebuildFonts();
- }),
-
new GapSettingsEntry(5, true),
new ButtonSettingsEntry(
@@ -178,10 +171,10 @@ public class SettingsTabLook : SettingsTab
}
}
- var globalUiScaleInPt = 12f * this.globalUiScale;
- if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp))
+ var globalUiScaleInPct = 100f * this.globalUiScale;
+ if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPct, 1f, 80f, 300f, "%.0f%%", ImGuiSliderFlags.AlwaysClamp))
{
- this.globalUiScale = globalUiScaleInPt / 12f;
+ this.globalUiScale = globalUiScaleInPct / 100f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
interfaceManager.RebuildFonts();
}
@@ -201,12 +194,53 @@ public class SettingsTabLook : SettingsTab
}
}
+ ImGuiHelpers.ScaledDummy(5);
+
+ if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font")))
+ {
+ var faf = Service.Get();
+ var fcd = new SingleFontChooserDialog(
+ faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async));
+ fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec;
+ fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId;
+ interfaceManager.Draw += fcd.Draw;
+ fcd.ResultTask.ContinueWith(
+ r => Service.Get().RunOnFrameworkThread(
+ () =>
+ {
+ interfaceManager.Draw -= fcd.Draw;
+ fcd.Dispose();
+
+ _ = r.Exception;
+ if (!r.IsCompletedSuccessfully)
+ return;
+
+ faf.DefaultFontSpecOverride = this.defaultFontSpec = r.Result;
+ interfaceManager.RebuildFonts();
+ }));
+ }
+
+ ImGui.SameLine();
+
+ using (interfaceManager.MonoFontHandle?.Push())
+ {
+ if (ImGui.Button(Loc.Localize("DalamudSettingResetDefaultFont", "Reset Default Font")))
+ {
+ var faf = Service.Get();
+ faf.DefaultFontSpecOverride =
+ this.defaultFontSpec =
+ new SingleFontSpec { FontId = new GameFontAndFamilyId(GameFontFamily.Axis) };
+ interfaceManager.RebuildFonts();
+ }
+ }
+
base.Draw();
}
public override void Load()
{
this.globalUiScale = Service.Get().GlobalUiScale;
+ this.defaultFontSpec = Service.Get().DefaultFontSpec;
base.Load();
}
@@ -214,6 +248,7 @@ public class SettingsTabLook : SettingsTab
public override void Save()
{
Service.Get().GlobalUiScale = this.globalUiScale;
+ Service.Get().DefaultFontSpec = this.defaultFontSpec;
base.Save();
}
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs
index a9c21f94e..0445499c8 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs
@@ -8,7 +8,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
///
-/// Wrapper for .
+/// Wrapper for .
+/// Not intended for plugins to implement.
///
public interface IFontAtlas : IDisposable
{
@@ -93,11 +94,15 @@ public interface IFontAtlas : IDisposable
///
/// Callback for .
/// Handle to a font that may or may not be ready yet.
+ ///
+ /// Consider calling to support
+ /// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users.
+ ///
///
/// On initialization:
///
/// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => {
- /// var config = new SafeFontConfig { SizePx = 16 };
+ /// var config = new SafeFontConfig { SizePx = UiBuilder.DefaultFontSizePx };
/// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config);
/// tk.AddGameSymbol(config);
/// tk.AddExtraGlyphsForDalamudLanguage(config);
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs
index f75ed4686..158366b12 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs
@@ -9,7 +9,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
///
-/// Common stuff for and .
+/// Common stuff for and .
+/// Not intended for plugins to implement.
///
public interface IFontAtlasBuildToolkit
{
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs
index eb7c7e08c..d824eca52 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs
@@ -5,7 +5,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
///
-/// Toolkit for use when the build state is .
+/// Toolkit for use when the build state is .
+/// Not intended for plugins to implement.
///
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
{
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs
index 38d8d2fe8..9ab480374 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs
@@ -1,6 +1,7 @@
using System.IO;
using System.Runtime.InteropServices;
+using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Utility;
@@ -10,6 +11,7 @@ namespace Dalamud.Interface.ManagedFontAtlas;
///
/// Toolkit for use when the build state is .
+/// Not intended for plugins to implement.
///
/// After returns,
/// either must be set,
@@ -52,6 +54,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// True if ignored.
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
+ ///
+ /// Registers a function to be run after build.
+ ///
+ /// The action to run.
+ void RegisterPostBuild(Action action);
+
///
/// Adds a font from memory region allocated using .
/// It WILL crash if you try to use a memory pointer allocated in some other way.
@@ -134,7 +142,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// As this involves adding multiple fonts, calling this function will set
/// as the return value of this function, if it was empty before.
///
- /// Font size in pixels.
+ ///
+ /// Font size in pixels.
+ /// If a negative value is supplied,
+ /// (. * ) will be
+ /// used as the font size. Specify -1 to use the default font size.
+ ///
/// The glyph ranges. Use .ToGlyphRange to build.
/// A font returned from .
ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null);
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
index 11c26616b..70799bb9c 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
@@ -5,7 +5,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
///
-/// Represents a reference counting handle for fonts.
+/// Represents a reference counting handle for fonts.
+/// Not intended for plugins to implement.
///
public interface IFontHandle : IDisposable
{
diff --git a/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs
index 9136d2723..a4cc3afa7 100644
--- a/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs
@@ -4,7 +4,8 @@ namespace Dalamud.Interface.ManagedFontAtlas;
///
/// The wrapper for , guaranteeing that the associated data will be available as long as
-/// this struct is not disposed.
+/// this struct is not disposed.
+/// Not intended for plugins to implement.
///
public interface ILockedImFont : IDisposable
{
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
index e2b096701..396c8b26a 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using System.Text.Unicode;
using Dalamud.Configuration.Internal;
+using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
@@ -42,6 +43,7 @@ internal sealed partial class FontAtlasFactory
private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance;
private readonly FontAtlasFactory factory;
private readonly FontAtlasBuiltData data;
+ private readonly List registeredPostBuildActions = new();
///
/// Initializes a new instance of the class.
@@ -162,6 +164,9 @@ internal sealed partial class FontAtlasFactory
///
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
this.data.AddNewTexture(textureWrap, disposeOnError);
+
+ ///
+ public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action);
///
public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
@@ -314,18 +319,32 @@ internal sealed partial class FontAtlasFactory
///
public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges)
{
- ImFontPtr font;
+ ImFontPtr font = default;
glyphRanges ??= this.factory.DefaultGlyphRanges;
- if (this.factory.UseAxis)
+
+ var dfid = this.factory.DefaultFontSpec;
+ if (sizePx < 0f)
+ sizePx *= -dfid.SizePx;
+
+ if (dfid is SingleFontSpec sfs)
{
- font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
+ if (sfs.FontId is DalamudDefaultFontAndFamilyId)
+ {
+ // invalid; calling sfs.AddToBuildToolkit calls this function, causing infinite recursion
+ }
+ else
+ {
+ sfs = sfs with { SizePx = sizePx };
+ font = sfs.AddToBuildToolkit(this);
+ if (sfs.FontId is not GameFontAndFamilyId { GameFontFamily: GameFontFamily.Axis })
+ this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
+ }
}
- else
+
+ if (font.IsNull())
{
- font = this.AddDalamudAssetFont(
- DalamudAsset.NotoSansJpMedium,
- new() { SizePx = sizePx, GlyphRanges = glyphRanges });
- this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
+ // fall back to AXIS fonts
+ font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
}
this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font });
@@ -531,6 +550,13 @@ internal sealed partial class FontAtlasFactory
substance.OnPostBuild(this);
}
+ public void PostBuildCallbacks()
+ {
+ foreach (var ac in this.registeredPostBuildActions)
+ ac.InvokeSafely();
+ this.registeredPostBuildActions.Clear();
+ }
+
public unsafe void UploadTextures()
{
var buf = Array.Empty();
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
index 4d636b8cf..4968bc891 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
@@ -658,7 +658,7 @@ internal sealed partial class FontAtlasFactory
toolkit = res.CreateToolkit(this.factory, isAsync);
// PreBuildSubstances deals with toolkit.Add... function family. Do this first.
- var defaultFont = toolkit.AddDalamudDefaultFont(InterfaceManager.DefaultFontSizePx, null);
+ var defaultFont = toolkit.AddDalamudDefaultFont(-1, null);
this.BuildStepChange?.Invoke(toolkit);
toolkit.PreBuildSubstances();
@@ -679,6 +679,7 @@ internal sealed partial class FontAtlasFactory
toolkit.PostBuild();
toolkit.PostBuildSubstances();
+ toolkit.PostBuildCallbacks();
this.BuildStepChange?.Invoke(toolkit);
foreach (var font in toolkit.Fonts)
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs
index 358ccd845..d3bc976f2 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
+using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Storage.Assets;
@@ -108,14 +109,29 @@ internal sealed partial class FontAtlasFactory
}
///
- /// Gets or sets a value indicating whether to override configuration for UseAxis.
+ /// Gets or sets a value indicating whether to override configuration for .
///
- public bool? UseAxisOverride { get; set; } = null;
+ public IFontSpec? DefaultFontSpecOverride { get; set; } = null;
///
- /// Gets a value indicating whether to use AXIS fonts.
+ /// Gets the default font ID.
///
- public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame;
+ public IFontSpec DefaultFontSpec =>
+ this.DefaultFontSpecOverride
+ ?? Service.Get().DefaultFontSpec
+#pragma warning disable CS0618 // Type or member is obsolete
+ ?? (Service.Get().UseAxisFontsFromGame
+#pragma warning restore CS0618 // Type or member is obsolete
+ ? new()
+ {
+ FontId = new GameFontAndFamilyId(GameFontFamily.Axis),
+ SizePx = InterfaceManager.DefaultFontSizePx,
+ }
+ : new SingleFontSpec
+ {
+ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
+ SizePx = InterfaceManager.DefaultFontSizePx + 1,
+ });
///
/// Gets the service instance of .
diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs
index cb7f7c65a..caa686856 100644
--- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs
@@ -26,7 +26,7 @@ public struct SafeFontConfig
this.PixelSnapH = true;
this.GlyphMaxAdvanceX = float.MaxValue;
this.RasterizerMultiply = 1f;
- this.RasterizerGamma = 1.4f;
+ this.RasterizerGamma = 1.7f;
this.EllipsisChar = unchecked((char)-1);
this.Raw.FontDataOwnedByAtlas = 1;
}
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index 55e11dfac..7a3eb6fb6 100644
--- a/Dalamud/Interface/UiBuilder.cs
+++ b/Dalamud/Interface/UiBuilder.cs
@@ -7,6 +7,7 @@ using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
+using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
@@ -173,12 +174,12 @@ public sealed class UiBuilder : IDisposable
///
/// Gets the default Dalamud font size in points.
///
- public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt;
+ public static float DefaultFontSizePt => Service.Get().DefaultFontSpec.SizePt;
///
/// Gets the default Dalamud font size in pixels.
///
- public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx;
+ public static float DefaultFontSizePx => Service.Get().DefaultFontSpec.SizePx;
///
/// Gets the default Dalamud font - supporting all game languages and icons.
@@ -198,6 +199,11 @@ public sealed class UiBuilder : IDisposable
///
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
+ ///
+ /// Gets the default font specifications.
+ ///
+ public IFontSpec DefaultFontSpec => Service.Get().DefaultFontSpec;
+
///
/// Gets the handle to the default Dalamud font - supporting all game languages and icons.
///
diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs
index 444463d41..f02effe1d 100644
--- a/Dalamud/Interface/Utility/ImGuiHelpers.cs
+++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Numerics;
using System.Reactive.Disposables;
using System.Runtime.InteropServices;
+using System.Text;
using System.Text.Unicode;
using Dalamud.Configuration.Internal;
@@ -543,6 +544,24 @@ public static class ImGuiHelpers
var pageIndex = unchecked((ushort)(codepoint / 4096));
font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7)));
}
+
+ ///
+ /// Sets the text for a text input, during the callback.
+ ///
+ /// The callback data.
+ /// The new text.
+ internal static unsafe void SetTextFromCallback(ImGuiInputTextCallbackData* data, string s)
+ {
+ if (data->BufTextLen != 0)
+ ImGuiNative.ImGuiInputTextCallbackData_DeleteChars(data, 0, data->BufTextLen);
+
+ var len = Encoding.UTF8.GetByteCount(s);
+ var buf = len < 1024 ? stackalloc byte[len] : new byte[len];
+ Encoding.UTF8.GetBytes(s, buf);
+ fixed (byte* pBuf = buf)
+ ImGuiNative.ImGuiInputTextCallbackData_InsertChars(data, 0, pBuf, pBuf + len);
+ ImGuiNative.ImGuiInputTextCallbackData_SelectAll(data);
+ }
///
/// Finds the corresponding ImGui viewport ID for the given window handle.
diff --git a/Dalamud/Utility/ArrayExtensions.cs b/Dalamud/Utility/ArrayExtensions.cs
index fa6e3dbe9..5b6ce2332 100644
--- a/Dalamud/Utility/ArrayExtensions.cs
+++ b/Dalamud/Utility/ArrayExtensions.cs
@@ -97,4 +97,76 @@ internal static class ArrayExtensions
/// casted as a if it is one; otherwise the result of .
public static IReadOnlyCollection AsReadOnlyCollection(this IEnumerable array) =>
array as IReadOnlyCollection ?? array.ToArray();
+
+ ///
+ public static int FindIndex(this IReadOnlyList list, Predicate match)
+ => list.FindIndex(0, list.Count, match);
+
+ ///
+ public static int FindIndex(this IReadOnlyList list, int startIndex, Predicate match)
+ => list.FindIndex(startIndex, list.Count - startIndex, match);
+
+ ///
+ public static int FindIndex(this IReadOnlyList list, int startIndex, int count, Predicate match)
+ {
+ if ((uint)startIndex > (uint)list.Count)
+ throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
+
+ if (count < 0 || startIndex > list.Count - count)
+ throw new ArgumentOutOfRangeException(nameof(count), count, null);
+
+ if (match == null)
+ throw new ArgumentNullException(nameof(match));
+
+ var endIndex = startIndex + count;
+ for (var i = startIndex; i < endIndex; i++)
+ {
+ if (match(list[i])) return i;
+ }
+
+ return -1;
+ }
+
+ ///
+ public static int FindLastIndex(this IReadOnlyList list, Predicate match)
+ => list.FindLastIndex(list.Count - 1, list.Count, match);
+
+ ///
+ public static int FindLastIndex(this IReadOnlyList list, int startIndex, Predicate match)
+ => list.FindLastIndex(startIndex, startIndex + 1, match);
+
+ ///
+ public static int FindLastIndex(this IReadOnlyList list, int startIndex, int count, Predicate match)
+ {
+ if (match == null)
+ throw new ArgumentNullException(nameof(match));
+
+ if (list.Count == 0)
+ {
+ // Special case for 0 length List
+ if (startIndex != -1)
+ throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
+ }
+ else
+ {
+ // Make sure we're not out of range
+ if ((uint)startIndex >= (uint)list.Count)
+ throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
+ }
+
+ // 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
+ if (count < 0 || startIndex - count + 1 < 0)
+ throw new ArgumentOutOfRangeException(nameof(count), count, null);
+
+ var endIndex = startIndex - count;
+ for (var i = startIndex; i > endIndex; i--)
+ {
+ if (match(list[i]))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
}
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index d53c2fe19..f5ad8b999 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -22,6 +22,9 @@ using Dalamud.Logging.Internal;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Serilog;
+
+using TerraFX.Interop.Windows;
+
using Windows.Win32.Storage.FileSystem;
namespace Dalamud.Utility;
@@ -684,6 +687,16 @@ public static class Util
return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString;
}
+ ///
+ /// Throws a corresponding exception if is true.
+ ///
+ /// The result value.
+ internal static void ThrowOnError(this HRESULT hr)
+ {
+ if (hr.FAILED)
+ Marshal.ThrowExceptionForHR(hr.Value);
+ }
+
///
/// Print formatted GameObject Information to ImGui.
///