diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 5359861cf..7930f5c79 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -130,9 +130,9 @@ namespace Dalamud.Configuration.Internal public float GlobalUiScale { get; set; } = 1.0f; /// - /// Gets or sets the game font to use for Dalamud UI. + /// Gets or sets a value indicating whether to use AXIS fonts from the game. /// - public GameFont DefaultFontFromGame { get; set; } = GameFont.Undefined; + public bool UseAxisFontsFromGame { get; set; } = false; /// /// Gets or sets a value indicating whether or not plugin UI should be hidden. diff --git a/Dalamud/Interface/GameFonts/GameFontFamily.cs b/Dalamud/Interface/GameFonts/GameFontFamily.cs new file mode 100644 index 000000000..2aa836927 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontFamily.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Enum of available game font families. + /// + public enum GameFontFamily + { + /// + /// Placeholder meaning unused. + /// + Undefined, + + /// + /// Sans-serif fonts used for the whole UI. Contains Japanese characters in addition to Latin characters. + /// + Axis, + + /// + /// Serif fonts used for job names. Contains Latin characters. + /// + Jupiter, + + /// + /// Digit-only serif fonts used for flying texts. Contains numbers. + /// + JupiterNumeric, + + /// + /// Digit-only sans-serif horizontally wide fonts used for HP/MP/IL numbers. + /// + Meidinger, + + /// + /// Sans-serif horizontally wide font used for names of gauges. Contains Latin characters. + /// + MiedingerMid, + + /// + /// Sans-serif horizontally narrow font used for addon titles. Contains Latin characters. + /// + TrumpGothic, + } +} diff --git a/Dalamud/Interface/GameFonts/GameFont.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs similarity index 97% rename from Dalamud/Interface/GameFonts/GameFont.cs rename to Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index f095b6fcc..1cbdf210f 100644 --- a/Dalamud/Interface/GameFonts/GameFont.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -1,9 +1,9 @@ namespace Dalamud.Interface.GameFonts { /// - /// Enum of available game fonts. + /// Enum of available game fonts in specific sizes. /// - public enum GameFont : int + public enum GameFontFamilyAndSize : int { /// /// Placeholder meaning unused. diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 6d9274ac0..a50941883 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,5 +1,5 @@ using System; - +using System.Numerics; using ImGuiNET; namespace Dalamud.Interface.GameFonts @@ -10,26 +10,92 @@ namespace Dalamud.Interface.GameFonts public class GameFontHandle : IDisposable { private readonly GameFontManager manager; - private readonly GameFont font; + private readonly GameFontStyle fontStyle; /// /// Initializes a new instance of the class. /// /// GameFontManager instance. /// Font to use. - internal GameFontHandle(GameFontManager manager, GameFont font) + internal GameFontHandle(GameFontManager manager, GameFontStyle font) { this.manager = manager; - this.font = font; + this.fontStyle = font; } + /// + /// Gets the font style. + /// + public GameFontStyle Style => this.fontStyle; + + /// + /// Gets a value indicating whether this font is ready for use. + /// + public bool Available => this.manager.GetFont(this.fontStyle) != null; + /// /// Gets the font. /// - /// Corresponding font or null. - public ImFontPtr? Get() => this.manager.GetFont(this.font); + public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + + /// + /// Gets the FdtReader. + /// + public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); + + /// + /// Creates a new GameFontLayoutPlan.Builder. + /// + /// Text. + /// A new builder for GameFontLayoutPlan. + public GameFontLayoutPlan.Builder LayoutBuilder(string text) + { + return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); + } /// - public void Dispose() => this.manager.DecreaseFontRef(this.font); + public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); + + /// + /// Draws text. + /// + /// Text to draw. + public void Text(string text) + { + if (!this.Available) + { + ImGui.TextUnformatted(text); + } + else + { + this.LayoutBuilder(text) + .Build() + .Draw(ImGui.GetWindowDrawList(), ImGui.GetWindowPos() + ImGui.GetCursorPos(), ImGui.GetColorU32(ImGuiCol.Text)); + } + } + + /// + /// Draws text in given color. + /// + /// Color. + /// Text to draw. + public void TextColored(Vector4 col, string text) + { + ImGui.PushStyleColor(ImGuiCol.Text, col); + this.Text(text); + ImGui.PopStyleColor(); + } + + /// + /// Draws disabled text. + /// + /// Text to draw. + public void TextDisabled(string text) + { + unsafe + { + this.TextColored(*ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled), text); + } + } } } diff --git a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs new file mode 100644 index 000000000..dc2d5f380 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +using ImGuiNET; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Plan on how glyphs will be rendered. + /// + public class GameFontLayoutPlan + { + /// + /// Horizontal alignment. + /// + public enum HorizontalAlignment + { + /// + /// Align to left. + /// + Left, + + /// + /// Align to center. + /// + Center, + + /// + /// Align to right. + /// + Right, + } + + /// + /// Gets the associated ImFontPtr. + /// + public ImFontPtr ImFontPtr { get; internal set; } + + /// + /// Gets the size in points of the text. + /// + public float Size { get; internal set; } + + /// + /// Gets the x offset of the leftmost glyph. + /// + public float X { get; internal set; } + + /// + /// Gets the width of the text. + /// + public float Width { get; internal set; } + + /// + /// Gets the height of the text. + /// + public float Height { get; internal set; } + + /// + /// Gets the list of plannen elements. + /// + public IList Elements { get; internal set; } + + /// + /// Draws font to ImGui. + /// + /// Target ImDrawList. + /// Position. + /// Color. + public void Draw(ImDrawListPtr drawListPtr, Vector2 pos, uint col) + { + ImGui.Dummy(new Vector2(this.Width, this.Height)); + foreach (var element in this.Elements) + { + if (element.IsControl) + continue; + + this.ImFontPtr.RenderChar( + drawListPtr, + this.Size, + new Vector2( + this.X + pos.X + element.X, + pos.Y + element.Y), + col, + element.Glyph.Char); + } + } + + /// + /// Plan on how each glyph will be rendered. + /// + public class Element + { + /// + /// Gets the original codepoint. + /// + public int Codepoint { get; init; } + + /// + /// Gets the corresponding or fallback glyph. + /// + public FdtReader.FontTableEntry Glyph { get; init; } + + /// + /// Gets the X offset of this glyph. + /// + public float X { get; internal set; } + + /// + /// Gets the Y offset of this glyph. + /// + public float Y { get; internal set; } + + /// + /// Gets a value indicating whether whether this codepoint is a control character. + /// + public bool IsControl + { + get + { + return this.Codepoint < 0x10000 && char.IsControl((char)this.Codepoint); + } + } + + /// + /// Gets a value indicating whether whether this codepoint is a space. + /// + public bool IsSpace + { + get + { + return this.Codepoint < 0x10000 && char.IsWhiteSpace((char)this.Codepoint); + } + } + + /// + /// Gets a value indicating whether whether this codepoint is a line break character. + /// + public bool IsLineBreak + { + get + { + return this.Codepoint == '\n' || this.Codepoint == '\r'; + } + } + + /// + /// Gets a value indicating whether whether this codepoint is a chinese character. + /// + public bool IsChineseCharacter + { + get + { + // CJK Symbols and Punctuation(〇) + if (this.Codepoint >= 0x3007 && this.Codepoint <= 0x3007) + return true; + + // CJK Unified Ideographs Extension A + if (this.Codepoint >= 0x3400 && this.Codepoint <= 0x4DBF) + return true; + + // CJK Unified Ideographs + if (this.Codepoint >= 0x4E00 && this.Codepoint <= 0x9FFF) + return true; + + // CJK Unified Ideographs Extension B + if (this.Codepoint >= 0x20000 && this.Codepoint <= 0x2A6DF) + return true; + + // CJK Unified Ideographs Extension C + if (this.Codepoint >= 0x2A700 && this.Codepoint <= 0x2B73F) + return true; + + // CJK Unified Ideographs Extension D + if (this.Codepoint >= 0x2B740 && this.Codepoint <= 0x2B81F) + return true; + + // CJK Unified Ideographs Extension E + if (this.Codepoint >= 0x2B820 && this.Codepoint <= 0x2CEAF) + return true; + + // CJK Unified Ideographs Extension F + if (this.Codepoint >= 0x2CEB0 && this.Codepoint <= 0x2EBEF) + return true; + + return false; + } + } + + /// + /// Gets a value indicating whether whether this codepoint is a good position to break word after. + /// + public bool IsWordBreakPoint + { + get + { + if (this.IsChineseCharacter) + return true; + + if (this.Codepoint >= 0x10000) + return false; + + // TODO: Whatever + switch (char.GetUnicodeCategory((char)this.Codepoint)) + { + case System.Globalization.UnicodeCategory.SpaceSeparator: + case System.Globalization.UnicodeCategory.LineSeparator: + case System.Globalization.UnicodeCategory.ParagraphSeparator: + case System.Globalization.UnicodeCategory.Control: + case System.Globalization.UnicodeCategory.Format: + case System.Globalization.UnicodeCategory.Surrogate: + case System.Globalization.UnicodeCategory.PrivateUse: + case System.Globalization.UnicodeCategory.ConnectorPunctuation: + case System.Globalization.UnicodeCategory.DashPunctuation: + case System.Globalization.UnicodeCategory.OpenPunctuation: + case System.Globalization.UnicodeCategory.ClosePunctuation: + case System.Globalization.UnicodeCategory.InitialQuotePunctuation: + case System.Globalization.UnicodeCategory.FinalQuotePunctuation: + case System.Globalization.UnicodeCategory.OtherPunctuation: + case System.Globalization.UnicodeCategory.MathSymbol: + case System.Globalization.UnicodeCategory.ModifierSymbol: + case System.Globalization.UnicodeCategory.OtherSymbol: + case System.Globalization.UnicodeCategory.OtherNotAssigned: + return true; + } + + return false; + } + } + } + + /// + /// Build a GameFontLayoutPlan. + /// + public class Builder + { + private readonly ImFontPtr fontPtr; + private readonly FdtReader fdt; + private readonly string text; + private int maxWidth = int.MaxValue; + private float size; + private HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left; + + /// + /// Initializes a new instance of the class. + /// + /// Corresponding ImFontPtr. + /// FDT file to base on. + /// Text. + public Builder(ImFontPtr fontPtr, FdtReader fdt, string text) + { + this.fontPtr = fontPtr; + this.fdt = fdt; + this.text = text; + this.size = fdt.FontHeader.LineHeight; + } + + /// + /// Sets the size of resulting text. + /// + /// Size in pixels. + /// This. + public Builder WithSize(float size) + { + this.size = size; + return this; + } + + /// + /// Sets the maximum width of the text. + /// + /// Maximum width in pixels. + /// This. + public Builder WithMaxWidth(int maxWidth) + { + this.maxWidth = maxWidth; + return this; + } + + /// + /// Sets the horizontal alignment of the text. + /// + /// Horizontal alignment. + /// This. + public Builder WithHorizontalAlignment(HorizontalAlignment horizontalAlignment) + { + this.horizontalAlignment = horizontalAlignment; + return this; + } + + /// + /// Builds the layout plan. + /// + /// Newly created layout plan. + public GameFontLayoutPlan Build() + { + var scale = this.size / this.fdt.FontHeader.LineHeight; + var unscaledMaxWidth = (float)Math.Ceiling(this.maxWidth / scale); + var elements = new List(); + foreach (var c in this.text) + elements.Add(new() { Codepoint = c, Glyph = this.fdt.GetGlyph(c), }); + + var lastBreakIndex = 0; + List lineBreakIndices = new() { 0 }; + for (var i = 1; i < elements.Count; i++) + { + var prev = elements[i - 1]; + var curr = elements[i]; + + if (prev.IsLineBreak) + { + curr.X = 0; + curr.Y = prev.Y + this.fdt.FontHeader.LineHeight; + lineBreakIndices.Add(i); + } + else + { + curr.X = prev.X + prev.Glyph.NextOffsetX + prev.Glyph.BoundingWidth + this.fdt.GetDistance(prev.Codepoint, curr.Codepoint); + curr.Y = prev.Y; + } + + if (prev.IsWordBreakPoint) + lastBreakIndex = i; + + if (curr.IsSpace) + continue; + + if (curr.X + curr.Glyph.BoundingWidth < unscaledMaxWidth) + continue; + + if (!prev.IsSpace && elements[lastBreakIndex].X > 0) + { + prev = elements[lastBreakIndex - 1]; + curr = elements[lastBreakIndex]; + i = lastBreakIndex; + } + else + { + lastBreakIndex = i; + } + + curr.X = 0; + curr.Y = prev.Y + this.fdt.FontHeader.LineHeight; + lineBreakIndices.Add(i); + } + + lineBreakIndices.Add(elements.Count); + + var targetX = 0f; + var targetWidth = 0f; + var targetHeight = 0f; + for (var i = 1; i < lineBreakIndices.Count; i++) + { + var from = lineBreakIndices[i - 1]; + var to = lineBreakIndices[i]; + while (to > from && elements[to - 1].IsSpace) + { + to--; + } + + if (from >= to) + continue; + + var right = 0f; + for (var j = from; j < to; j++) + { + var e = elements[j]; + right = Math.Max(right, e.X + Math.Max(e.Glyph.BoundingWidth, e.Glyph.AdvanceWidth)); + targetHeight = Math.Max(targetHeight, e.Y + e.Glyph.BoundingHeight); + } + + targetWidth = Math.Max(targetWidth, right - elements[from].X); + float offsetX; + if (this.horizontalAlignment == HorizontalAlignment.Center) + offsetX = (unscaledMaxWidth - right) / 2; + else if (this.horizontalAlignment == HorizontalAlignment.Right) + offsetX = unscaledMaxWidth - right; + else if (this.horizontalAlignment == HorizontalAlignment.Left) + offsetX = 0; + else + throw new ArgumentException("Invalid horizontal alignment"); + for (var j = from; j < to; j++) + elements[j].X += offsetX; + targetX = i == 1 ? elements[from].X : Math.Min(targetX, elements[from].X); + } + + targetHeight = Math.Max(targetHeight, this.fdt.FontHeader.LineHeight * (lineBreakIndices.Count - 1)); + + targetWidth *= scale; + targetHeight *= scale; + targetX *= scale; + foreach (var e in elements) + { + e.X *= scale; + e.Y *= scale; + } + + return new GameFontLayoutPlan() + { + ImFontPtr = this.fontPtr, + Size = this.size, + X = targetX, + Width = targetWidth, + Height = targetHeight, + Elements = elements, + }; + } + } + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index 9cca00236..44772bc48 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -33,10 +33,9 @@ namespace Dalamud.Interface.GameFonts private readonly FdtReader?[] fdts; private readonly List texturePixels; - private readonly ImFontPtr?[] fonts = new ImFontPtr?[FontNames.Length]; - - private readonly int[] fontUseCounter = new int[FontNames.Length]; - private readonly List>> glyphRectIds = new(); + private readonly Dictionary fonts = new(); + private readonly Dictionary fontUseCounter = new(); + private readonly Dictionary>> glyphRectIds = new(); /// /// Initializes a new instance of the class. @@ -60,34 +59,34 @@ namespace Dalamud.Interface.GameFonts /// /// Font to describe. /// A string in a form of "FontName (NNNpt)". - public static string DescribeFont(GameFont font) + public static string DescribeFont(GameFontFamilyAndSize font) { return font switch { - GameFont.Undefined => "-", - GameFont.Axis96 => "AXIS (9.6pt)", - GameFont.Axis12 => "AXIS (12pt)", - GameFont.Axis14 => "AXIS (14pt)", - GameFont.Axis18 => "AXIS (18pt)", - GameFont.Axis36 => "AXIS (36pt)", - GameFont.Jupiter16 => "Jupiter (16pt)", - GameFont.Jupiter20 => "Jupiter (20pt)", - GameFont.Jupiter23 => "Jupiter (23pt)", - GameFont.Jupiter45 => "Jupiter Numeric (45pt)", - GameFont.Jupiter46 => "Jupiter (46pt)", - GameFont.Jupiter90 => "Jupiter Numeric (90pt)", - GameFont.Meidinger16 => "Meidinger Numeric (16pt)", - GameFont.Meidinger20 => "Meidinger Numeric (20pt)", - GameFont.Meidinger40 => "Meidinger Numeric (40pt)", - GameFont.MiedingerMid10 => "MiedingerMid (10pt)", - GameFont.MiedingerMid12 => "MiedingerMid (12pt)", - GameFont.MiedingerMid14 => "MiedingerMid (14pt)", - GameFont.MiedingerMid18 => "MiedingerMid (18pt)", - GameFont.MiedingerMid36 => "MiedingerMid (36pt)", - GameFont.TrumpGothic184 => "Trump Gothic (18.4pt)", - GameFont.TrumpGothic23 => "Trump Gothic (23pt)", - GameFont.TrumpGothic34 => "Trump Gothic (34pt)", - GameFont.TrumpGothic68 => "Trump Gothic (68pt)", + GameFontFamilyAndSize.Undefined => "-", + GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", + GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", + GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", + GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", + GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", + GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", + GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", + GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", + GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", + GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", + GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", + GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", + GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", + GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", + GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", + GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", + GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", + GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", + GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", + GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", + GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", + GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", + GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), }; } @@ -97,15 +96,15 @@ namespace Dalamud.Interface.GameFonts /// /// Font to check. /// True if it can. - public static bool IsGenericPurposeFont(GameFont font) + public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) { return font switch { - GameFont.Axis96 => true, - GameFont.Axis12 => true, - GameFont.Axis14 => true, - GameFont.Axis18 => true, - GameFont.Axis36 => true, + GameFontFamilyAndSize.Axis96 => true, + GameFontFamilyAndSize.Axis12 => true, + GameFontFamilyAndSize.Axis14 => true, + GameFontFamilyAndSize.Axis18 => true, + GameFontFamilyAndSize.Axis36 => true, _ => false, }; } @@ -174,32 +173,38 @@ namespace Dalamud.Interface.GameFonts /// /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. /// - /// Font to use. + /// Font to use. /// Handle to game font that may or may not be ready yet. - public GameFontHandle NewFontRef(GameFont gameFont) + public GameFontHandle NewFontRef(GameFontStyle style) { - var fontIndex = (int)gameFont; var needRebuild = false; lock (this.syncRoot) { - var prev = this.fontUseCounter[fontIndex] == 0; - this.fontUseCounter[fontIndex] += 1; - needRebuild = prev != (this.fontUseCounter[fontIndex] == 0); + var prevValue = this.fontUseCounter.GetValueOrDefault(style, 0); + var newValue = this.fontUseCounter[style] = prevValue + 1; + needRebuild = (prevValue == 0) != (newValue == 0) && !this.fonts.ContainsKey(style); } if (needRebuild) this.interfaceManager.RebuildFonts(); - return new(this, gameFont); + return new(this, style); } /// /// Gets the font. /// - /// Font to get. + /// Font to get. /// Corresponding font or null. - public ImFontPtr? GetFont(GameFont gameFont) => this.fonts[(int)gameFont]; + public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); + + /// + /// Gets the corresponding FdtReader. + /// + /// Font to get. + /// Corresponding FdtReader or null. + public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; /// /// Fills missing glyphs in target font from source font, if both are not null. @@ -208,9 +213,9 @@ namespace Dalamud.Interface.GameFonts /// Target font. /// Whether to copy missing glyphs only. /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFont target, bool missingOnly, bool rebuildLookupTable) + public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[(int)target], missingOnly, rebuildLookupTable); + GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -220,9 +225,9 @@ namespace Dalamud.Interface.GameFonts /// Target font. /// Whether to copy missing glyphs only. /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFont source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) + public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], target, missingOnly, rebuildLookupTable); + GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); } /// @@ -232,9 +237,9 @@ namespace Dalamud.Interface.GameFonts /// Target font. /// Whether to copy missing glyphs only. /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFont source, GameFont target, bool missingOnly, bool rebuildLookupTable) + public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], this.fonts[(int)target], missingOnly, rebuildLookupTable); + GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -242,30 +247,38 @@ namespace Dalamud.Interface.GameFonts /// public void BuildFonts() { - this.glyphRectIds.Clear(); var io = ImGui.GetIO(); io.Fonts.TexDesiredWidth = 4096; - for (var i = 0; i < FontNames.Length; i++) - { - this.fonts[i] = null; - this.glyphRectIds.Add(new()); + this.glyphRectIds.Clear(); + this.fonts.Clear(); - var fdt = this.fdts[i]; - if (this.fontUseCounter[i] == 0 || fdt == null) + foreach (var style in this.fontUseCounter.Keys) + { + var rectIds = this.glyphRectIds[style] = new(); + + var fdt = this.fdts[(int)style.FamilyAndSize]; + if (fdt == null) continue; - Log.Information($"GameFontManager BuildFont: {FontNames[i]}"); - var font = io.Fonts.AddFontDefault(); - this.fonts[i] = font; + this.fonts[style] = font; foreach (var glyph in fdt.Glyphs) { var c = glyph.Char; if (c < 32 || c >= 0xFFFF) continue; - this.glyphRectIds[i][c] = Tuple.Create(io.Fonts.AddCustomRectFontGlyph(font, c, glyph.BoundingWidth + 1, glyph.BoundingHeight + 1, glyph.BoundingWidth + glyph.NextOffsetX, new Vector2(0, glyph.CurrentOffsetY)), glyph); + var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph); + rectIds[c] = Tuple.Create( + io.Fonts.AddCustomRectFontGlyph( + font, + c, + glyph.BoundingWidth + widthAdjustment + 1, + glyph.BoundingHeight + 1, + glyph.AdvanceWidth, + new Vector2(0, glyph.CurrentOffsetY)), + glyph); } } } @@ -279,13 +292,9 @@ namespace Dalamud.Interface.GameFonts io.Fonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height); var pixels32 = (uint*)pixels8; - for (var i = 0; i < this.fonts.Length; i++) + foreach (var (style, font) in this.fonts) { - if (!this.fonts[i].HasValue) - continue; - - var font = this.fonts[i]!.Value; - var fdt = this.fdts[i]; + var fdt = this.fdts[(int)style.FamilyAndSize]; var fontPtr = font.NativePtr; fontPtr->ConfigData->SizePixels = fontPtr->FontSize = fdt.FontHeader.LineHeight; fontPtr->Ascent = fdt.FontHeader.Ascent; @@ -301,75 +310,83 @@ namespace Dalamud.Interface.GameFonts } } - fixed (char* c = FontNames[i]) + fixed (char* c = FontNames[(int)style.FamilyAndSize]) { for (var j = 0; j < 40; j++) fontPtr->ConfigData->Name[j] = 0; - Encoding.UTF8.GetBytes(c, FontNames[i].Length, fontPtr->ConfigData->Name, 40); + Encoding.UTF8.GetBytes(c, FontNames[(int)style.FamilyAndSize].Length, fontPtr->ConfigData->Name, 40); } - foreach (var (c, (rectId, glyph)) in this.glyphRectIds[i]) + foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) { var rc = io.Fonts.GetCustomRectByIndex(rectId); var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; var sourceBufferDelta = glyph.TextureChannelByteIndex; - for (var y = 0; y < glyph.BoundingHeight; y++) + var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph); + if (widthAdjustment == 0) { - for (var x = 0; x < glyph.BoundingWidth; x++) + for (var y = 0; y < glyph.BoundingHeight; y++) { - var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; - pixels32[((rc.Y + y) * width) + rc.X + x] = (uint)(a << 24) | 0xFFFFFFu; + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; + pixels32[((rc.Y + y) * width) + rc.X + x] = (uint)(a << 24) | 0xFFFFFFu; + } + } + } + else + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) + pixels32[((rc.Y + y) * width) + rc.X + x] = 0xFFFFFFu; + } + + for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) + { + var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); + for (var y = 0; y < glyph.BoundingHeight; y++) + { + float xDelta = xbold; + if (style.SkewStrength > 0) + xDelta += style.SkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; + else if (style.SkewStrength < 0) + xDelta -= style.SkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; + var xDeltaInt = (int)Math.Floor(xDelta); + var xness = xDelta - xDeltaInt; + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; + var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; + var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; + var n = (a1 * xness) + (a2 * (1 - xness)); + var targetOffset = ((rc.Y + y) * width) + rc.X + x + xDeltaInt; + pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); + } + } } } } } - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis96, true, false); - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis12, true, false); - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis14, true, false); - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis18, true, false); - this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis36, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.Jupiter16, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter20, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter23, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter45, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter46, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter90, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.Meidinger16, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Meidinger20, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Meidinger40, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis96, GameFont.MiedingerMid10, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis12, GameFont.MiedingerMid12, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis14, GameFont.MiedingerMid14, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.MiedingerMid18, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.MiedingerMid36, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.TrumpGothic184, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic23, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic34, true, false); - this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic68, true, false); - - foreach (var font in this.fonts) - font?.BuildLookupTable(); + foreach (var font in this.fonts.Values) + { + CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false); + font.BuildLookupTable(); + } } /// - /// Decrease font reference counter and release if nobody is using it. + /// Decrease font reference counter. /// - /// Font to release. - internal void DecreaseFontRef(GameFont gameFont) + /// Font to release. + internal void DecreaseFontRef(GameFontStyle style) { - var fontIndex = (int)gameFont; - var needRebuild = false; - lock (this.syncRoot) { - var prev = this.fontUseCounter[fontIndex] == 0; - this.fontUseCounter[fontIndex] -= 1; - needRebuild = prev != (this.fontUseCounter[fontIndex] == 0); + if ((this.fontUseCounter[style] -= 1) == 0) + this.fontUseCounter.Remove(style); } - - if (needRebuild) - this.interfaceManager.RebuildFonts(); } private struct ImFontGlyphReal diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs new file mode 100644 index 000000000..8a713f1cf --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Describes a font based on game resource file. + /// + public struct GameFontStyle + { + /// + /// Font family of the font. + /// + public GameFontFamilyAndSize FamilyAndSize; + + /// + /// Weight of the font. + /// + /// 0 is unaltered. + /// Any value greater than 0 will make it bolder. + /// + public float Weight; + + /// + /// Skewedness of the font. + /// + /// 0 is unaltered. + /// Greater than 1 will make upper part go rightwards. + /// Less than 1 will make lower part go rightwards. + /// + public float SkewStrength; + + /// + /// Initializes a new instance of the struct. + /// + /// Font family. + /// Size in points. + public GameFontStyle(GameFontFamily family, float size) + { + this.FamilyAndSize = GetRecommendedFamilyAndSize(family, size); + this.Weight = this.SkewStrength = 0f; + } + + /// + /// Initializes a new instance of the struct. + /// + /// Font family and size. + public GameFontStyle(GameFontFamilyAndSize familyAndSize) + { + this.FamilyAndSize = familyAndSize; + this.Weight = this.SkewStrength = 0f; + } + + /// + /// Gets the font family. + /// + public GameFontFamily Family => this.FamilyAndSize switch + { + GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, + GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, + GameFontFamilyAndSize.Axis12 => GameFontFamily.Axis, + GameFontFamilyAndSize.Axis14 => GameFontFamily.Axis, + GameFontFamilyAndSize.Axis18 => GameFontFamily.Axis, + GameFontFamilyAndSize.Axis36 => GameFontFamily.Axis, + GameFontFamilyAndSize.Jupiter16 => GameFontFamily.Jupiter, + GameFontFamilyAndSize.Jupiter20 => GameFontFamily.Jupiter, + GameFontFamilyAndSize.Jupiter23 => GameFontFamily.Jupiter, + GameFontFamilyAndSize.Jupiter45 => GameFontFamily.JupiterNumeric, + GameFontFamilyAndSize.Jupiter46 => GameFontFamily.Jupiter, + GameFontFamilyAndSize.Jupiter90 => GameFontFamily.JupiterNumeric, + GameFontFamilyAndSize.Meidinger16 => GameFontFamily.Meidinger, + GameFontFamilyAndSize.Meidinger20 => GameFontFamily.Meidinger, + GameFontFamilyAndSize.Meidinger40 => GameFontFamily.Meidinger, + GameFontFamilyAndSize.MiedingerMid10 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.MiedingerMid12 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.MiedingerMid14 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.MiedingerMid18 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.MiedingerMid36 => GameFontFamily.MiedingerMid, + GameFontFamilyAndSize.TrumpGothic184 => GameFontFamily.TrumpGothic, + GameFontFamilyAndSize.TrumpGothic23 => GameFontFamily.TrumpGothic, + GameFontFamilyAndSize.TrumpGothic34 => GameFontFamily.TrumpGothic, + GameFontFamilyAndSize.TrumpGothic68 => GameFontFamily.TrumpGothic, + _ => throw new InvalidOperationException(), + }; + + /// + /// Gets the font size. + /// + public float Size => this.FamilyAndSize switch + { + GameFontFamilyAndSize.Undefined => 0, + GameFontFamilyAndSize.Axis96 => 9.6f, + GameFontFamilyAndSize.Axis12 => 12, + GameFontFamilyAndSize.Axis14 => 14, + GameFontFamilyAndSize.Axis18 => 18, + GameFontFamilyAndSize.Axis36 => 36, + GameFontFamilyAndSize.Jupiter16 => 16, + GameFontFamilyAndSize.Jupiter20 => 20, + GameFontFamilyAndSize.Jupiter23 => 23, + GameFontFamilyAndSize.Jupiter45 => 45, + GameFontFamilyAndSize.Jupiter46 => 46, + GameFontFamilyAndSize.Jupiter90 => 90, + GameFontFamilyAndSize.Meidinger16 => 16, + GameFontFamilyAndSize.Meidinger20 => 20, + GameFontFamilyAndSize.Meidinger40 => 40, + GameFontFamilyAndSize.MiedingerMid10 => 10, + GameFontFamilyAndSize.MiedingerMid12 => 12, + GameFontFamilyAndSize.MiedingerMid14 => 14, + GameFontFamilyAndSize.MiedingerMid18 => 18, + GameFontFamilyAndSize.MiedingerMid36 => 36, + GameFontFamilyAndSize.TrumpGothic184 => 18.4f, + GameFontFamilyAndSize.TrumpGothic23 => 23, + GameFontFamilyAndSize.TrumpGothic34 => 34, + GameFontFamilyAndSize.TrumpGothic68 => 8, + _ => throw new InvalidOperationException(), + }; + + /// + /// Gets or sets a value indicating whether this font is bold. + /// + public bool Bold + { + get => this.Weight > 0f; + set => this.Weight = value ? 1f : 0f; + } + + /// + /// Gets or sets a value indicating whether this font is italic. + /// + public bool Italic + { + get => this.SkewStrength != 0; + set => this.SkewStrength = value ? 4 : 0; + } + + /// + /// Gets the recommend GameFontFamilyAndSize given family and size. + /// + /// Font family. + /// Font size in points. + /// Recommended GameFontFamilyAndSize. + public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) + { + if (size <= 0) + return GameFontFamilyAndSize.Undefined; + + switch (family) + { + case GameFontFamily.Undefined: + return GameFontFamilyAndSize.Undefined; + + case GameFontFamily.Axis: + if (size <= 9.6) + return GameFontFamilyAndSize.Axis96; + else if (size <= 12) + return GameFontFamilyAndSize.Axis12; + else if (size <= 14) + return GameFontFamilyAndSize.Axis14; + else if (size <= 18) + return GameFontFamilyAndSize.Axis18; + else + return GameFontFamilyAndSize.Axis36; + + case GameFontFamily.Jupiter: + if (size <= 16) + return GameFontFamilyAndSize.Jupiter16; + else if (size <= 20) + return GameFontFamilyAndSize.Jupiter20; + else if (size <= 23) + return GameFontFamilyAndSize.Jupiter23; + else + return GameFontFamilyAndSize.Jupiter46; + + case GameFontFamily.JupiterNumeric: + if (size <= 45) + return GameFontFamilyAndSize.Jupiter45; + else + return GameFontFamilyAndSize.Jupiter90; + + case GameFontFamily.Meidinger: + if (size <= 16) + return GameFontFamilyAndSize.Meidinger16; + else if (size <= 20) + return GameFontFamilyAndSize.Meidinger20; + else + return GameFontFamilyAndSize.Meidinger40; + + case GameFontFamily.MiedingerMid: + if (size <= 10) + return GameFontFamilyAndSize.MiedingerMid10; + else if (size <= 12) + return GameFontFamilyAndSize.MiedingerMid12; + else if (size <= 14) + return GameFontFamilyAndSize.MiedingerMid14; + else if (size <= 18) + return GameFontFamilyAndSize.MiedingerMid18; + else + return GameFontFamilyAndSize.MiedingerMid36; + + case GameFontFamily.TrumpGothic: + if (size <= 18.4) + return GameFontFamilyAndSize.TrumpGothic184; + else if (size <= 23) + return GameFontFamilyAndSize.TrumpGothic23; + else if (size <= 34) + return GameFontFamilyAndSize.TrumpGothic34; + else + return GameFontFamilyAndSize.TrumpGothic68; + + default: + return GameFontFamilyAndSize.Undefined; + } + } + + /// + /// Calculates the adjustment to width resulting fron Weight and SkewStrength. + /// + /// Font information. + /// Glyph. + /// Width adjustment in pixel unit. + public int CalculateWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) + { + var widthDelta = this.Weight; + if (this.SkewStrength > 0) + widthDelta += 1f * this.SkewStrength * (reader.FontHeader.LineHeight - glyph.CurrentOffsetY) / reader.FontHeader.LineHeight; + else if (this.SkewStrength < 0) + widthDelta -= 1f * this.SkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight) / reader.FontHeader.LineHeight; + + return (int)Math.Ceiling(widthDelta); + } + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 1e9654dc0..58f18bf70 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -57,8 +57,7 @@ namespace Dalamud.Interface.Internal private readonly SwapChainVtableResolver address; private RawDX11Scene? scene; - private GameFont overwriteDefaultFontFromGameFont = GameFont.Undefined; - private GameFontHandle? overwriteDefaultFontFromGameFontHandle; + private GameFontHandle? axisFontHandle; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -313,16 +312,7 @@ namespace Dalamud.Interface.Internal if (!this.isRebuildingFonts) { Log.Verbose("[FONT] RebuildFonts() trigger"); - var configuration = Service.Get(); - if (this.overwriteDefaultFontFromGameFont != configuration.DefaultFontFromGame) - { - this.overwriteDefaultFontFromGameFont = configuration.DefaultFontFromGame; - this.overwriteDefaultFontFromGameFontHandle?.Dispose(); - if (configuration.DefaultFontFromGame == GameFont.Undefined) - this.overwriteDefaultFontFromGameFontHandle = null; - else - this.overwriteDefaultFontFromGameFontHandle = Service.Get().NewFontRef(configuration.DefaultFontFromGame); - } + this.SetAxisFonts(); this.isRebuildingFonts = true; this.scene.OnNewRenderFrame += this.RebuildFontsInternal; @@ -342,6 +332,26 @@ namespace Dalamud.Interface.Internal Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); } + private void SetAxisFonts() + { + var configuration = Service.Get(); + if (configuration.UseAxisFontsFromGame) + { + var currentFamilyAndSize = GameFontStyle.GetRecommendedFamilyAndSize(GameFontFamily.Axis, this.axisFontHandle?.Style.Size ?? 0f); + var expectedFamilyAndSize = GameFontStyle.GetRecommendedFamilyAndSize(GameFontFamily.Axis, 12 * ImGui.GetIO().FontGlobalScale); + if (currentFamilyAndSize == expectedFamilyAndSize) + return; + + this.axisFontHandle?.Dispose(); + this.axisFontHandle = Service.Get().NewFontRef(new(expectedFamilyAndSize)); + } + else + { + this.axisFontHandle?.Dispose(); + this.axisFontHandle = null; + } + } + /* * NOTE(goat): When hooking ReShade DXGISwapChain::runtime_present, this is missing the syncInterval arg. * Seems to work fine regardless, I guess, so whatever. @@ -403,15 +413,7 @@ namespace Dalamud.Interface.Internal this.scene.OnBuildUI += this.Display; this.scene.OnNewInputFrame += this.OnNewInputFrame; - if (this.overwriteDefaultFontFromGameFont != configuration.DefaultFontFromGame) - { - this.overwriteDefaultFontFromGameFont = configuration.DefaultFontFromGame; - this.overwriteDefaultFontFromGameFontHandle?.Dispose(); - if (configuration.DefaultFontFromGame == GameFont.Undefined) - this.overwriteDefaultFontFromGameFontHandle = null; - else - this.overwriteDefaultFontFromGameFontHandle = Service.Get().NewFontRef(configuration.DefaultFontFromGame); - } + this.SetAxisFonts(); this.SetupFonts(); @@ -591,7 +593,7 @@ namespace Dalamud.Interface.Internal ImGui.GetIO().Fonts.Build(); gameFontManager.AfterBuildFonts(); - GameFontManager.CopyGlyphsAcrossFonts(this.overwriteDefaultFontFromGameFontHandle?.Get(), DefaultFont, false, true); + GameFontManager.CopyGlyphsAcrossFonts(this.axisFontHandle?.ImFont, DefaultFont, false, true); Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); this.AfterBuildFonts?.Invoke(); diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 6fa6df808..ac01392a7 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -28,8 +28,6 @@ namespace Dalamud.Interface.Internal.Windows private const float MinScale = 0.3f; private const float MaxScale = 2.0f; - private readonly List validFontChoices; - private readonly string[] validFontNames; private readonly string[] languages; private readonly string[] locLanguages; private int langIndex; @@ -40,6 +38,7 @@ namespace Dalamud.Interface.Internal.Windows private bool doCfChatMessage; private float globalUiScale; + private bool doUseAxisFontsFromGame; private bool doToggleUiHide; private bool doToggleUiHideDuringCutscenes; private bool doToggleUiHideDuringGpose; @@ -69,8 +68,6 @@ namespace Dalamud.Interface.Internal.Windows private bool doButtonsSystemMenu; private bool disableRmtFiltering; - private int validFontIndex; - #region Experimental private bool doPluginTest; @@ -94,6 +91,7 @@ namespace Dalamud.Interface.Internal.Windows this.doCfChatMessage = configuration.DutyFinderChatMessage; this.globalUiScale = configuration.GlobalUiScale; + this.doUseAxisFontsFromGame = configuration.UseAxisFontsFromGame; this.doToggleUiHide = configuration.ToggleUiHide; this.doToggleUiHideDuringCutscenes = configuration.ToggleUiHideDuringCutscenes; this.doToggleUiHideDuringGpose = configuration.ToggleUiHideDuringGpose; @@ -116,10 +114,6 @@ namespace Dalamud.Interface.Internal.Windows this.doButtonsSystemMenu = configuration.DoButtonsSystemMenu; this.disableRmtFiltering = configuration.DisableRmtFiltering; - this.validFontChoices = Enum.GetValues().Where(x => x == GameFont.Undefined || GameFontManager.IsGenericPurposeFont(x)).ToList(); - this.validFontNames = this.validFontChoices.Select(x => GameFontManager.DescribeFont(x)).ToArray(); - this.validFontIndex = Math.Max(0, this.validFontChoices.IndexOf(configuration.DefaultFontFromGame)); - this.languages = Localization.ApplicableLangCodes.Prepend("en").ToArray(); try { @@ -286,20 +280,19 @@ namespace Dalamud.Interface.Internal.Windows { this.globalUiScale = 1.0f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; + Service.Get().RebuildFonts(); } if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f")) + { ImGui.GetIO().FontGlobalScale = this.globalUiScale; + Service.Get().RebuildFonts(); + } ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale all XIVLauncher UI elements - useful for 4K displays.")); ImGuiHelpers.ScaledDummy(10, 16); - ImGui.Text(Loc.Localize("DalamudSettingsGlobalFont", "Global Font")); - ImGui.Combo("##DalamudSettingsGlobalFontDrag", ref this.validFontIndex, this.validFontNames, this.validFontNames.Length); - - ImGuiHelpers.ScaledDummy(10, 16); - if (ImGui.Button(Loc.Localize("DalamudSettingsOpenStyleEditor", "Open Style Editor"))) { Service.Get().OpenStyleEditor(); @@ -311,6 +304,9 @@ namespace Dalamud.Interface.Internal.Windows ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below.")); + ImGui.Checkbox(Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"), ref this.doUseAxisFontsFromGame); + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font.")); + ImGui.Checkbox(Loc.Localize("DalamudSettingToggleUiHide", "Hide plugin UI when the game UI is toggled off"), ref this.doToggleUiHide); ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideHint", "Hide any open windows by plugins when toggling the game overlay.")); @@ -814,7 +810,7 @@ namespace Dalamud.Interface.Internal.Windows configuration.IsFocusManagementEnabled = this.doFocus; configuration.ShowTsm = this.doTsm; - configuration.DefaultFontFromGame = this.validFontChoices[this.validFontIndex]; + configuration.UseAxisFontsFromGame = this.doUseAxisFontsFromGame; // This is applied every frame in InterfaceManager::CheckViewportState() configuration.IsDisableViewport = !this.doViewport; @@ -858,9 +854,8 @@ namespace Dalamud.Interface.Internal.Windows configuration.Save(); - Service.Get().RebuildFonts(); - _ = Service.Get().ReloadPluginMastersAsync(); + Service.Get().RebuildFonts(); } } } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index c57a130b4..b4c089fbe 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Numerics; @@ -8,6 +8,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Windowing; using ImGuiNET; using ImGuiScene; @@ -19,6 +20,7 @@ namespace Dalamud.Interface.Internal.Windows /// internal class TitleScreenMenuWindow : Window, IDisposable { + private const float TargetFontSize = 16.2f; private readonly TextureWrap shadeTexture; private readonly Dictionary shadeEasings = new(); @@ -27,6 +29,8 @@ namespace Dalamud.Interface.Internal.Windows private InOutCubic? fadeOutEasing; + private GameFontHandle? axisFontHandle; + private State state = State.Hide; /// @@ -67,14 +71,19 @@ namespace Dalamud.Interface.Internal.Windows /// public override void PreDraw() { + this.SetAxisFonts(); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0)); + if (this.axisFontHandle?.Available ?? false) + ImGui.PushFont(this.axisFontHandle.ImFont); base.PreDraw(); } /// public override void PostDraw() { + if (this.axisFontHandle?.Available ?? false) + ImGui.PopFont(); ImGui.PopStyleVar(2); base.PostDraw(); } @@ -90,128 +99,143 @@ namespace Dalamud.Interface.Internal.Windows /// public override void Draw() { - ImGui.SetWindowFontScale(1.3f); + ImGui.SetWindowFontScale(TargetFontSize / ImGui.GetFont().FontSize * 4 / 3); var tsm = Service.Get(); switch (this.state) { case State.Show: - { - for (var i = 0; i < tsm.Entries.Count; i++) { - var entry = tsm.Entries[i]; - - if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) + for (var i = 0; i < tsm.Entries.Count; i++) { - moveEasing = new InOutQuint(TimeSpan.FromMilliseconds(400)); - this.moveEasings.Add(entry.Id, moveEasing); + var entry = tsm.Entries[i]; + + if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) + { + moveEasing = new InOutQuint(TimeSpan.FromMilliseconds(400)); + this.moveEasings.Add(entry.Id, moveEasing); + } + + if (!moveEasing.IsRunning && !moveEasing.IsDone) + { + moveEasing.Restart(); + } + + if (moveEasing.IsDone) + { + moveEasing.Stop(); + } + + moveEasing.Update(); + + var finalPos = (i + 1) * this.shadeTexture.Height; + var pos = moveEasing.Value * finalPos; + + // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. + if (moveEasing.IsDone) + { + pos = finalPos; + } + + this.DrawEntry(entry, moveEasing.IsRunning && i != 0, true, i == 0, true); + + var cursor = ImGui.GetCursorPos(); + cursor.Y = (float)pos; + ImGui.SetCursorPos(cursor); } - if (!moveEasing.IsRunning && !moveEasing.IsDone) + if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | + ImGuiHoveredFlags.AllowWhenOverlapped | + ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)) { - moveEasing.Restart(); + this.state = State.FadeOut; } - if (moveEasing.IsDone) - { - moveEasing.Stop(); - } - - moveEasing.Update(); - - var finalPos = (i + 1) * this.shadeTexture.Height; - var pos = moveEasing.Value * finalPos; - - // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. - if (moveEasing.IsDone) - { - pos = finalPos; - } - - this.DrawEntry(entry, moveEasing.IsRunning && i != 0, true, i == 0, true); - - var cursor = ImGui.GetCursorPos(); - cursor.Y = (float)pos; - ImGui.SetCursorPos(cursor); + break; } - if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | - ImGuiHoveredFlags.AllowWhenOverlapped | - ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)) - { - this.state = State.FadeOut; - } - - break; - } - case State.FadeOut: - { - this.fadeOutEasing ??= new InOutCubic(TimeSpan.FromMilliseconds(400)) { - IsInverse = true, - }; + this.fadeOutEasing ??= new InOutCubic(TimeSpan.FromMilliseconds(400)) + { + IsInverse = true, + }; - if (!this.fadeOutEasing.IsRunning && !this.fadeOutEasing.IsDone) - { - this.fadeOutEasing.Restart(); + if (!this.fadeOutEasing.IsRunning && !this.fadeOutEasing.IsDone) + { + this.fadeOutEasing.Restart(); + } + + if (this.fadeOutEasing.IsDone) + { + this.fadeOutEasing.Stop(); + } + + this.fadeOutEasing.Update(); + + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value); + + for (var i = 0; i < tsm.Entries.Count; i++) + { + var entry = tsm.Entries[i]; + + var finalPos = (i + 1) * this.shadeTexture.Height; + + this.DrawEntry(entry, i != 0, true, i == 0, false); + + var cursor = ImGui.GetCursorPos(); + cursor.Y = finalPos; + ImGui.SetCursorPos(cursor); + } + + ImGui.PopStyleVar(); + + var isHover = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | + ImGuiHoveredFlags.AllowWhenOverlapped | + ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + + if (!isHover && this.fadeOutEasing!.IsDone) + { + this.state = State.Hide; + this.fadeOutEasing = null; + } + else if (isHover) + { + this.state = State.Show; + this.fadeOutEasing = null; + } + + break; } - if (this.fadeOutEasing.IsDone) - { - this.fadeOutEasing.Stop(); - } - - this.fadeOutEasing.Update(); - - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value); - - for (var i = 0; i < tsm.Entries.Count; i++) - { - var entry = tsm.Entries[i]; - - var finalPos = (i + 1) * this.shadeTexture.Height; - - this.DrawEntry(entry, i != 0, true, i == 0, false); - - var cursor = ImGui.GetCursorPos(); - cursor.Y = finalPos; - ImGui.SetCursorPos(cursor); - } - - ImGui.PopStyleVar(); - - var isHover = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | - ImGuiHoveredFlags.AllowWhenOverlapped | - ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); - - if (!isHover && this.fadeOutEasing!.IsDone) - { - this.state = State.Hide; - this.fadeOutEasing = null; - } - else if (isHover) - { - this.state = State.Show; - this.fadeOutEasing = null; - } - - break; - } - case State.Hide: - { - if (this.DrawEntry(tsm.Entries[0], true, false, true, true)) { - this.state = State.Show; - } + if (this.DrawEntry(tsm.Entries[0], true, false, true, true)) + { + this.state = State.Show; + } - this.moveEasings.Clear(); - this.logoEasings.Clear(); - this.shadeEasings.Clear(); - break; - } + this.moveEasings.Clear(); + this.logoEasings.Clear(); + this.shadeEasings.Clear(); + break; + } + } + } + + private void SetAxisFonts() + { + var configuration = Service.Get(); + if (configuration.UseAxisFontsFromGame) + { + if (this.axisFontHandle == null) + this.axisFontHandle = Service.Get().NewFontRef(new(GameFontFamily.Axis, TargetFontSize)); + } + else + { + this.axisFontHandle?.Dispose(); + this.axisFontHandle = null; } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 35c82102b..e3f85426c 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -215,9 +215,9 @@ namespace Dalamud.Interface /// /// Gets a game font. /// - /// Font to get. + /// Font to get. /// Handle to the game font which may or may not be available for use yet. - public GameFontHandle GetGameFontHandle(GameFont gameFont) => Service.Get().NewFontRef(gameFont); + public GameFontHandle GetGameFontHandle(GameFontStyle style) => Service.Get().NewFontRef(style); /// /// Call this to queue a rebuild of the font atlas.