using System; using System.Collections.Generic; using UIWidgets.ui.txt; using UnityEngine; namespace UIWidgets.ui { public struct Vector2d { public double x; public double y; public Vector2d(double x = 0.0, double y = 0.0) { this.x = x; this.y = y; } public static Vector2d operator +(Vector2d a, Vector2d b) { return new Vector2d(a.x + b.x, a.y + b.y); } public static Vector2d operator -(Vector2d a, Vector2d b) { return new Vector2d(a.x - b.x, a.y - b.y); } } public class Paragraph { struct Range: IEquatable> { public Range(T start, T end) { this.start = start; this.end = end; } public bool Equals(Range other) { return EqualityComparer.Default.Equals(start, other.start) && EqualityComparer.Default.Equals(end, other.end); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; return obj is Range && Equals((Range) obj); } public override int GetHashCode() { unchecked { return (EqualityComparer.Default.GetHashCode(start) * 397) ^ EqualityComparer.Default.GetHashCode(end); } } public static bool operator ==(Range left, Range right) { return left.Equals(right); } public static bool operator !=(Range left, Range right) { return !left.Equals(right); } public T start, end; } class LineRange { public LineRange(int start, int end, int endExcludingWhitespace, int endIncludingNewLine, bool hardBreak) { this.start = start; this.end = end; this.endExcludingWhitespace = endExcludingWhitespace; this.endIncludingNewLine = endIncludingNewLine; this.hardBreak = hardBreak; } public readonly int start; public readonly int end; public readonly int endExcludingWhitespace; public readonly int endIncludingNewLine; public readonly bool hardBreak; } private static readonly Shader textShader; static Paragraph() { textShader = Shader.Find("UIWidgets/Text Shader"); if (textShader == null) { throw new Exception("UIWidgets/Text Shader Lines not found"); } } private bool _needsLayout = true; private string _text; private StyledRuns _runs; private ParagraphStyle _paragraphStyle; private List _lineRanges = new List(); private List _lineWidths = new List(); private Vector2d[] _characterPositions; private double _maxIntrinsicWidth; private double _minIntrinsicWidth; private double _alphabeticBaseline; private double _ideographicBaseline; private double[] _characterWidths; private List _lineHeights = new List(); private List _paintRecords = new List(); private bool _didExceedMaxLines; // private double _characterWidth; private double _width; public const char CHAR_NBSP = '\u00A0'; public static bool isWordSpace(char ch) { return ch == ' ' || ch == CHAR_NBSP; } // This function determines whether a character is a space that disappears at end of line. // It is the Unicode set: [[:General_Category=Space_Separator:]-[:Line_Break=Glue:]], // plus '\n'. // Note: all such characters are in the BMP, so it's ok to use code units for this. public static bool isLineEndSpace(char c) { return c == '\n' || c == ' ' || c == 0x1680 || (0x2000 <= c && c <= 0x200A && c != 0x2007) || c == 0x205F || c == 0x3000; } public double height { get { return _lineHeights.Count == 0 ? 0 : _lineHeights[_lineHeights.Count - 1]; } } public double minIntrinsicWidth { get { return _minIntrinsicWidth; } } public double maxIntrinsicWidth { get { return _maxIntrinsicWidth; } } public double width { get { return _width; } } public double alphabeticBaseline { get { return _alphabeticBaseline; } } public double ideographicBaseline { get { return _ideographicBaseline; } } public bool didExceedMaxLines { get { return _didExceedMaxLines; } } public void paint(Canvas canvas, double x, double y) { foreach (var paintRecord in _paintRecords) { canvas.drawTextBlob(paintRecord.text, x, y); } } public void layout(ParagraphConstraints constraints) { if (!_needsLayout && _width == constraints.width) { return; } _needsLayout = false; _width = Math.Floor(constraints.width); this.setup(); computeLineBreak(); var maxLines = _paragraphStyle.maxLines ?? 0; _didExceedMaxLines = !(maxLines == 0 || _lineRanges.Count <= maxLines); var lineLimits = maxLines == 0 ? _lineRanges.Count : Math.Min(maxLines, _lineRanges.Count); layoutLines(lineLimits); double maxWordWidth = 0; for (int lineNumber = 0; lineNumber < lineLimits; ++lineNumber) { var line = _lineRanges[lineNumber]; var words = findWords(line.start, line.end); words.ForEach((word) => { Debug.Assert(word.start < word.end); double wordWidth = _characterPositions[word.end - 1].x - _characterPositions[word.start].x + _characterWidths[word.end - 1]; if (wordWidth > maxWordWidth) { maxWordWidth = wordWidth; } }); if (_paragraphStyle.TextAlign == TextAlign.justify && !_lineRanges[lineNumber].hardBreak && lineNumber != lineLimits - 1) { justifyLine(lineNumber, words); } else if (line.endExcludingWhitespace > line.start) { Debug.Assert(!isLineEndSpace(_text[line.endExcludingWhitespace - 1])); var lineTotalAdvance = _characterPositions[line.endExcludingWhitespace - 1].x + _characterWidths[line.endExcludingWhitespace - 1]; double xOffset = getLineXOffset(lineTotalAdvance); if (xOffset > 0 || xOffset < 0) { offsetCharacters(new Vector2d(xOffset, 0), _characterPositions, line.start, line.endExcludingWhitespace); } } } computeWidthMetrics(maxWordWidth); } public void setText(string text, StyledRuns runs) { _text = text; _runs = runs; _needsLayout = true; } public void setParagraphStyle(ParagraphStyle style) { _needsLayout = true; _paragraphStyle = style; } public static void offsetCharacters(Vector2d offset, Vector2d[] characterPos, int start, int end) { if (characterPos != null) { for (int i = start; i < characterPos.Length && i < end; ++i) { characterPos[i] = characterPos[i] + offset; } } } private void computeWidthMetrics(double maxWordWidth) { _maxIntrinsicWidth = 0; double lineBlockWidth = 0; for (int i = 0; i < _lineWidths.Count; ++i) { var line = _lineRanges[i]; lineBlockWidth += _lineWidths[i]; if (line.hardBreak) { _maxIntrinsicWidth = Math.Max(lineBlockWidth, _maxIntrinsicWidth); lineBlockWidth = 0; } } if (_paragraphStyle.maxLines == 1 || (((_paragraphStyle.maxLines??0) == 0) && !string.IsNullOrEmpty(_paragraphStyle.ellipsis))) { _minIntrinsicWidth = _maxIntrinsicWidth; } else { _minIntrinsicWidth = Math.Min(maxWordWidth, _maxIntrinsicWidth); } } private void setup() { if (_characterPositions == null || _characterPositions.Length < _text.Length) { _characterPositions = new Vector2d[_text.Length]; } _lineHeights.Clear(); _lineRanges.Clear(); _lineWidths.Clear(); _paintRecords.Clear(); _characterWidths = new double[_text.Length]; for (int i = 0; i < _runs.size; ++i) { var run = _runs.getRun(i); if (run.start < run.end) { var font = FontManager.instance.getOrCreate(run.style.safeFontFamily, run.style.UnityFontSize); font.RequestCharactersInTexture(_text.Substring(run.start, run.end - run.start), 0, run.style.UnityFontStyle); } } } private void layoutLines(int lineLimits) { double yOffset = 0; var runIndex = 0; double lastDescent = 0.0f; for (int lineNumber = 0; lineNumber < lineLimits; lineNumber++) { var line = _lineRanges[lineNumber]; double maxAscent = 0.0f; double maxDescent = 0.0f; for (;;) { var run = _runs.getRun(runIndex); if (run.start < run.end && run.start < line.end && run.end > line.start) { var font = FontManager.instance.getOrCreate(run.style.safeFontFamily, run.style.UnityFontSize); var ascent = font.ascent * (run.style.height??1.0); var descent = (font.lineHeight - font.ascent) * (run.style.height??1.0); if (ascent > maxAscent) { maxAscent = ascent; } if (descent > maxDescent) { maxDescent = descent; } int start = Math.Max(run.start, line.start); int end = Math.Min(run.end, line.end); var width = _characterPositions[end - 1].x + _characterWidths[end - 1] - _characterPositions[start].x; if (end > start) { var bounds = Rect.fromLTWH(0, -ascent, _characterPositions[end - 1].x + _characterWidths[end - 1] - _characterPositions[start].x, descent); _paintRecords.Add(new PaintRecord(run.style, new TextBlob( _text, start, end, _characterPositions, run.style, bounds), lineNumber, width)); } } if (runIndex + 1 >= _runs.size) { break; } if (run.end < line.end) { runIndex++; } else { break; } } if (lineNumber == 0) { _alphabeticBaseline = maxAscent; _ideographicBaseline = maxAscent; // todo Properly implement ideographic_baseline } lastDescent = maxDescent; yOffset = Utils.PixelCorrectRound(yOffset + maxAscent + lastDescent); for (var charIndex = line.start; charIndex < line.end; charIndex++) { _characterPositions[charIndex].y = yOffset; } _lineHeights.Add((_lineHeights.Count == 0 ? 0 : _lineHeights[_lineHeights.Count - 1]) + Math.Round(maxAscent + maxDescent)); } } private void computeLineBreak() { var newLinePositions = new List(); for (var i = 0; i < _text.Length; i++) { if (_text[i] == '\n') { newLinePositions.Add(i); } } newLinePositions.Add(_text.Length); var lineBreaker = new LineBreaker(); lineBreaker.setup(_text, _runs, _width, _characterPositions, _characterWidths); for (var newlineIndex = 0; newlineIndex < newLinePositions.Count; ++newlineIndex) { var blockStart = newlineIndex > 0 ? newLinePositions[newlineIndex - 1] + 1 : 0; var blockEnd = newLinePositions[newlineIndex]; var blockSize = blockEnd - blockStart; if (blockSize == 0) { _lineRanges.Add(new LineRange(blockStart, blockEnd, blockEnd, blockEnd + 1, true)); _lineWidths.Add(0); continue; } lineBreaker.doBreak(blockStart, blockEnd); var lines = lineBreaker.getLines(); for (int i = 0; i < lines.Count; ++i) { var line = lines[i]; var end = i + 1 < lines.Count ? lines[i + 1].start : blockEnd; var nonSpaceEnd = end; while (nonSpaceEnd > line.start && isLineEndSpace(_text[nonSpaceEnd - 1])) { nonSpaceEnd--; } _lineRanges.Add(new LineRange(line.start, end, nonSpaceEnd, end + 1, end == blockEnd)); _lineWidths.Add(line.width); } } return; } private double getLineXOffset(double lineTotalAdvance) { if (double.IsInfinity(_width)) { return 0; } var align = _paragraphStyle.TextAlign; if (align == TextAlign.right) { return _width - lineTotalAdvance; } else if (align == TextAlign.center) { return Utils.PixelCorrectRound((_width - lineTotalAdvance) / 2); } else { return 0; } } private void justifyLine(int lineNumber, List> words) { if (words.Count <= 1) { return; } var line = _lineRanges[lineNumber]; Debug.Assert(!isLineEndSpace(_text[line.endExcludingWhitespace - 1])); var lineTotalAdvance = _characterPositions[line.endExcludingWhitespace - 1].x + _characterWidths[line.endExcludingWhitespace - 1]; double gapWidth = (_width - lineTotalAdvance) / (words.Count - 1); double justifyOffset = 0.0; foreach (var word in words) { offsetCharacters(new Vector2d(justifyOffset), _characterPositions, word.start, word.end); justifyOffset += gapWidth; justifyOffset = Utils.PixelCorrectRound(justifyOffset); } } private List> findWords(int start, int end) { var inWord = false; int wordStart = 0; List> words = new List>(); for (int i = start; i < end; ++i) { bool isSpace = isWordSpace(_text[i]); if (!inWord && !isSpace) { wordStart = i; inWord = true; } else if (inWord && isSpace) { words.Add(new Range(wordStart, i)); inWord = false; } } if (inWord) { words.Add(new Range(wordStart, end)); } return words; } } }