diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 69e754727..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 @@ -43,7 +43,59 @@ namespace Dalamud.Interface.GameFonts /// 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.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 34d385bba..44772bc48 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -183,7 +183,7 @@ namespace Dalamud.Interface.GameFonts { var prevValue = this.fontUseCounter.GetValueOrDefault(style, 0); var newValue = this.fontUseCounter[style] = prevValue + 1; - needRebuild = (prevValue == 0) != (newValue == 0); + needRebuild = (prevValue == 0) != (newValue == 0) && !this.fonts.ContainsKey(style); } if (needRebuild)