// 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);
}
}
}
}