using System; using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Media.TextFormatting { internal class TextFormatterImpl : TextFormatter { /// public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null) { var textWrapping = paragraphProperties.TextWrapping; var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak); var textRange = GetTextRange(textRuns); TextLine textLine; switch (textWrapping) { case TextWrapping.NoWrap: { textLine = new TextLineImpl(textRuns, textRange, paragraphWidth, paragraphProperties, nextLineBreak); break; } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: { textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties, nextLineBreak); break; } default: throw new ArgumentOutOfRangeException(); } return textLine; } /// /// Measures the number of characters that fit into available width. /// /// The text run. /// The available width. /// The count of fitting characters. /// /// true if characters fit into the available width; otherwise, false. /// internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth, out int count) { var glyphRun = textCharacters.GlyphRun; if (glyphRun.Size.Width < availableWidth) { count = glyphRun.Characters.Length; return true; } var glyphCount = 0; var currentWidth = 0.0; if (glyphRun.GlyphAdvances.IsEmpty) { var glyphTypeface = glyphRun.GlyphTypeface; if (glyphRun.IsLeftToRight) { foreach (var glyph in glyphRun.GlyphIndices) { var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; if (currentWidth + advance > availableWidth) { break; } currentWidth += advance; glyphCount++; } } else { for (var index = glyphRun.GlyphClusters.Length - 1; index > 0; index--) { var glyph = glyphRun.GlyphIndices[index]; var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; if (currentWidth + advance > availableWidth) { break; } currentWidth += advance; glyphCount++; } } } else { if (glyphRun.IsLeftToRight) { for (var index = 0; index < glyphRun.GlyphAdvances.Length; index++) { var advance = glyphRun.GlyphAdvances[index]; if (currentWidth + advance > availableWidth) { break; } currentWidth += advance; glyphCount++; } } else { for (var index = glyphRun.GlyphAdvances.Length - 1; index > 0; index--) { var advance = glyphRun.GlyphAdvances[index]; if (currentWidth + advance > availableWidth) { break; } currentWidth += advance; glyphCount++; } } } if (glyphCount == 0) { count = 0; return false; } if (glyphCount == glyphRun.GlyphIndices.Length) { count = glyphRun.Characters.Length; return true; } if (glyphRun.GlyphClusters.IsEmpty) { count = glyphCount; return true; } var firstCluster = glyphRun.GlyphClusters[0]; var lastCluster = glyphRun.GlyphClusters[glyphCount]; if (glyphRun.IsLeftToRight) { count = lastCluster - firstCluster; } else { count = firstCluster - lastCluster; } return count > 0; } /// /// Split a sequence of runs into two segments at specified length. /// /// The text run's. /// The length to split at. /// The split text runs. internal static SplitTextRunsResult SplitTextRuns(List textRuns, int length) { var currentLength = 0; for (var i = 0; i < textRuns.Count; i++) { var currentRun = textRuns[i]; if (currentLength + currentRun.GlyphRun.Characters.Length < length) { currentLength += currentRun.GlyphRun.Characters.Length; continue; } var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; var first = new List(firstCount); if (firstCount > 1) { for (var j = 0; j < i; j++) { first.Add(textRuns[j]); } } var secondCount = textRuns.Count - firstCount; if (currentLength + currentRun.GlyphRun.Characters.Length == length) { var second = new List(secondCount); var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; if (secondCount > 0) { for (var j = 0; j < secondCount; j++) { second.Add(textRuns[i + j + offset]); } } first.Add(currentRun); return new SplitTextRunsResult(first, second); } else { secondCount++; var second = new List(secondCount); var split = currentRun.Split(length - currentLength); first.Add(split.First); second.Add(split.Second); if (secondCount > 0) { for (var j = 1; j < secondCount; j++) { second.Add(textRuns[i + j]); } } return new SplitTextRunsResult(first, second); } } return new SplitTextRunsResult(textRuns, null); } /// /// Fetches text runs. /// /// The text source. /// The first text source index. /// Previous line break. Can be null. /// Next line break. Can be null. /// /// The formatted text runs. /// private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak) { nextLineBreak = default; var currentLength = 0; var textRuns = new List(); if (previousLineBreak != null) { for (var index = 0; index < previousLineBreak.RemainingCharacters.Count; index++) { var shapedCharacters = previousLineBreak.RemainingCharacters[index]; if (shapedCharacters == null) { continue; } textRuns.Add(shapedCharacters); if (TryGetLineBreak(shapedCharacters, out var runLineBreak)) { var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); if (++index < previousLineBreak.RemainingCharacters.Count) { for (; index < previousLineBreak.RemainingCharacters.Count; index++) { splitResult.Second.Add(previousLineBreak.RemainingCharacters[index]); } } nextLineBreak = new TextLineBreak(splitResult.Second); return splitResult.First; } currentLength += shapedCharacters.Text.Length; } } firstTextSourceIndex += currentLength; var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); while (textRunEnumerator.MoveNext()) { var textRun = textRunEnumerator.Current; switch (textRun) { case TextCharacters textCharacters: { var shapeableRuns = textCharacters.GetShapeableCharacters(); foreach (var run in shapeableRuns) { var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface, run.Properties.FontRenderingEmSize, run.Properties.CultureInfo); var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties); textRuns.Add(shapedCharacters); } break; } case TextEndOfLine textEndOfLine: nextLineBreak = new TextLineBreak(textEndOfLine); break; } if (TryGetLineBreak(textRun, out var runLineBreak)) { var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); nextLineBreak = new TextLineBreak(splitResult.Second); return splitResult.First; } currentLength += textRun.Text.Length; } return textRuns; } private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak) { lineBreak = default; if (textRun.Text.IsEmpty) { return false; } var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text); while (lineBreakEnumerator.MoveNext()) { if (!lineBreakEnumerator.Current.Required) { continue; } lineBreak = lineBreakEnumerator.Current; if (lineBreak.PositionWrap >= textRun.Text.Length) { return true; } return true; } return false; } /// /// Performs text wrapping returns a list of text lines. /// /// The text run's. /// The text range that is covered by the text runs. /// The paragraph width. /// The text paragraph properties. /// The current line break if the line was explicitly broken. /// The wrapped text line. private static TextLine PerformTextWrapping(List textRuns, TextRange textRange, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak currentLineBreak) { var availableWidth = paragraphWidth; var currentWidth = 0.0; var measuredLength = 0; foreach (var currentRun in textRuns) { if (currentWidth + currentRun.Size.Width > availableWidth) { if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var count)) { measuredLength += count; } break; } currentWidth += currentRun.Size.Width; measuredLength += currentRun.Text.Length; } var currentLength = 0; var lastWrapPosition = 0; var currentPosition = 0; if (measuredLength == 0 && paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow) { measuredLength = 1; } else { for (var index = 0; index < textRuns.Count; index++) { var currentRun = textRuns[index]; var lineBreaker = new LineBreakEnumerator(currentRun.Text); var breakFound = false; while (lineBreaker.MoveNext()) { if (lineBreaker.Current.Required && currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) { breakFound = true; currentPosition = currentLength + lineBreaker.Current.PositionWrap; break; } if ((paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow || lastWrapPosition != 0) && currentLength + lineBreaker.Current.PositionMeasure > measuredLength) { if (lastWrapPosition > 0) { currentPosition = lastWrapPosition; } else { currentPosition = currentLength + measuredLength; } breakFound = true; break; } if (currentLength + lineBreaker.Current.PositionWrap >= measuredLength) { currentPosition = currentLength + lineBreaker.Current.PositionWrap; if (index < textRuns.Count - 1 && lineBreaker.Current.PositionWrap == currentRun.Text.Length) { var nextRun = textRuns[index + 1]; lineBreaker = new LineBreakEnumerator(nextRun.Text); if (lineBreaker.MoveNext() && lineBreaker.Current.PositionMeasure == 0) { currentPosition += lineBreaker.Current.PositionWrap; } } breakFound = true; break; } lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; } if (!breakFound) { currentLength += currentRun.Text.Length; continue; } measuredLength = currentPosition; break; } } var splitResult = SplitTextRuns(textRuns, measuredLength); textRange = new TextRange(textRange.Start, measuredLength); var remainingCharacters = splitResult.Second; var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(remainingCharacters) : null; if (lineBreak is null && currentLineBreak.TextEndOfLine != null) { lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine); } return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, lineBreak); } /// /// Gets the text range that is covered by the text runs. /// /// The text runs. /// The text range that is covered by the text runs. private static TextRange GetTextRange(IReadOnlyList textRuns) { if (textRuns is null || textRuns.Count == 0) { return new TextRange(); } var firstTextRun = textRuns[0]; if (textRuns.Count == 1) { return new TextRange(firstTextRun.Text.Start, firstTextRun.Text.Length); } var start = firstTextRun.Text.Start; var end = textRuns[textRuns.Count - 1].Text.End + 1; return new TextRange(start, end - start); } internal readonly struct SplitTextRunsResult { public SplitTextRunsResult(List first, List second) { First = first; Second = second; } /// /// Gets the first text runs. /// /// /// The first text runs. /// public List First { get; } /// /// Gets the second text runs. /// /// /// The second text runs. /// public List Second { get; } } private struct TextRunEnumerator { private readonly ITextSource _textSource; private int _pos; public TextRunEnumerator(ITextSource textSource, int firstTextSourceIndex) { _textSource = textSource; _pos = firstTextSourceIndex; Current = null; } // ReSharper disable once MemberHidesStaticFromOuterClass public TextRun Current { get; private set; } public bool MoveNext() { Current = _textSource.GetTextRun(_pos); if (Current is null) { return false; } if (Current.TextSourceLength == 0) { return false; } _pos += Current.TextSourceLength; return true; } } } }