// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; using Avalonia.Media.Immutable; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utility; namespace Avalonia.Media.TextFormatting { /// /// Represents a multi line text layout. /// public class TextLayout { private static readonly ReadOnlySlice s_empty = new ReadOnlySlice(new[] { '\u200B' }); private readonly ReadOnlySlice _text; private readonly TextParagraphProperties _paragraphProperties; private readonly IReadOnlyList _textStyleOverrides; /// /// Initializes a new instance of the class. /// /// The text. /// The typeface. /// Size of the font. /// The foreground. /// The text alignment. /// The text wrapping. /// The text trimming. /// The text decorations. /// The maximum width. /// The maximum height. /// The text style overrides. public TextLayout( string text, Typeface typeface, double fontSize, IBrush foreground, TextAlignment textAlignment = TextAlignment.Left, TextWrapping textWrapping = TextWrapping.NoWrap, TextTrimming textTrimming = TextTrimming.None, TextDecorationCollection textDecorations = null, double maxWidth = double.PositiveInfinity, double maxHeight = double.PositiveInfinity, IReadOnlyList textStyleOverrides = null) { _text = string.IsNullOrEmpty(text) ? new ReadOnlySlice() : new ReadOnlySlice(text.AsMemory()); _paragraphProperties = CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, textDecorations?.ToImmutable()); _textStyleOverrides = textStyleOverrides; MaxWidth = maxWidth; MaxHeight = maxHeight; UpdateLayout(); } /// /// Gets the maximum width. /// public double MaxWidth { get; } /// /// Gets the maximum height. /// public double MaxHeight { get; } /// /// Gets the text lines. /// /// /// The text lines. /// public IReadOnlyList TextLines { get; private set; } /// /// Gets the bounds of the layout. /// /// /// The bounds. /// public Rect Bounds { get; private set; } /// /// Draws the text layout. /// /// The drawing context. /// The origin. public void Draw(IDrawingContextImpl context, Point origin) { if (!TextLines.Any()) { return; } var currentY = origin.Y; foreach (var textLine in TextLines) { textLine.Draw(context, new Point(origin.X, currentY)); currentY += textLine.LineMetrics.Size.Height; } } /// /// Creates the default that are used by the . /// /// The typeface. /// The font size. /// The foreground. /// The text alignment. /// The text wrapping. /// The text trimming. /// The text decorations. /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming, ImmutableTextDecoration[] textDecorations) { var textRunStyle = new TextStyle(typeface, fontSize, foreground, textDecorations); return new TextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming); } /// /// Updates the current bounds. /// /// The text line. /// The left. /// The right. /// The bottom. private static void UpdateBounds(TextLine textLine, ref double left, ref double right, ref double bottom) { if (right < textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width) { right = textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width; } if (left < textLine.LineMetrics.BaselineOrigin.X) { left = textLine.LineMetrics.BaselineOrigin.X; } bottom += textLine.LineMetrics.Size.Height; } /// /// Creates an empty text line. /// /// The empty text line. private TextLine CreateEmptyTextLine(int startingIndex) { var textFormat = _paragraphProperties.DefaultTextStyle.TextFormat; var glyphRun = TextShaper.Current.ShapeText(s_empty, textFormat); var textRuns = new[] { new ShapedTextRun(glyphRun, _paragraphProperties.DefaultTextStyle) }; return new SimpleTextLine(new TextPointer(startingIndex, 0), textRuns, TextLineMetrics.Create(textRuns, MaxWidth, _paragraphProperties.TextAlignment)); } /// /// Updates the layout and applies specified text style overrides. /// private void UpdateLayout() { if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon) { var textLine = CreateEmptyTextLine(0); TextLines = new List { textLine }; Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height); } else { var textLines = new List(); double left = 0.0, right = 0.0, bottom = 0.0; var lineBreaker = new LineBreakEnumerator(_text); var currentPosition = 0; while (currentPosition < _text.Length) { int length; if (lineBreaker.MoveNext()) { if (!lineBreaker.Current.Required) { continue; } length = lineBreaker.Current.PositionWrap - currentPosition; if (currentPosition + length < _text.Length) { //The line breaker isn't treating \n\r as a pair so we have to fix that here. if (_text[lineBreaker.Current.PositionMeasure] == '\n' && _text[lineBreaker.Current.PositionWrap] == '\r') { length++; } } } else { length = _text.Length - currentPosition; } var remainingLength = length; while (remainingLength > 0) { var textSlice = _text.AsSlice(currentPosition, remainingLength); var textSource = new FormattedTextSource(textSlice, _paragraphProperties.DefaultTextStyle, _textStyleOverrides); var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties); UpdateBounds(textLine, ref left, ref right, ref bottom); textLines.Add(textLine); if (_paragraphProperties.TextTrimming != TextTrimming.None) { currentPosition += remainingLength; break; } remainingLength -= textLine.Text.Length; currentPosition += textLine.Text.Length; } if (lineBreaker.Current.Required && currentPosition == _text.Length) { var emptyTextLine = CreateEmptyTextLine(currentPosition); UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); textLines.Add(emptyTextLine); break; } if (!double.IsPositiveInfinity(MaxHeight) && MaxHeight < Bounds.Height) { break; } } Bounds = new Rect(left, 0, right, bottom); TextLines = textLines; } } private struct FormattedTextSource : ITextSource { private readonly ReadOnlySlice _text; private readonly TextStyle _defaultStyle; private readonly IReadOnlyList _textStyleOverrides; public FormattedTextSource(ReadOnlySlice text, TextStyle defaultStyle, IReadOnlyList textStyleOverrides) { _text = text; _defaultStyle = defaultStyle; _textStyleOverrides = textStyleOverrides; } public TextRun GetTextRun(int textSourceIndex) { var runText = _text.Skip(textSourceIndex); if (runText.IsEmpty) { return new TextEndOfLine(); } var textStyleRun = CreateTextStyleRunWithOverride(runText, _defaultStyle, _textStyleOverrides); return new TextCharacters(runText.Take(textStyleRun.TextPointer.Length), textStyleRun.Style); } /// /// Creates a text style run that has overrides applied. Only overrides with equal TextStyle. /// If optimizeForShaping is true Foreground is ignored. /// /// The text to create the run for. /// The default text style for segments that don't have an override. /// The text style overrides. /// /// The created text style run. /// private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice text, TextStyle defaultTextStyle, IReadOnlyList textStyleOverrides) { if(textStyleOverrides == null || textStyleOverrides.Count == 0) { return new TextStyleRun(new TextPointer(text.Start, text.Length), defaultTextStyle); } var currentTextStyle = defaultTextStyle; var hasOverride = false; var i = 0; var length = 0; for (; i < textStyleOverrides.Count; i++) { var styleOverride = textStyleOverrides[i]; var textPointer = styleOverride.TextPointer; if (textPointer.End < text.Start) { continue; } if (textPointer.Start > text.End) { length = text.Length; break; } if (textPointer.Start > text.Start) { if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat || !currentTextStyle.Foreground.Equals(styleOverride.Style.Foreground)) { length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length); break; } } length += Math.Min(text.Length - length, textPointer.Length); if (hasOverride) { continue; } hasOverride = true; currentTextStyle = styleOverride.Style; } if (length < text.Length && i == textStyleOverrides.Count) { if (currentTextStyle.Foreground.Equals(defaultTextStyle.Foreground) && currentTextStyle.TextFormat == defaultTextStyle.TextFormat) { length = text.Length; } } if (length != text.Length) { text = text.Take(length); } return new TextStyleRun(new TextPointer(text.Start, length), currentTextStyle); } } } }