From 34daa73612b0b6419734c4d8c457a3933b6ca22b Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 14 Feb 2024 05:09:46 +0900 Subject: [PATCH] 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 --- .../Internal/DalamudConfiguration.cs | 9 +- .../DalamudAssetFontAndFamilyId.cs | 87 ++ .../DalamudDefaultFontAndFamilyId.cs | 77 ++ .../FontIdentifier/GameFontAndFamilyId.cs | 81 ++ .../Interface/FontIdentifier/IFontFamilyId.cs | 102 ++ Dalamud/Interface/FontIdentifier/IFontId.cs | 40 + Dalamud/Interface/FontIdentifier/IFontSpec.cs | 50 + .../IObjectWithLocalizableName.cs | 76 ++ .../FontIdentifier/SingleFontSpec.cs | 155 +++ .../FontIdentifier/SystemFontFamilyId.cs | 181 +++ .../Interface/FontIdentifier/SystemFontId.cs | 163 +++ .../SingleFontChooserDialog.cs | 1117 +++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 9 +- .../Internal/Windows/ConsoleWindow.cs | 21 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 87 +- .../Windows/Settings/SettingsWindow.cs | 4 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 79 +- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 9 +- .../IFontAtlasBuildToolkit.cs | 3 +- .../IFontAtlasBuildToolkitPostBuild.cs | 3 +- .../IFontAtlasBuildToolkitPreBuild.cs | 15 +- .../Interface/ManagedFontAtlas/IFontHandle.cs | 3 +- .../ManagedFontAtlas/ILockedImFont.cs | 3 +- .../FontAtlasFactory.BuildToolkit.cs | 42 +- .../FontAtlasFactory.Implementation.cs | 3 +- .../Internals/FontAtlasFactory.cs | 24 +- .../ManagedFontAtlas/SafeFontConfig.cs | 2 +- Dalamud/Interface/UiBuilder.cs | 10 +- Dalamud/Interface/Utility/ImGuiHelpers.cs | 19 + Dalamud/Utility/ArrayExtensions.cs | 72 ++ Dalamud/Utility/Util.cs | 13 + 31 files changed, 2478 insertions(+), 81 deletions(-) create mode 100644 Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontSpec.cs create mode 100644 Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs create mode 100644 Dalamud/Interface/FontIdentifier/SingleFontSpec.cs create mode 100644 Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/SystemFontId.cs create mode 100644 Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs 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. ///