123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541 |
- using System;
- using System.Collections.Generic;
- using System.Text;
- #if MONOGAME || FNA
- using Microsoft.Xna.Framework;
- #elif STRIDE
- using Stride.Core.Mathematics;
- #else
- using System.Drawing;
- using Color = Veldrid.RgbaFloat;
- #endif
- namespace FontStashSharp.RichText
- {
- internal class LayoutBuilder
- {
- public const int NewLineWidth = 0;
- public const string Commands = "cfivst";
- private string _text;
- private SpriteFontBase _font;
- private bool _measureRun;
- private readonly List<TextLine> _lines = new List<TextLine>();
- private TextLine _currentLine;
- private int lineTop, lineBottom;
- private int? width;
- private int _lineCount;
- private int _currentLineWidth;
- private int _currentLineChunks;
- private readonly StringBuilder _stringBuilder = new StringBuilder();
- private Color? _currentColor;
- private SpriteFontBase _currentFont;
- private int _currentVerticalOffset;
- private TextStyle _currentTextStyle;
- public List<TextLine> Lines => _lines;
- public int VerticalSpacing { get; set; }
- public bool SupportsCommands { get; set; } = true;
- public bool CalculateGlyphs { get; set; }
- public bool ShiftByTop { get; set; } = true;
- public char CommandPrefix { get; set; } = '/';
- private bool IsCommand(int i)
- {
- if (!SupportsCommands ||
- i >= _text.Length - 2 ||
- _text[i] != CommandPrefix ||
- Commands.IndexOf(_text[i + 1]) == -1)
- {
- return false;
- }
- ++i;
- var command = _text[i].ToString();
- if (command == "t")
- {
-
- switch (_text[i + 1])
- {
- case 's':
- case 'u':
- case 'd':
- break;
- default:
- return false;
- }
- }
- else if (_text[i + 1] == 'd')
- {
- switch (command)
- {
- case "c":
- case "f":
- case "v":
- break;
- default:
- return false;
- }
- }
- else
- {
- if (_text[i + 1] != '[')
- {
- return false;
- }
- // Find end
- var startPos = i + 2;
- int j;
- var foundEnclosing = false;
- for (j = startPos; j < _text.Length; ++j)
- {
- if (_text[j] == ']')
- {
- // Found enclosing 'j'
- foundEnclosing = true;
- break;
- }
- else if (_text[j] == '[')
- {
- break;
- }
- }
- if (!foundEnclosing)
- {
- return false;
- }
- }
- return true;
- }
- private bool ProcessCommand(ref int i, ref ChunkInfo r, out bool chunkFilled)
- {
- chunkFilled = false;
- if (!IsCommand(i))
- {
- return false;
- }
- ++i;
- var command = _text[i].ToString();
- if (command == "t")
- {
- switch (_text[i + 1])
- {
- case 's':
- _currentTextStyle = TextStyle.Strikethrough;
- break;
- case 'u':
- _currentTextStyle = TextStyle.Underline;
- break;
- case 'd':
- _currentTextStyle = TextStyle.None;
- break;
- }
- i += 2;
- }
- else if (_text[i + 1] == 'd')
- {
- switch (command)
- {
- case "c":
- _currentColor = null;
- break;
- case "f":
- // Switch to default font
- _currentFont = _font;
- break;
- case "v":
- _currentVerticalOffset = 0;
- break;
- }
- i += 2;
- }
- else
- {
- // Find end
- var startPos = i + 2;
- int j;
- for (j = startPos; j < _text.Length; ++j)
- {
- if (_text[j] == ']')
- {
- // Found enclosing 'j'
- break;
- }
- }
- var parameters = _text.Substring(startPos, j - startPos);
- switch (command)
- {
- case "c":
- _currentColor = ColorStorage.FromName(parameters);
- break;
- case "f":
- if (RichTextDefaults.FontResolver == null)
- {
- throw new Exception($"FontResolver isnt set");
- }
- _currentFont = RichTextDefaults.FontResolver(parameters);
- break;
- case "s":
- var size = int.Parse(parameters);
- r.Type = ChunkInfoType.Space;
- r.X = size;
- r.Y = 0;
- r.LineEnd = false;
- chunkFilled = true;
- break;
- case "v":
- _currentVerticalOffset = int.Parse(parameters);
- break;
- case "i":
- if (RichTextDefaults.ImageResolver == null)
- {
- throw new Exception($"ImageResolver isnt set");
- }
- var renderable = RichTextDefaults.ImageResolver(parameters);
- r.Type = ChunkInfoType.Image;
- r.Renderable = renderable;
- r.LineEnd = false;
- chunkFilled = true;
- break;
- }
- i = j + 1;
- }
- return true;
- }
- private ChunkInfo GetNextChunk(ref int i, int? remainingWidth)
- {
- var r = new ChunkInfo
- {
- LineEnd = true
- };
- // Process commands at the beginning of the chunk
- bool chunkFilled;
- while (ProcessCommand(ref i, ref r, out chunkFilled))
- {
- if (chunkFilled)
- {
- // Content chunk(image or space) is filled, return it
- return r;
- }
- }
- _stringBuilder.Clear();
- r.StartIndex = r.EndIndex = i;
- Point? lastBreakMeasure = null;
- var lastBreakIndex = i;
- for (; i < _text.Length; ++i, ++r.EndIndex)
- {
- var c = _text[i];
- if (char.IsHighSurrogate(c))
- {
- _stringBuilder.Append(c);
- continue;
- }
- if (SupportsCommands &&
- c == CommandPrefix &&
- i < _text.Length - 1)
- {
- if (_text[i + 1] == 'n')
- {
- var sz2 = new Point(r.X + NewLineWidth, Math.Max(r.Y, _currentFont.LineHeight));
- // Break right here
- r.X = sz2.X;
- r.Y = sz2.Y;
- i += 2;
- break;
- }
- if (i < _text.Length - 1 && _text[i + 1] == CommandPrefix)
- {
- // Two '\' means one
- ++i; ++r.EndIndex;
- }
- else if (IsCommand(i))
- {
- // Return right here, so the command
- // would be processed in the next chunk
- r.LineEnd = false;
- break;
- }
- }
- _stringBuilder.Append(c);
- Point sz;
- if (c != '\n')
- {
- var v = _currentFont.MeasureString(_stringBuilder);
- sz = new Point((int)v.X, _font.LineHeight);
- }
- else
- {
- sz = new Point(r.X + NewLineWidth, Math.Max(r.Y, _font.LineHeight));
- // Break right here
- ++r.EndIndex;
- ++i;
- r.X = sz.X;
- r.Y = sz.Y;
- break;
- }
- if (remainingWidth != null && sz.X > remainingWidth.Value && i > r.StartIndex &&
- (lastBreakMeasure != null || _currentLineChunks == 0))
- {
- if (lastBreakMeasure != null)
- {
- r.X = lastBreakMeasure.Value.X;
- r.Y = lastBreakMeasure.Value.Y;
- r.EndIndex = i = lastBreakIndex;
- }
- break;
- }
- if (char.IsWhiteSpace(c) || c == '.')
- {
- lastBreakMeasure = sz;
- lastBreakIndex = i + 1;
- }
- r.X = sz.X;
- r.Y = sz.Y;
- }
- return r;
- }
- private void ResetCurrents()
- {
- _currentColor = null;
- _currentFont = _font;
- _currentVerticalOffset = 0;
- _currentTextStyle = TextStyle.None;
- }
- private void StartLine(int startIndex, int? rowWidth)
- {
- if (!_measureRun)
- {
- _currentLine = new TextLine
- {
- TextStartIndex = startIndex
- };
- }
- lineTop = 0;
- lineBottom = 0;
- _currentLineWidth = 0;
- _currentLineChunks = 0;
- width = rowWidth;
- }
- private void EndLine(ref Point size)
- {
- var lineHeight = lineBottom - lineTop;
- ++_lineCount;
- if (_currentLineWidth > size.X)
- {
- size.X = _currentLineWidth;
- }
- size.Y += lineHeight;
- if (!_measureRun)
- {
- if (ShiftByTop)
- {
- // Shift all chunks top by lineTop
- foreach (var lineChunk in _currentLine.Chunks)
- {
- lineChunk.VerticalOffset -= lineTop;
- }
- }
- _currentLine.Size.Y = lineHeight;
- // New line
- _lines.Add(_currentLine);
- }
- }
- public Point Layout(string text, SpriteFontBase font, int? rowWidth, bool measureRun = false)
- {
- if (!measureRun)
- {
- _lines.Clear();
- }
- _lineCount = 0;
- var size = Utility.PointZero;
- if (string.IsNullOrEmpty(text))
- {
- return size;
- }
- _text = text;
- _font = font;
- _measureRun = measureRun;
- ResetCurrents();
- var i = 0;
- StartLine(0, rowWidth);
- while (i < _text.Length)
- {
- var c = GetNextChunk(ref i, width);
- if (width != null && c.Width > width.Value && _currentLineChunks > 0)
- {
- // New chunk doesn't fit in the line
- // Hence move it to the second
- EndLine(ref size);
- StartLine(i, rowWidth);
- c.LineEnd = false;
- }
- width -= c.Width;
- if (_currentVerticalOffset < lineTop)
- {
- lineTop = _currentVerticalOffset;
- }
- if (_currentVerticalOffset + c.Height > lineBottom)
- {
- lineBottom = _currentVerticalOffset + c.Height;
- }
- _currentLineWidth += c.Width;
- if (!_measureRun)
- {
- Point? startPos = null;
- if (CalculateGlyphs)
- {
- startPos = new Point(_currentLine.Size.X, size.Y);
- }
- _currentLine.Size.X += c.Width;
- BaseChunk chunk = null;
- switch (c.Type)
- {
- case ChunkInfoType.Text:
- var t = _text.Substring(c.StartIndex, c.EndIndex - c.StartIndex);
- if (SupportsCommands)
- {
- t = t.Replace("//", "/");
- }
- var textChunk = new TextChunk(_currentFont, t, new Point(c.X, c.Y), startPos)
- {
- Style = _currentTextStyle
- };
- chunk = textChunk;
- break;
- case ChunkInfoType.Space:
- chunk = new SpaceChunk(c.X);
- break;
- case ChunkInfoType.Image:
- chunk = new ImageChunk(c.Renderable);
- break;
- }
- chunk.Color = _currentColor;
- chunk.VerticalOffset = _currentVerticalOffset;
- var asText = chunk as TextChunk;
- if (asText != null)
- {
- _currentLine.Count += asText.Count;
- }
- _currentLine.Chunks.Add(chunk);
- }
- ++_currentLineChunks;
- if (c.LineEnd)
- {
- EndLine(ref size);
- StartLine(i, rowWidth);
- }
- }
- // Add last line if it isnt empty
- if (_currentLineChunks > 0)
- {
- EndLine(ref size);
- }
- // If text ends with '\n', then add additional line
- if (_text[_text.Length - 1] == '\n')
- {
- var lineSize = _currentFont.MeasureString(" ");
- if (!_measureRun)
- {
- var additionalLine = new TextLine
- {
- TextStartIndex = _text.Length
- };
- additionalLine.Size.Y = (int)lineSize.Y;
- _lines.Add(additionalLine);
- }
- size.Y += (int)lineSize.Y;
- }
- // Index lines and chunks
- if (!_measureRun)
- {
- for (i = 0; i < _lines.Count; ++i)
- {
- _currentLine = _lines[i];
- _currentLine.LineIndex = i;
- for (var j = 0; j < _currentLine.Chunks.Count; ++j)
- {
- var chunk = _currentLine.Chunks[j];
- chunk.LineIndex = _currentLine.LineIndex;
- chunk.ChunkIndex = j;
- }
- }
- }
- size.Y += (_lineCount - 1) * VerticalSpacing;
- return size;
- }
- }
- }
|