mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-15 05:04:15 +01:00
Add kerned text draw helper
This commit is contained in:
parent
17c77e6bfd
commit
3026eb6fa8
3 changed files with 468 additions and 2 deletions
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
|
||||
using System.Numerics;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.GameFonts
|
||||
|
|
@ -43,7 +43,59 @@ namespace Dalamud.Interface.GameFonts
|
|||
/// </summary>
|
||||
public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GameFontLayoutPlan.Builder.
|
||||
/// </summary>
|
||||
/// <param name="text">Text.</param>
|
||||
/// <returns>A new builder for GameFontLayoutPlan.</returns>
|
||||
public GameFontLayoutPlan.Builder LayoutBuilder(string text)
|
||||
{
|
||||
return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle);
|
||||
|
||||
/// <summary>
|
||||
/// Draws text.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws text in given color.
|
||||
/// </summary>
|
||||
/// <param name="col">Color.</param>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
public void TextColored(Vector4 col, string text)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, col);
|
||||
this.Text(text);
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws disabled text.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
public void TextDisabled(string text)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
this.TextColored(*ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled), text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
414
Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs
Normal file
414
Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Plan on how glyphs will be rendered.
|
||||
/// </summary>
|
||||
public class GameFontLayoutPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// Horizontal alignment.
|
||||
/// </summary>
|
||||
public enum HorizontalAlignment
|
||||
{
|
||||
/// <summary>
|
||||
/// Align to left.
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// Align to center.
|
||||
/// </summary>
|
||||
Center,
|
||||
|
||||
/// <summary>
|
||||
/// Align to right.
|
||||
/// </summary>
|
||||
Right,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated ImFontPtr.
|
||||
/// </summary>
|
||||
public ImFontPtr ImFontPtr { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size in points of the text.
|
||||
/// </summary>
|
||||
public float Size { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the x offset of the leftmost glyph.
|
||||
/// </summary>
|
||||
public float X { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the text.
|
||||
/// </summary>
|
||||
public float Width { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the text.
|
||||
/// </summary>
|
||||
public float Height { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of plannen elements.
|
||||
/// </summary>
|
||||
public IList<Element> Elements { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Draws font to ImGui.
|
||||
/// </summary>
|
||||
/// <param name="drawListPtr">Target ImDrawList.</param>
|
||||
/// <param name="pos">Position.</param>
|
||||
/// <param name="col">Color.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plan on how each glyph will be rendered.
|
||||
/// </summary>
|
||||
public class Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the original codepoint.
|
||||
/// </summary>
|
||||
public int Codepoint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the corresponding or fallback glyph.
|
||||
/// </summary>
|
||||
public FdtReader.FontTableEntry Glyph { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the X offset of this glyph.
|
||||
/// </summary>
|
||||
public float X { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Y offset of this glyph.
|
||||
/// </summary>
|
||||
public float Y { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether whether this codepoint is a control character.
|
||||
/// </summary>
|
||||
public bool IsControl
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Codepoint < 0x10000 && char.IsControl((char)this.Codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether whether this codepoint is a space.
|
||||
/// </summary>
|
||||
public bool IsSpace
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Codepoint < 0x10000 && char.IsWhiteSpace((char)this.Codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether whether this codepoint is a line break character.
|
||||
/// </summary>
|
||||
public bool IsLineBreak
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Codepoint == '\n' || this.Codepoint == '\r';
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether whether this codepoint is a chinese character.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether whether this codepoint is a good position to break word after.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a GameFontLayoutPlan.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Builder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">Corresponding ImFontPtr.</param>
|
||||
/// <param name="fdt">FDT file to base on.</param>
|
||||
/// <param name="text">Text.</param>
|
||||
public Builder(ImFontPtr fontPtr, FdtReader fdt, string text)
|
||||
{
|
||||
this.fontPtr = fontPtr;
|
||||
this.fdt = fdt;
|
||||
this.text = text;
|
||||
this.size = fdt.FontHeader.LineHeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the size of resulting text.
|
||||
/// </summary>
|
||||
/// <param name="size">Size in pixels.</param>
|
||||
/// <returns>This.</returns>
|
||||
public Builder WithSize(float size)
|
||||
{
|
||||
this.size = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum width of the text.
|
||||
/// </summary>
|
||||
/// <param name="maxWidth">Maximum width in pixels.</param>
|
||||
/// <returns>This.</returns>
|
||||
public Builder WithMaxWidth(int maxWidth)
|
||||
{
|
||||
this.maxWidth = maxWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the horizontal alignment of the text.
|
||||
/// </summary>
|
||||
/// <param name="horizontalAlignment">Horizontal alignment.</param>
|
||||
/// <returns>This.</returns>
|
||||
public Builder WithHorizontalAlignment(HorizontalAlignment horizontalAlignment)
|
||||
{
|
||||
this.horizontalAlignment = horizontalAlignment;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the layout plan.
|
||||
/// </summary>
|
||||
/// <returns>Newly created layout plan.</returns>
|
||||
public GameFontLayoutPlan Build()
|
||||
{
|
||||
var scale = this.size / this.fdt.FontHeader.LineHeight;
|
||||
var unscaledMaxWidth = (float)Math.Ceiling(this.maxWidth / scale);
|
||||
var elements = new List<Element>();
|
||||
foreach (var c in this.text)
|
||||
elements.Add(new() { Codepoint = c, Glyph = this.fdt.GetGlyph(c), });
|
||||
|
||||
var lastBreakIndex = 0;
|
||||
List<int> 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue