Dashboard/Quik/Typography/TextLayout.cs

509 lines
15 KiB
C#
Raw Normal View History

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;
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; }
/// <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-06-29 13:17:32 +02:00
// Font = font;
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);
}
Width = width;
Ascend = ascend;
Descend = descend;
}
public HorizontalTextBlock(float width)
{
2023-06-29 13:17:32 +02:00
// Font = null;
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; }
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-06-29 13:17:32 +02:00
// Font = font;
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;
}
Width = width;
Height = height;
}
public VerticalTextBlock(float height)
{
2023-06-29 13:17:32 +02:00
// Font = null;
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-06-29 13:17:32 +02:00
public void ConsumeText(object font, string text)
{
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;
QVec2 pen = new QVec2(0, -PreSpace);
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;
group.BoundingBox = new QRectangle(width, 0, 0, pen.Y);
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,
ref QVec2 pen)
{
QVec2 penpal = pen;
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;
}
}
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;
}
}
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)
{
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)
{
Blocks.Add(new VerticalTextBlock(font, text));
}
}
public struct TypesetCharacter
{
public int Character;
public QuikTexture Texture;
public QRectangle Position;
public QRectangle UV;
public TypesetCharacter(
int chr,
QuikTexture texture,
in QRectangle position,
in QRectangle uv)
{
Character = chr;
Texture = texture;
Position = position;
UV = uv;
}
}
public class TypesetGroup : ICollection<TypesetCharacter>
{
private int _count = 0;
private TypesetCharacter[] _array = Array.Empty<TypesetCharacter>();
public QRectangle BoundingBox;
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;
}
public void Translate(QVec2 offset)
{
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
}
}