Due to unforseen naming conflicts, the project has been rebranded under the ReFuel umbrealla and will now be referred to as Dashboard from now on. Other changes will occur to suit the library more for the engine whilst keeping the freestanding nature of the library. Rename folder. Rename to Dashboard.OpenTK Rename to Dashboard.Media.Defaults. Do the last renames and path fixes.
509 lines
15 KiB
C#
509 lines
15 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using Dashboard.Media;
|
|
|
|
namespace Dashboard.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>
|
|
// 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;
|
|
|
|
public HorizontalTextBlock(object font, string text, bool rtl = false)
|
|
{
|
|
// Font = font;
|
|
Text = text;
|
|
IsRTL = rtl;
|
|
|
|
float width = 0.0f;
|
|
float ascend = 0.0f;
|
|
float descend = 0.0f;
|
|
|
|
foreach (char chr in text)
|
|
{
|
|
// 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)
|
|
{
|
|
// 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
|
|
{
|
|
// public QuikFont Font { get; }
|
|
public string Text { get; }
|
|
public bool IsWhitespace => string.IsNullOrWhiteSpace(Text);
|
|
public float Width { get; }
|
|
public float Height { get; }
|
|
|
|
public VerticalTextBlock(object font, string text)
|
|
{
|
|
// Font = font;
|
|
Text = text;
|
|
|
|
float width = 0.0f;
|
|
float height = 0.0f;
|
|
|
|
foreach(char chr in text)
|
|
{
|
|
// 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)
|
|
{
|
|
// 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);
|
|
|
|
protected abstract void AppendBlock(object font, string text, bool rtl = false);
|
|
|
|
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, pen.Y, 0, 0);
|
|
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];
|
|
// 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];
|
|
// 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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
protected override void AppendBlock(object font, string text, bool rtl = false)
|
|
{
|
|
Blocks.Add(new VerticalTextBlock(font, text));
|
|
}
|
|
}
|
|
|
|
public struct TypesetCharacter
|
|
{
|
|
public int Character;
|
|
public QImage Texture;
|
|
public QRectangle Position;
|
|
public QRectangle UV;
|
|
|
|
public TypesetCharacter(
|
|
int chr,
|
|
QImage 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
|
|
}
|
|
} |