using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text; using Quik.Media; namespace Quik.Typography { /// /// An atomic horizontal block of text which cannot be further divided. /// public struct HorizontalTextBlock { /// /// The font associated with the text block. /// /// // public QuikFont Font { get; } /// /// Textual contents of the text block. /// public string Text { get; } /// /// Indicates this text block should be layed out right to left. /// public bool IsRTL { get; } /// /// Indicates this is a whitespace block. /// 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; } } /// /// An atomic vertical block of text which cannot be further divided. /// 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 Blocks { get; } = new List(); public override void Typeset(TypesetGroup group, float width) { Queue line = new Queue(); 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 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 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 Blocks { get; } = new List(); 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 { private int _count = 0; private TypesetCharacter[] _array = Array.Empty(); 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 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 comparer) { Array.Sort(_array, 0, _count, comparer); } public static IComparer SortByTexture { get; } = new SortByTextureComparer(); private class SortByTextureComparer : IComparer { public int Compare(TypesetCharacter x, TypesetCharacter y) { return y.Texture.GetHashCode() - x.Texture.GetHashCode(); } } } /// /// An enumeration of possible text alignments. /// [Flags] public enum TextAlignment { /// /// Align to the left margin, horizontally. /// AlignLeft = 0x01, /// /// Align text to center of left and right margins. /// AlignCenterH = 0x03, /// /// Align to the right margin, horizontally. /// AlignRight = 0x02, /// /// A bitmask for values relating to horizontal alignment. /// HorizontalMask = 0x03, /// /// Align text to the top margin. /// AlignTop = 0x00, /// /// Align text between the top and bottom margins. /// AlignCenterV = 0x04, /// /// Align text to the bottom margin. /// AlignBottom = 0x08, /// /// A bitmask for values relating to the vertical alignment. /// VerticalMask = 0x0C, /// /// Distribute characters uniformly on the line, when possible. /// Justify = 0x10, /// /// The default text alignment value. /// Default = AlignTop | AlignLeft } }