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)