using System; using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utility; namespace Avalonia.Media.TextFormatting { internal class SimpleTextFormatter : TextFormatter { private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); /// /// Formats a text line. /// /// The text source. /// The first character index to start the text line from. /// A value that specifies the width of the paragraph that the line fills. /// A value that represents paragraph properties, /// such as TextWrapping, TextAlignment, or TextStyle. /// The formatted line. public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) { var textTrimming = paragraphProperties.TextTrimming; var textWrapping = paragraphProperties.TextWrapping; TextLine textLine; var textRuns = FormatTextRuns(textSource, firstTextSourceIndex, out var textPointer); if (textTrimming != TextTrimming.None) { textLine = PerformTextTrimming(textPointer, textRuns, paragraphWidth, paragraphProperties); } else { if (textWrapping == TextWrapping.Wrap) { textLine = PerformTextWrapping(textPointer, textRuns, paragraphWidth, paragraphProperties); } else { var textLineMetrics = TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment); textLine = new SimpleTextLine(textPointer, textRuns, textLineMetrics); } } return textLine; } /// /// Formats text runs with optional text style overrides. /// /// The text source. /// The first text source index. /// The text pointer that covers the formatted text runs. /// /// The formatted text runs. /// private List FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer) { var start = firstTextSourceIndex; var textRuns = new List(); while (true) { var textRun = textSource.GetTextRun(firstTextSourceIndex); if (textRun.Text.IsEmpty) { break; } if (textRun is TextEndOfLine) { break; } if (!(textRun is TextCharacters)) { throw new NotSupportedException("Run type not supported by the formatter."); } var runText = textRun.Text; while (!runText.IsEmpty) { var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style); var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length), shapableTextStyleRun.Style); textRuns.Add(shapedRun); runText = runText.Skip(shapedRun.Text.Length); } firstTextSourceIndex += textRun.Text.Length; } textPointer = new TextPointer(start, firstTextSourceIndex - start); return textRuns; } /// /// Performs text trimming and returns a trimmed line. /// /// A value that specifies the width of the paragraph that the line fills. /// A value that represents paragraph properties, /// such as TextWrapping, TextAlignment, or TextStyle. /// The text runs to perform the trimming on. /// The text that was used to construct the text runs. /// private TextLine PerformTextTrimming(TextPointer text, IReadOnlyList textRuns, double paragraphWidth, TextParagraphProperties paragraphProperties) { var textTrimming = paragraphProperties.TextTrimming; var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; currentWidth += currentRun.GlyphRun.Bounds.Width; if (currentWidth > availableWidth) { var ellipsisRun = CreateEllipsisRun(currentRun.Style); var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); if (textTrimming == TextTrimming.WordEllipsis) { if (measuredLength < text.End) { var currentBreakPosition = 0; var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionWrap; if (nextBreakPosition == 0) { break; } if (nextBreakPosition > measuredLength) { break; } currentBreakPosition = nextBreakPosition; } measuredLength = currentBreakPosition; } } var splitResult = SplitTextRuns(textRuns, measuredLength); var trimmedRuns = new List(splitResult.First.Count + 1); trimmedRuns.AddRange(splitResult.First); trimmedRuns.Add(ellipsisRun); var textLineMetrics = TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment); return new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics); } availableWidth -= currentRun.GlyphRun.Bounds.Width; runIndex++; } return new SimpleTextLine(text, textRuns, TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); } /// /// Performs text wrapping returns a list of text lines. /// /// The text paragraph properties. /// The text run'S. /// The text to analyze for break opportunities. /// /// private TextLine PerformTextWrapping(TextPointer text, IReadOnlyList textRuns, double paragraphWidth, TextParagraphProperties paragraphProperties) { var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; while (runIndex < textRuns.Count) { var currentRun = textRuns[runIndex]; currentWidth += currentRun.GlyphRun.Bounds.Width; if (currentWidth > availableWidth) { var measuredLength = MeasureText(currentRun, paragraphWidth); if (measuredLength < text.End) { var currentBreakPosition = -1; var lineBreaker = new LineBreakEnumerator(currentRun.Text); while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { var nextBreakPosition = lineBreaker.Current.PositionWrap; if (nextBreakPosition == 0) { break; } if (nextBreakPosition > measuredLength) { break; } currentBreakPosition = nextBreakPosition; } if (currentBreakPosition != -1) { measuredLength = currentBreakPosition; } } var splitResult = SplitTextRuns(textRuns, measuredLength); var textLineMetrics = TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment); return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics); } availableWidth -= currentRun.GlyphRun.Bounds.Width; runIndex++; } return new SimpleTextLine(text, textRuns, TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); } /// /// Measures the number of characters that fits into available width. /// /// The text run. /// The available width. /// private int MeasureText(ShapedTextRun textRun, double availableWidth) { if (textRun.GlyphRun.Bounds.Width < availableWidth) { return textRun.Text.Length; } var measuredWidth = 0.0; var index = 0; for (; index < textRun.GlyphRun.GlyphAdvances.Length; index++) { var advance = textRun.GlyphRun.GlyphAdvances[index]; if (measuredWidth + advance > availableWidth) { break; } measuredWidth += advance; } var cluster = textRun.GlyphRun.GlyphClusters[index]; var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _); return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start + (textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0); } /// /// Creates an ellipsis. /// /// The text style. /// private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle) { var formatterImpl = AvaloniaLocator.Current.GetService(); var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat); return new ShapedTextRun(glyphRun, textStyle); } private readonly struct SplitTextRunsResult { public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second) { First = first; Second = second; } /// /// Gets the first text runs. /// /// /// The first text runs. /// public IReadOnlyList First { get; } /// /// Gets the second text runs. /// /// /// The second text runs. /// public IReadOnlyList Second { get; } } /// /// Split a sequence of runs into two segments at specified length. /// /// The text run's. /// The length to split at. /// private static SplitTextRunsResult SplitTextRuns(IReadOnlyList 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 ShapedTextRun[firstCount]; if (firstCount > 1) { for (var j = 0; j < i; j++) { first[j] = textRuns[j]; } } var secondCount = textRuns.Count - firstCount; if (currentLength + currentRun.GlyphRun.Characters.Length == length) { var second = new ShapedTextRun[secondCount]; var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; if (secondCount > 0) { for (var j = 0; j < secondCount; j++) { second[j] = textRuns[i + j + offset]; } } first[i] = currentRun; return new SplitTextRunsResult(first, second); } else { secondCount++; var second = new ShapedTextRun[secondCount]; if (secondCount > 0) { for (var j = 1; j < secondCount; j++) { second[j] = textRuns[i + j]; } } var split = currentRun.Split(length - currentLength); first[i] = split.First; second[0] = split.Second; return new SplitTextRunsResult(first, second); } } return new SplitTextRunsResult(textRuns, null); } } }