Implement FontChooserDialog (#1637)

* Implement FontChooserDialog

* Minor fixes

* Fixes 2

* Add Reset default font button

* Add failsafe

* reduce uninteresting exception message

* Add remarks to use AttachExtraGlyphsForDalamudLanguage

* Support advanced font configuration options

* fixes

* Shift ui elements

* more fixes

* Add To(Localized)String for IFontSpec

* Untie GlobalFontScale from default font size

* Layout fixes

* Make UiBuilder.DefaultFontSize point to user configured value

* Update example for NewDelegateFontHandle

* Font interfaces: write notes on not intended for plugins to implement

* Update default gamma to 1.7 to match closer to prev behavior (1.4**2)

* Fix console window layout
This commit is contained in:
srkizer 2024-02-14 05:09:46 +09:00 committed by GitHub
parent 3b3823d4e6
commit 34daa73612
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 2478 additions and 81 deletions

View file

@ -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;
/// <summary>
/// Represents a font from Dalamud assets.
/// </summary>
public sealed class DalamudAssetFontAndFamilyId : IFontFamilyId, IFontId
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAssetFontAndFamilyId"/> class.
/// </summary>
/// <param name="asset">The font asset.</param>
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;
}
/// <summary>
/// Gets the font asset.
/// </summary>
[JsonProperty]
public DalamudAsset Asset { get; init; }
/// <inheritdoc/>
[JsonIgnore]
public string EnglishName => $"Dalamud: {this.Asset}";
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyDictionary<string, string>? LocaleNames => null;
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
/// <inheritdoc/>
[JsonIgnore]
public IFontFamilyId Family => this;
/// <inheritdoc/>
[JsonIgnore]
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
/// <inheritdoc/>
[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);
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is DalamudAssetFontAndFamilyId other && this.Equals(other);
/// <inheritdoc/>
public override int GetHashCode() => (int)this.Asset;
/// <inheritdoc/>
public override string ToString() => $"{nameof(DalamudAssetFontAndFamilyId)}:{this.Asset}";
/// <inheritdoc/>
public int FindBestMatch(int weight, int stretch, int style) => 0;
/// <inheritdoc/>
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
tk.AddDalamudAssetFont(this.Asset, config);
private bool Equals(DalamudAssetFontAndFamilyId other) => this.Asset == other.Asset;
}

View file

@ -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;
/// <summary>
/// Represents the default Dalamud font.
/// </summary>
public sealed class DalamudDefaultFontAndFamilyId : IFontId, IFontFamilyId
{
/// <summary>
/// The shared instance of <see cref="DalamudDefaultFontAndFamilyId"/>.
/// </summary>
public static readonly DalamudDefaultFontAndFamilyId Instance = new();
private DalamudDefaultFontAndFamilyId()
{
}
/// <inheritdoc/>
[JsonIgnore]
public string EnglishName => "(Default)";
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyDictionary<string, string>? LocaleNames => null;
/// <inheritdoc/>
[JsonIgnore]
public IFontFamilyId Family => this;
/// <inheritdoc/>
[JsonIgnore]
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { 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;
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is DalamudDefaultFontAndFamilyId;
/// <inheritdoc/>
public override int GetHashCode() => 12345678;
/// <inheritdoc/>
public override string ToString() => nameof(DalamudDefaultFontAndFamilyId);
/// <inheritdoc/>
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
=> tk.AddDalamudDefaultFont(config.SizePx, config.GlyphRanges);
// TODO: mergeFont
/// <inheritdoc/>
public int FindBestMatch(int weight, int stretch, int style) => 0;
}

View file

@ -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;
/// <summary>
/// Represents a font from the game.
/// </summary>
public sealed class GameFontAndFamilyId : IFontId, IFontFamilyId
{
/// <summary>
/// Initializes a new instance of the <see cref="GameFontAndFamilyId"/> class.
/// </summary>
/// <param name="family">The game font family.</param>
public GameFontAndFamilyId(GameFontFamily family) => this.GameFontFamily = family;
/// <summary>
/// Gets the game font family.
/// </summary>
[JsonProperty]
public GameFontFamily GameFontFamily { get; init; }
/// <inheritdoc/>
[JsonIgnore]
public string EnglishName => $"Game: {Enum.GetName(this.GameFontFamily) ?? throw new NotSupportedException()}";
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyDictionary<string, string>? LocaleNames => null;
/// <inheritdoc/>
[JsonIgnore]
public IFontFamilyId Family => this;
/// <inheritdoc/>
[JsonIgnore]
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
public static bool operator ==(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => Equals(left, right);
public static bool operator !=(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => !Equals(left, right);
/// <inheritdoc/>
public override bool Equals(object? obj) =>
ReferenceEquals(this, obj) || (obj is GameFontAndFamilyId other && this.Equals(other));
/// <inheritdoc/>
public override int GetHashCode() => (int)this.GameFontFamily;
/// <inheritdoc/>
public int FindBestMatch(int weight, int stretch, int style) => 0;
/// <inheritdoc/>
public override string ToString() => $"{nameof(GameFontAndFamilyId)}:{this.GameFontFamily}";
/// <inheritdoc/>
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;
}

View file

@ -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;
/// <summary>
/// Represents a font family identifier.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontFamilyId : IObjectWithLocalizableName
{
/// <summary>
/// Gets the list of fonts under this family.
/// </summary>
[JsonIgnore]
IReadOnlyList<IFontId> Fonts { get; }
/// <summary>
/// Finds the index of the font inside <see cref="Fonts"/> that best matches the given parameters.
/// </summary>
/// <param name="weight">The weight of the font.</param>
/// <param name="stretch">The stretch of the font.</param>
/// <param name="style">The style of the font.</param>
/// <returns>The index of the font. Guaranteed to be a valid index.</returns>
int FindBestMatch(int weight, int stretch, int style);
/// <summary>
/// Gets the list of Dalamud-provided fonts.
/// </summary>
/// <returns>The list of fonts.</returns>
public static List<IFontFamilyId> ListDalamudFonts() =>
new()
{
new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
new DalamudAssetFontAndFamilyId(DalamudAsset.InconsolataRegular),
new DalamudAssetFontAndFamilyId(DalamudAsset.FontAwesomeFreeSolid),
};
/// <summary>
/// Gets the list of Game-provided fonts.
/// </summary>
/// <returns>The list of fonts.</returns>
public static List<IFontFamilyId> 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),
};
/// <summary>
/// Gets the list of System-provided fonts.
/// </summary>
/// <param name="refresh">If <c>true</c>, try to refresh the list.</param>
/// <returns>The list of fonts.</returns>
public static unsafe List<IFontFamilyId> ListSystemFonts(bool refresh)
{
using var dwf = default(ComPtr<IDWriteFactory>);
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<IDWriteFontCollection>);
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), refresh).ThrowOnError();
var count = (int)sfc.Get()->GetFontFamilyCount();
var result = new List<IFontFamilyId>(count);
for (var i = 0; i < count; i++)
{
using var ff = default(ComPtr<IDWriteFontFamily>);
if (sfc.Get()->GetFontFamily((uint)i, ff.GetAddressOf()).FAILED)
{
// Ignore errors, if any
continue;
}
try
{
result.Add(SystemFontFamilyId.FromDWriteFamily(ff));
}
catch
{
// ignore
}
}
return result;
}
}

View file

@ -0,0 +1,40 @@
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a font identifier.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontId : IObjectWithLocalizableName
{
/// <summary>
/// Gets the associated font family.
/// </summary>
IFontFamilyId Family { get; }
/// <summary>
/// Gets the font weight, ranging from 1 to 999.
/// </summary>
int Weight { get; }
/// <summary>
/// Gets the font stretch, ranging from 1 to 9.
/// </summary>
int Stretch { get; }
/// <summary>
/// Gets the font style. Treat as an opaque value.
/// </summary>
int Style { get; }
/// <summary>
/// Adds this font to the given font build toolkit.
/// </summary>
/// <param name="tk">The font build toolkit.</param>
/// <param name="config">The font configuration. Some parameters may be ignored.</param>
/// <returns>The added font.</returns>
ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config);
}

View file

@ -0,0 +1,50 @@
using Dalamud.Interface.ManagedFontAtlas;
using ImGuiNET;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents a user's choice of font(s).<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontSpec
{
/// <summary>
/// Gets the font size in pixels.
/// </summary>
float SizePx { get; }
/// <summary>
/// Gets the font size in points.
/// </summary>
float SizePt { get; }
/// <summary>
/// Gets the line height in pixels.
/// </summary>
float LineHeightPx { get; }
/// <summary>
/// Creates a font handle corresponding to this font specification.
/// </summary>
/// <param name="atlas">The atlas to bind this font handle to.</param>
/// <param name="callback">Optional callback to be called after creating the font handle.</param>
/// <returns>The new font handle.</returns>
IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null);
/// <summary>
/// Adds this font to the given font build toolkit.
/// </summary>
/// <param name="tk">The font build toolkit.</param>
/// <param name="mergeFont">The font to merge to.</param>
/// <returns>The added font.</returns>
ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default);
/// <summary>
/// Represents this font specification, preferrably in the requested locale.
/// </summary>
/// <param name="localeCode">The locale code. Must be in lowercase(invariant).</param>
/// <returns>The value.</returns>
string ToLocalizedString(string localeCode);
}

View file

@ -0,0 +1,76 @@
using System.Collections.Generic;
using Dalamud.Utility;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.FontIdentifier;
/// <summary>
/// Represents an object with localizable names.
/// </summary>
public interface IObjectWithLocalizableName
{
/// <summary>
/// Gets the name, preferrably in English.
/// </summary>
string EnglishName { get; }
/// <summary>
/// Gets the names per locales.
/// </summary>
IReadOnlyDictionary<string, string>? LocaleNames { get; }
/// <summary>
/// Gets the name in the requested locale if available; otherwise, <see cref="EnglishName"/>.
/// </summary>
/// <param name="localeCode">The locale code. Must be in lowercase(invariant).</param>
/// <returns>The value.</returns>
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;
}
/// <summary>
/// Resolves all names per locales.
/// </summary>
/// <param name="fn">The names.</param>
/// <returns>A new dictionary mapping from locale code to localized names.</returns>
internal static unsafe IReadOnlyDictionary<string, string> 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<string, string>((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;
}
}

View file

@ -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;
/// <summary>
/// Represents a user's choice of a single font.
/// </summary>
[SuppressMessage(
"StyleCop.CSharp.OrderingRules",
"SA1206:Declaration keywords should follow order",
Justification = "public required")]
public record SingleFontSpec : IFontSpec
{
/// <summary>
/// Gets the font id.
/// </summary>
[JsonProperty]
public required IFontId FontId { get; init; }
/// <inheritdoc/>
[JsonProperty]
public float SizePx { get; init; } = 16;
/// <inheritdoc/>
[JsonIgnore]
public float SizePt
{
get => (this.SizePx * 3) / 4;
init => this.SizePx = (value * 4) / 3;
}
/// <inheritdoc/>
[JsonIgnore]
public float LineHeightPx => MathF.Round(this.SizePx * this.LineHeight);
/// <summary>
/// Gets the line height ratio to the font size.
/// </summary>
[JsonProperty]
public float LineHeight { get; init; } = 1f;
/// <summary>
/// Gets the glyph offset in pixels.
/// </summary>
[JsonProperty]
public Vector2 GlyphOffset { get; init; }
/// <summary>
/// Gets the letter spacing in pixels.
/// </summary>
[JsonProperty]
public float LetterSpacing { get; init; }
/// <summary>
/// Gets the glyph ranges.
/// </summary>
[JsonProperty]
public ushort[]? GlyphRanges { get; init; }
/// <inheritdoc/>
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();
}
/// <inheritdoc/>
public override string ToString() => this.ToLocalizedString("en");
/// <inheritdoc/>
public IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null) =>
atlas.NewDelegateFontHandle(tk =>
{
tk.OnPreBuild(e => e.Font = this.AddToBuildToolkit(e));
callback?.Invoke(tk);
});
/// <inheritdoc/>
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;
}
}

View file

@ -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;
/// <summary>
/// Represents a font from system.
/// </summary>
public sealed class SystemFontFamilyId : IFontFamilyId
{
[JsonIgnore]
private IReadOnlyList<IFontId>? fontsLazy;
/// <summary>
/// Initializes a new instance of the <see cref="SystemFontFamilyId"/> class.
/// </summary>
/// <param name="englishName">The font name in English.</param>
/// <param name="localeNames">The localized font name for display purposes.</param>
[JsonConstructor]
internal SystemFontFamilyId(string englishName, IReadOnlyDictionary<string, string> localeNames)
{
this.EnglishName = englishName;
this.LocaleNames = localeNames;
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemFontFamilyId"/> class.
/// </summary>
/// <param name="localeNames">The localized font name for display purposes.</param>
internal SystemFontFamilyId(IReadOnlyDictionary<string, string> 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;
}
/// <inheritdoc/>
[JsonProperty]
public string EnglishName { get; init; }
/// <inheritdoc/>
[JsonProperty]
public IReadOnlyDictionary<string, string>? LocaleNames { get; }
/// <inheritdoc/>
[JsonIgnore]
public IReadOnlyList<IFontId> 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);
/// <inheritdoc/>
public int FindBestMatch(int weight, int stretch, int style)
{
using var matchingFont = default(ComPtr<IDWriteFont>);
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;
}
/// <inheritdoc/>
public override string ToString() => $"{nameof(SystemFontFamilyId)}:{this.EnglishName}";
/// <inheritdoc/>
public override bool Equals(object? obj) =>
ReferenceEquals(this, obj) || (obj is SystemFontFamilyId other && this.Equals(other));
/// <inheritdoc/>
public override int GetHashCode() => this.EnglishName.GetHashCode();
/// <summary>
/// Create a new instance of <see cref="SystemFontFamilyId"/> from an <see cref="IDWriteFontFamily"/>.
/// </summary>
/// <param name="family">The family.</param>
/// <returns>The new instance.</returns>
internal static unsafe SystemFontFamilyId FromDWriteFamily(ComPtr<IDWriteFontFamily> family)
{
using var fn = default(ComPtr<IDWriteLocalizedStrings>);
family.Get()->GetFamilyNames(fn.GetAddressOf()).ThrowOnError();
return new(IObjectWithLocalizableName.GetLocaleNames(fn));
}
private unsafe IReadOnlyList<IFontId> GetFonts()
{
using var dwf = default(ComPtr<IDWriteFactory>);
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<IDWriteFontCollection>);
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<IDWriteFontFamily>);
sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
var fontCount = (int)family.Get()->GetFontCount();
var fonts = new List<IFontId>(fontCount);
for (var i = 0; i < fontCount; i++)
{
using var font = default(ComPtr<IDWriteFont>);
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;
}

View file

@ -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;
/// <summary>
/// Represents a font installed in the system.
/// </summary>
public sealed class SystemFontId : IFontId
{
/// <summary>
/// Initializes a new instance of the <see cref="SystemFontId"/> class.
/// </summary>
/// <param name="family">The parent font family.</param>
/// <param name="font">The font.</param>
internal unsafe SystemFontId(SystemFontFamilyId family, ComPtr<IDWriteFont> 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<IDWriteLocalizedStrings>);
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<string, string> localeNames, IFontFamilyId family)
{
this.EnglishName = englishName;
this.LocaleNames = localeNames;
this.Family = family;
}
/// <inheritdoc/>
[JsonProperty]
public string EnglishName { get; init; }
/// <inheritdoc/>
[JsonProperty]
public IReadOnlyDictionary<string, string>? LocaleNames { get; }
/// <inheritdoc/>
[JsonProperty]
public IFontFamilyId Family { get; init; }
/// <inheritdoc/>
[JsonProperty]
public int Weight { get; init; } = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
/// <inheritdoc/>
[JsonProperty]
public int Stretch { get; init; } = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
/// <inheritdoc/>
[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);
/// <inheritdoc/>
public override bool Equals(object? obj) =>
ReferenceEquals(this, obj) || (obj is SystemFontId other && this.Equals(other));
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Family, this.Weight, this.Stretch, this.Style);
/// <inheritdoc/>
public override string ToString() =>
$"{nameof(SystemFontId)}:{this.Weight}:{this.Stretch}:{this.Style}:{this.Family}";
/// <inheritdoc/>
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
{
var (path, index) = this.GetFileAndIndex();
return tk.AddFontFromFile(path, config with { FontNo = index });
}
/// <summary>
/// Gets the file containing this font, and the font index within.
/// </summary>
/// <returns>The path and index.</returns>
public unsafe (string Path, int Index) GetFileAndIndex()
{
using var dwf = default(ComPtr<IDWriteFactory>);
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<IDWriteFontCollection>);
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<IDWriteFontFamily>);
sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
using var font = default(ComPtr<IDWriteFont>);
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<IDWriteFontFace>);
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<IDWriteFontFile>);
fface.Get()->GetFiles((uint*)&fileCount, ffile.GetAddressOf()).ThrowOnError();
void* refKey;
var refKeySize = 0u;
ffile.Get()->GetReferenceKey(&refKey, &refKeySize).ThrowOnError();
using var floader = default(ComPtr<IDWriteFontFileLoader>);
ffile.Get()->GetLoader(floader.GetAddressOf()).ThrowOnError();
using var flocal = default(ComPtr<IDWriteLocalFontFileLoader>);
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;
}