2023-05-13 15:17:57 +02:00
|
|
|
using System;
|
|
|
|
using System.Collections;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Globalization;
|
|
|
|
using System.Text;
|
2023-06-29 13:17:32 +02:00
|
|
|
using Quik.Media;
|
2023-05-13 15:17:57 +02:00
|
|
|
|
|
|
|
namespace Quik.Typography
|
|
|
|
{
|
|
|
|
/// <summary>
|
|
|
|
/// An atomic horizontal block of text which cannot be further divided.
|
|
|
|
/// </summary>
|
|
|
|
public struct HorizontalTextBlock
|
|
|
|
{
|
|
|
|
/// <summary>
|
|
|
|
/// The font associated with the text block.
|
|
|
|
/// </summary>
|
|
|
|
/// <value></value>
|
2023-06-29 13:17:32 +02:00
|
|
|
// public QuikFont Font { get; }
|
2023-05-13 15:17:57 +02:00
|
|
|
/// <summary>
|
|
|
|
/// Textual contents of the text block.
|
|
|
|
/// </summary>
|
|
|
|
public string Text { get; }
|
|
|
|
/// <summary>
|
|
|
|
/// Indicates this text block should be layed out right to left.
|
|
|
|
/// </summary>
|
|
|
|
public bool IsRTL { get; }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Indicates this is a whitespace block.
|
|
|
|
/// </summary>
|
|
|
|
public bool IsWhitespace => string.IsNullOrWhiteSpace(Text);
|
|
|
|
|
|
|
|
public float Width { get; }
|
|
|
|
public float Ascend { get; }
|
|
|
|
public float Descend { get; }
|
|
|
|
public float Height => Ascend - Descend;
|
|
|
|
|
2023-06-29 13:17:32 +02:00
|
|
|
public HorizontalTextBlock(object font, string text, bool rtl = false)
|
2023-05-13 15:17:57 +02:00
|
|
|
{
|
2023-06-29 13:17:32 +02:00
|
|
|
// Font = font;
|
2023-05-13 15:17:57 +02:00
|
|
|
Text = text;
|
|
|
|
IsRTL = rtl;
|
|
|
|
|
|
|
|
float width = 0.0f;
|
|
|
|
float ascend = 0.0f;
|
|
|
|
float descend = 0.0f;
|
|
|
|
|
|
|
|
foreach (char chr in text)
|
|
|
|
{
|
2023-06-29 13:17:32 +02:00
|
|
|
// font.GetCharacter(chr, out _, out QGlyphMetrics glyph);
|
|
|
|
// width += glyph.Advance.X;
|
|
|
|
// ascend = Math.Max(ascend, glyph.HorizontalBearing.Y);
|
|
|
|
// descend = Math.Min(descend, glyph.HorizontalBearing.Y - glyph.Size.Y);
|
2023-05-13 15:17:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Width = width;
|
|
|
|
Ascend = ascend;
|
|
|
|
Descend = descend;
|
|
|
|
}
|
|
|
|
|
|
|
|
public HorizontalTextBlock(float width)
|
|
|
|
{
|
2023-06-29 13:17:32 +02:00
|
|
|
// Font = null;
|
2023-05-13 15:17:57 +02:00
|
|
|
Text = string.Empty;
|
|
|
|
IsRTL = false;
|
|
|
|
Width = width;
|
|
|
|
Ascend = Descend = 0.0f;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// An atomic vertical block of text which cannot be further divided.
|
|
|
|
/// </summary>
|
|
|
|
public struct VerticalTextBlock
|
|
|
|
{
|
2023-06-29 13:17:32 +02:00
|
|
|
// public QuikFont Font { get; }
|
2023-05-13 15:17:57 +02:00
|
|
|
public string Text { get; }
|
|
|
|
public bool IsWhitespace => string.IsNullOrWhiteSpace(Text);
|
|
|
|
public float Width { get; }
|
|
|
|
public float Height { get; }
|
|
|
|
|
2023-06-29 13:17:32 +02:00
|
|
|
public VerticalTextBlock(object font, string text)
|
2023-05-13 15:17:57 +02:00
|
|
|
{
|
2023-06-29 13:17:32 +02:00
|
|
|
// Font = font;
|
2023-05-13 15:17:57 +02:00
|
|
|
Text = text;
|
|
|
|
|
|
|
|
float width = 0.0f;
|
|
|
|
float height = 0.0f;
|
|
|
|
|
|
|
|
foreach(char chr in text)
|
|
|
|
{
|
2023-06-29 13:17:32 +02:00
|
|
|
// font.GetCharacter(chr, out _, out QGlyphMetrics glyph);
|
|
|
|
// width = Math.Max(width, - glyph.VerticalBearing.X * 2);
|
|
|
|
// height += glyph.Advance.Y;
|
2023-05-13 15:17:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Width = width;
|
|
|
|
Height = height;
|
|
|
|
}
|
|
|
|
|
|
|
|
public VerticalTextBlock(float height)
|
|
|
|
{
|
2023-06-29 13:17:32 +02:00
|
|
|
// Font = null;
|
2023-05-13 15:17:57 +02:00
|
|
|
Text = string.Empty;
|
|
|
|
Width = 0.0f;
|
|
|
|
Height = height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public abstract class Paragraph
|
|
|
|
{
|
|
|
|
public abstract bool IsVertical { get; }
|
|
|
|
|
|
|
|
public float JustifyLimit { get; set; } = 30.0f;
|
|
|
|
public TextAlignment Alignment { get; set; } = TextAlignment.Default;
|
|
|
|
public float PreSpace { get; set; } = 0.0f;
|
|
|
|
public float PostSpace { get; set; } = 0.0f;
|
|
|
|
public float FirstLineInset { get; set; } = 0.0f;
|
|
|
|
public float LineGap { get; set; } = 12.0f;
|
|
|
|
|
|
|
|
public abstract void Typeset(TypesetGroup group, float width);
|
|
|
|
|
2023-06-29 13:17:32 +02:00
|
|
|
protected abstract void AppendBlock(object font, string text, bool rtl = false);
|
2023-05-13 15:17:57 +02:00
|
|
|
|
2023-06-29 13:17:32 +02:00
|
|
|
public void ConsumeText(object font, string text)
|
2023-05-13 15:17:57 +02:00
|
|
|
{
|
|
|
|
StringBuilder segment = new StringBuilder();
|
|
|
|
bool rtl = false;
|
|
|
|
bool ws = false;
|
|
|
|
|
|
|
|
foreach(char chr in text)
|
|
|
|
{
|
|
|
|
UnicodeCategory cat = char.GetUnicodeCategory(chr);
|
|
|
|
// FIXME: don't ignore control characters like a barbarian.
|
|
|
|
// TODO: how do I detect text flow direction???
|
|
|
|
if (char.IsWhiteSpace(chr) && chr != '\u00a0')
|
|
|
|
{
|
|
|
|
if (ws)
|
|
|
|
{
|
|
|
|
segment.Append(chr);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
AppendBlock(font, segment.ToString());
|
|
|
|
segment.Clear();
|
|
|
|
segment.Append(chr);
|
|
|
|
ws = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (ws)
|
|
|
|
{
|
|
|
|
AppendBlock(font, segment.ToString(), rtl);
|
|
|
|
segment.Clear();
|
|
|
|
segment.Append(chr);
|
|
|
|
ws = false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
segment.Append(chr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (segment.Length > 0)
|
|
|
|
{
|
|
|
|
AppendBlock(font, segment.ToString(), rtl);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public class HorizontalParagraph : Paragraph
|
|
|
|
{
|
|
|
|
public override bool IsVertical => false;
|
|
|
|
public List<HorizontalTextBlock> Blocks { get; } = new List<HorizontalTextBlock>();
|
|
|
|
|
|
|
|
public override void Typeset(TypesetGroup group, float width)
|
|
|
|
{
|
|
|
|
Queue<HorizontalTextBlock> line = new Queue<HorizontalTextBlock>();
|
|
|
|
int index = 0;
|
|
|
|
bool firstLine = true;
|
|
|
|
|
2023-06-29 09:42:02 +02:00
|
|
|
QVec2 pen = new QVec2(0, -PreSpace);
|
2023-05-13 15:17:57 +02:00
|
|
|
|
|
|
|
while (index < Blocks.Count)
|
|
|
|
{
|
|
|
|
index
|
|
|
|
+= GatherLine(
|
|
|
|
index,
|
|
|
|
width - (firstLine ? FirstLineInset : 0),
|
|
|
|
line,
|
|
|
|
out float excess,
|
|
|
|
out float ascend,
|
|
|
|
out float descend);
|
|
|
|
|
|
|
|
firstLine = false;
|
|
|
|
|
|
|
|
pen.Y -= ascend;
|
|
|
|
|
|
|
|
float interblockWs =
|
|
|
|
Alignment.HasFlag(TextAlignment.Justify) && excess < JustifyLimit
|
|
|
|
? excess / (line.Count - 1)
|
|
|
|
: 0.0f;
|
|
|
|
|
|
|
|
switch (Alignment & TextAlignment.HorizontalMask)
|
|
|
|
{
|
|
|
|
default:
|
|
|
|
case TextAlignment.AlignLeft:
|
|
|
|
if (firstLine) pen.X += FirstLineInset;
|
|
|
|
break;
|
|
|
|
case TextAlignment.AlignCenterH:
|
|
|
|
pen.X += excess / 2;
|
|
|
|
break;
|
|
|
|
case TextAlignment.AlignRight:
|
|
|
|
pen.X += excess;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
PutBlock(group, line, interblockWs, ref pen);
|
|
|
|
|
|
|
|
pen.Y -= LineGap - descend;
|
|
|
|
pen.X = 0.0f;
|
|
|
|
}
|
|
|
|
|
|
|
|
pen.Y -= PostSpace;
|
|
|
|
|
2023-06-29 09:42:02 +02:00
|
|
|
group.BoundingBox = new QRectangle(width, 0, 0, pen.Y);
|
2023-05-13 15:17:57 +02:00
|
|
|
group.Translate(-pen);
|
|
|
|
}
|
|
|
|
|
|
|
|
private int GatherLine(
|
|
|
|
int index,
|
|
|
|
float width,
|
|
|
|
Queue<HorizontalTextBlock> line,
|
|
|
|
out float excess,
|
|
|
|
out float ascend,
|
|
|
|
out float descend)
|
|
|
|
{
|
|
|
|
float currentWidth = 0.0f;
|
|
|
|
ascend = descend = 0.0f;
|
|
|
|
|
|
|
|
for (int i = index; i < Blocks.Count; i++)
|
|
|
|
{
|
|
|
|
HorizontalTextBlock block = Blocks[i];
|
|
|
|
|
|
|
|
if (currentWidth + block.Width > width)
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
ascend = Math.Max(ascend, block.Ascend);
|
|
|
|
descend = Math.Min(descend, block.Descend);
|
|
|
|
currentWidth += block.Width;
|
|
|
|
line.Enqueue(block);
|
|
|
|
}
|
|
|
|
|
|
|
|
excess = width - currentWidth;
|
|
|
|
return line.Count;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void PutBlock(
|
|
|
|
TypesetGroup group,
|
|
|
|
Queue<HorizontalTextBlock> line,
|
|
|
|
float interblockWs,
|
2023-06-29 09:42:02 +02:00
|
|
|
ref QVec2 pen)
|
2023-05-13 15:17:57 +02:00
|
|
|
{
|
2023-06-29 09:42:02 +02:00
|
|
|
QVec2 penpal = pen;
|
2023-05-13 15:17:57 +02:00
|
|
|
|
|
|
|
while (line.TryDequeue(out HorizontalTextBlock block))
|
|
|
|
{
|
|
|
|
if (block.IsWhitespace)
|
|
|
|
{
|
|
|
|
penpal.X += block.Width + interblockWs;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (block.IsRTL)
|
|
|
|
{
|
|
|
|
for (int i = block.Text.Length - 1; i >= 0; i--)
|
|
|
|
{
|
|
|
|
char chr = block.Text[i];
|
2023-06-29 13:17:32 +02:00
|
|
|
// block.Font.GetCharacter(chr, out QuikTexture texture, out QGlyphMetrics metrics);
|
|
|
|
// group.Add(
|
|
|
|
// new TypesetCharacter(
|
|
|
|
// chr,
|
|
|
|
// texture,
|
|
|
|
// new QRectangle(
|
|
|
|
// penpal.X + metrics.Advance.X,
|
|
|
|
// penpal.Y + metrics.HorizontalBearing.Y,
|
|
|
|
// penpal.X + metrics.HorizontalBearing.X,
|
|
|
|
// penpal.Y - metrics.Size.Y + metrics.HorizontalBearing.Y),
|
|
|
|
// metrics.Location
|
|
|
|
// )
|
|
|
|
// );
|
|
|
|
// penpal.X += metrics.Advance.X;
|
2023-05-13 15:17:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for (int i = 0; i < block.Text.Length; i++)
|
|
|
|
{
|
|
|
|
char chr = block.Text[i];
|
2023-06-29 13:17:32 +02:00
|
|
|
// block.Font.GetCharacter(chr, out QuikTexture texture, out QGlyphMetrics metrics);
|
|
|
|
// group.Add(
|
|
|
|
// new TypesetCharacter(
|
|
|
|
// chr,
|
|
|
|
// texture,
|
|
|
|
// new QRectangle(
|
|
|
|
// penpal.X + metrics.Advance.X,
|
|
|
|
// penpal.Y + metrics.HorizontalBearing.Y,
|
|
|
|
// penpal.X + metrics.HorizontalBearing.X,
|
|
|
|
// penpal.Y - metrics.Size.Y + metrics.HorizontalBearing.Y),
|
|
|
|
// metrics.Location
|
|
|
|
// )
|
|
|
|
// );
|
|
|
|
|
|
|
|
// penpal.X += metrics.Advance.X;
|
2023-05-13 15:17:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
penpal.X += interblockWs;
|
|
|
|
}
|
|
|
|
|
|
|
|
penpal.X -= interblockWs;
|
|
|
|
|
|
|
|
pen = penpal;
|
|
|
|
}
|
|
|
|
|
2023-06-29 13:17:32 +02:00
|
|
|
protected override void AppendBlock(object font, string text, bool rtl = false)
|
2023-05-13 15:17:57 +02:00
|
|
|
{
|
|
|
|
Blocks.Add(new HorizontalTextBlock(font, text, rtl));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public class VerticalParagraph : Paragraph
|
|
|
|
{
|
|
|
|
public override bool IsVertical => true;
|
|
|
|
public List<VerticalTextBlock> Blocks { get; } = new List<VerticalTextBlock>();
|
|
|
|
|
|
|
|
public override void Typeset(TypesetGroup group, float width)
|
|
|
|
{
|
|
|
|
throw new NotImplementedException();
|
|
|
|
}
|
|
|
|
|
2023-06-29 13:17:32 +02:00
|
|
|
protected override void AppendBlock(object font, string text, bool rtl = false)
|
2023-05-13 15:17:57 +02:00
|
|
|
{
|
|
|
|
Blocks.Add(new VerticalTextBlock(font, text));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public struct TypesetCharacter
|
|
|
|
{
|
|
|
|
public int Character;
|
2023-05-15 20:49:48 +02:00
|
|
|
public QuikTexture Texture;
|
2023-06-29 09:42:02 +02:00
|
|
|
public QRectangle Position;
|
|
|
|
public QRectangle UV;
|
2023-05-13 15:17:57 +02:00
|
|
|
|
|
|
|
public TypesetCharacter(
|
|
|
|
int chr,
|
2023-05-15 20:49:48 +02:00
|
|
|
QuikTexture texture,
|
2023-06-29 09:42:02 +02:00
|
|
|
in QRectangle position,
|
|
|
|
in QRectangle uv)
|
2023-05-13 15:17:57 +02:00
|
|
|
{
|
|
|
|
Character = chr;
|
|
|
|
Texture = texture;
|
|
|
|
Position = position;
|
|
|
|
UV = uv;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public class TypesetGroup : ICollection<TypesetCharacter>
|
|
|
|
{
|
|
|
|
private int _count = 0;
|
|
|
|
private TypesetCharacter[] _array = Array.Empty<TypesetCharacter>();
|
|
|
|
|
2023-06-29 09:42:02 +02:00
|
|
|
public QRectangle BoundingBox;
|
2023-05-13 15:17:57 +02:00
|
|
|
|
|
|
|
public int Count => _count;
|
|
|
|
|
|
|
|
public bool IsReadOnly => false;
|
|
|
|
|
|
|
|
public void Add(TypesetCharacter item)
|
|
|
|
{
|
|
|
|
if (_count == _array.Length)
|
|
|
|
{
|
|
|
|
Array.Resize(ref _array, _array.Length + 256);
|
|
|
|
}
|
|
|
|
|
|
|
|
_array[_count++] = item;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Clear()
|
|
|
|
{
|
|
|
|
_count = 0;
|
|
|
|
}
|
|
|
|
|
2023-06-29 09:42:02 +02:00
|
|
|
public void Translate(QVec2 offset)
|
2023-05-13 15:17:57 +02:00
|
|
|
{
|
|
|
|
BoundingBox.Translate(offset);
|
|
|
|
|
|
|
|
for (int i = 0; i < _count; i++)
|
|
|
|
{
|
|
|
|
_array[i].Position.Translate(offset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool Contains(TypesetCharacter item)
|
|
|
|
{
|
|
|
|
throw new NotSupportedException();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void CopyTo(TypesetCharacter[] array, int arrayIndex)
|
|
|
|
{
|
|
|
|
_array.CopyTo(array, arrayIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
public IEnumerator<TypesetCharacter> GetEnumerator()
|
|
|
|
{
|
|
|
|
for (int i = 0; i < _count; i++)
|
|
|
|
{
|
|
|
|
yield return _array[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool Remove(TypesetCharacter item)
|
|
|
|
{
|
|
|
|
throw new NotSupportedException();
|
|
|
|
}
|
|
|
|
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
|
|
{
|
|
|
|
return (IEnumerator)GetEnumerator();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SortBy(IComparer<TypesetCharacter> comparer)
|
|
|
|
{
|
|
|
|
Array.Sort(_array, 0, _count, comparer);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static IComparer<TypesetCharacter> SortByTexture { get; } = new SortByTextureComparer();
|
|
|
|
|
|
|
|
private class SortByTextureComparer : IComparer<TypesetCharacter>
|
|
|
|
{
|
|
|
|
public int Compare(TypesetCharacter x, TypesetCharacter y)
|
|
|
|
{
|
|
|
|
return y.Texture.GetHashCode() - x.Texture.GetHashCode();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// An enumeration of possible text alignments.
|
|
|
|
/// </summary>
|
|
|
|
[Flags]
|
|
|
|
public enum TextAlignment
|
|
|
|
{
|
|
|
|
/// <summary>
|
|
|
|
/// Align to the left margin, horizontally.
|
|
|
|
/// </summary>
|
|
|
|
AlignLeft = 0x01,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Align text to center of left and right margins.
|
|
|
|
/// </summary>
|
|
|
|
AlignCenterH = 0x03,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Align to the right margin, horizontally.
|
|
|
|
/// </summary>
|
|
|
|
AlignRight = 0x02,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A bitmask for values relating to horizontal alignment.
|
|
|
|
/// </summary>
|
|
|
|
HorizontalMask = 0x03,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Align text to the top margin.
|
|
|
|
/// </summary>
|
|
|
|
AlignTop = 0x00,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Align text between the top and bottom margins.
|
|
|
|
/// <summary>
|
|
|
|
AlignCenterV = 0x04,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Align text to the bottom margin.
|
|
|
|
/// </summary>
|
|
|
|
AlignBottom = 0x08,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A bitmask for values relating to the vertical alignment.
|
|
|
|
/// </summary>
|
|
|
|
VerticalMask = 0x0C,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Distribute characters uniformly on the line, when possible.
|
|
|
|
/// <summary>
|
|
|
|
Justify = 0x10,
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The default text alignment value.
|
|
|
|
/// </summary>
|
|
|
|
Default = AlignTop | AlignLeft
|
|
|
|
}
|
|
|
|
}
|