using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text; 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> // 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, 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]; // 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 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 } }