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

@ -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
/// <summary>
/// Gets or sets a value indicating whether to use AXIS fonts from the game.
/// </summary>
public bool UseAxisFontsFromGame { get; set; } = false;
[Obsolete($"See {nameof(DefaultFontSpec)}")]
public bool UseAxisFontsFromGame { get; set; } = true;
/// <summary>
/// Gets or sets the default font spec.
/// </summary>
public IFontSpec? DefaultFontSpec { get; set; }
/// <summary>
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.

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;
}

File diff suppressed because it is too large Load diff

View file

@ -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<FontAtlasFactory>.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<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
})));
this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild(
tk =>
{

View file

@ -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();
}

View file

@ -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<byte> testStringBuffer;
private IFontAtlas? privateAtlas;
private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance };
private IFontHandle? fontDialogHandle;
private IReadOnlyDictionary<GameFontFamily, (GameFontStyle Size, Lazy<IFontHandle> 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<FontAtlasFactory>.Get().CreateFontAtlas(
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
FontAtlasAutoRebuildMode.Async));
fcd.SelectedFont = this.fontSpec;
fcd.IgnorePreviewGlobalScale = !this.useGlobalScale;
Service<InterfaceManager>.Get().Draw += fcd.Draw;
fcd.ResultTask.ContinueWith(
r => Service<Framework>.Get().RunOnFrameworkThread(
() =>
{
Service<InterfaceManager>.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<GameFontFamilyAndSize>()
.Where(x => x.GetAttribute<GameFontFamilyAndSizeAttribute>() 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;
}

View file

@ -68,11 +68,11 @@ internal class SettingsWindow : Window
var interfaceManager = Service<InterfaceManager>.Get();
var fontAtlasFactory = Service<FontAtlasFactory>.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();

View file

@ -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<bool>(
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<FontAtlasFactory>.Get().UseAxisOverride = v;
Service<InterfaceManager>.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<FontAtlasFactory>.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<Framework>.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<FontAtlasFactory>.Get();
faf.DefaultFontSpecOverride =
this.defaultFontSpec =
new SingleFontSpec { FontId = new GameFontAndFamilyId(GameFontFamily.Axis) };
interfaceManager.RebuildFonts();
}
}
base.Draw();
}
public override void Load()
{
this.globalUiScale = Service<DalamudConfiguration>.Get().GlobalUiScale;
this.defaultFontSpec = Service<FontAtlasFactory>.Get().DefaultFontSpec;
base.Load();
}
@ -214,6 +248,7 @@ public class SettingsTabLook : SettingsTab
public override void Save()
{
Service<DalamudConfiguration>.Get().GlobalUiScale = this.globalUiScale;
Service<DalamudConfiguration>.Get().DefaultFontSpec = this.defaultFontSpec;
base.Save();
}

View file

@ -8,7 +8,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Wrapper for <see cref="ImFontAtlasPtr"/>.
/// Wrapper for <see cref="ImFontAtlasPtr"/>.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontAtlas : IDisposable
{
@ -93,11 +94,15 @@ public interface IFontAtlas : IDisposable
/// </summary>
/// <param name="buildStepDelegate">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
/// <returns>Handle to a font that may or may not be ready yet.</returns>
/// <remarks>
/// Consider calling <see cref="IFontAtlasBuildToolkitPreBuild.AttachExtraGlyphsForDalamudLanguage"/> to support
/// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users.
/// </remarks>
/// <example>
/// <b>On initialization</b>:
/// <code>
/// 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);

View file

@ -9,7 +9,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontAtlasBuildToolkit
{

View file

@ -5,7 +5,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
{

View file

@ -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;
/// <summary>
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PreBuild"/>.<br />
/// Not intended for plugins to implement.<br />
/// <br />
/// After <see cref="FontAtlasBuildStepDelegate"/> returns,
/// either <see cref="IFontAtlasBuildToolkit.Font"/> must be set,
@ -52,6 +54,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// <returns>True if ignored.</returns>
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
/// <summary>
/// Registers a function to be run after build.
/// </summary>
/// <param name="action">The action to run.</param>
void RegisterPostBuild(Action action);
/// <summary>
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
/// <b>It WILL crash if you try to use a memory pointer allocated in some other way.</b><br />
@ -134,7 +142,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// As this involves adding multiple fonts, calling this function will set <see cref="IFontAtlasBuildToolkit.Font"/>
/// as the return value of this function, if it was empty before.
/// </summary>
/// <param name="sizePx">Font size in pixels.</param>
/// <param name="sizePx">
/// Font size in pixels.
/// If a negative value is supplied,
/// (<see cref="UiBuilder.DefaultFontSpec"/>.<see cref="IFontSpec.SizePx"/> * <paramref name="sizePx"/>) will be
/// used as the font size. Specify -1 to use the default font size.
/// </param>
/// <param name="glyphRanges">The glyph ranges. Use <see cref="FontAtlasBuildToolkitUtilities"/>.ToGlyphRange to build.</param>
/// <returns>A font returned from <see cref="ImFontAtlasPtr.AddFont"/>.</returns>
ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null);

View file

@ -5,7 +5,8 @@ using ImGuiNET;
namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// Represents a reference counting handle for fonts.
/// Represents a reference counting handle for fonts.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface IFontHandle : IDisposable
{

View file

@ -4,7 +4,8 @@ namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary>
/// The wrapper for <see cref="ImFontPtr"/>, guaranteeing that the associated data will be available as long as
/// this struct is not disposed.
/// this struct is not disposed.<br />
/// Not intended for plugins to implement.
/// </summary>
public interface ILockedImFont : IDisposable
{

View file

@ -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<Action> registeredPostBuildActions = new();
/// <summary>
/// Initializes a new instance of the <see cref="BuildToolkit"/> class.
@ -162,6 +164,9 @@ internal sealed partial class FontAtlasFactory
/// <inheritdoc/>
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
this.data.AddNewTexture(textureWrap, disposeOnError);
/// <inheritdoc/>
public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action);
/// <inheritdoc/>
public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
@ -314,18 +319,32 @@ internal sealed partial class FontAtlasFactory
/// <inheritdoc/>
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<byte>();

View file

@ -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)

View file

@ -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
}
/// <summary>
/// Gets or sets a value indicating whether to override configuration for UseAxis.
/// Gets or sets a value indicating whether to override configuration for <see cref="DefaultFontSpec"/>.
/// </summary>
public bool? UseAxisOverride { get; set; } = null;
public IFontSpec? DefaultFontSpecOverride { get; set; } = null;
/// <summary>
/// Gets a value indicating whether to use AXIS fonts.
/// Gets the default font ID.
/// </summary>
public bool UseAxis => this.UseAxisOverride ?? Service<DalamudConfiguration>.Get().UseAxisFontsFromGame;
public IFontSpec DefaultFontSpec =>
this.DefaultFontSpecOverride
?? Service<DalamudConfiguration>.Get().DefaultFontSpec
#pragma warning disable CS0618 // Type or member is obsolete
?? (Service<DalamudConfiguration>.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,
});
/// <summary>
/// Gets the service instance of <see cref="Framework"/>.

View file

@ -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;
}

View file

@ -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
/// <summary>
/// Gets the default Dalamud font size in points.
/// </summary>
public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt;
public static float DefaultFontSizePt => Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePt;
/// <summary>
/// Gets the default Dalamud font size in pixels.
/// </summary>
public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx;
public static float DefaultFontSizePx => Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx;
/// <summary>
/// Gets the default Dalamud font - supporting all game languages and icons.<br />
@ -198,6 +199,11 @@ public sealed class UiBuilder : IDisposable
/// </summary>
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
/// <summary>
/// Gets the default font specifications.
/// </summary>
public IFontSpec DefaultFontSpec => Service<FontAtlasFactory>.Get().DefaultFontSpec;
/// <summary>
/// Gets the handle to the default Dalamud font - supporting all game languages and icons.
/// </summary>

View file

@ -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)));
}
/// <summary>
/// Sets the text for a text input, during the callback.
/// </summary>
/// <param name="data">The callback data.</param>
/// <param name="s">The new text.</param>
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);
}
/// <summary>
/// Finds the corresponding ImGui viewport ID for the given window handle.

View file

@ -97,4 +97,76 @@ internal static class ArrayExtensions
/// <returns><paramref name="array"/> casted as a <see cref="IReadOnlyCollection{T}"/> if it is one; otherwise the result of <see cref="Enumerable.ToArray{TSource}"/>.</returns>
public static IReadOnlyCollection<T> AsReadOnlyCollection<T>(this IEnumerable<T> array) =>
array as IReadOnlyCollection<T> ?? array.ToArray();
/// <inheritdoc cref="List{T}.FindIndex(System.Predicate{T})"/>
public static int FindIndex<T>(this IReadOnlyList<T> list, Predicate<T> match)
=> list.FindIndex(0, list.Count, match);
/// <inheritdoc cref="List{T}.FindIndex(int,System.Predicate{T})"/>
public static int FindIndex<T>(this IReadOnlyList<T> list, int startIndex, Predicate<T> match)
=> list.FindIndex(startIndex, list.Count - startIndex, match);
/// <inheritdoc cref="List{T}.FindIndex(int,int,System.Predicate{T})"/>
public static int FindIndex<T>(this IReadOnlyList<T> list, int startIndex, int count, Predicate<T> 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;
}
/// <inheritdoc cref="List{T}.FindLastIndex(System.Predicate{T})"/>
public static int FindLastIndex<T>(this IReadOnlyList<T> list, Predicate<T> match)
=> list.FindLastIndex(list.Count - 1, list.Count, match);
/// <inheritdoc cref="List{T}.FindLastIndex(int,System.Predicate{T})"/>
public static int FindLastIndex<T>(this IReadOnlyList<T> list, int startIndex, Predicate<T> match)
=> list.FindLastIndex(startIndex, startIndex + 1, match);
/// <inheritdoc cref="List{T}.FindLastIndex(int,int,System.Predicate{T})"/>
public static int FindLastIndex<T>(this IReadOnlyList<T> list, int startIndex, int count, Predicate<T> 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;
}
}

View file

@ -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;
}
/// <summary>
/// Throws a corresponding exception if <see cref="HRESULT.FAILED"/> is true.
/// </summary>
/// <param name="hr">The result value.</param>
internal static void ThrowOnError(this HRESULT hr)
{
if (hr.FAILED)
Marshal.ThrowExceptionForHR(hr.Value);
}
/// <summary>
/// Print formatted GameObject Information to ImGui.
/// </summary>