using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
///
/// Represents a multi line text layout.
///
public class TextLayout
{
private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties;
private readonly TextTrimming _textTrimming;
private int _textSourceLength;
///
/// 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 text flow direction.
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
/// The maximum number of text lines.
/// The text style overrides.
public TextLayout(
string? text,
Typeface typeface,
double fontSize,
IBrush? foreground,
TextAlignment textAlignment = TextAlignment.Left,
TextWrapping textWrapping = TextWrapping.NoWrap,
TextTrimming? textTrimming = null,
TextDecorationCollection? textDecorations = null,
FlowDirection flowDirection = FlowDirection.LeftToRight,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
int maxLines = 0,
IReadOnlyList>? textStyleOverrides = null)
{
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
_textTrimming = textTrimming ?? TextTrimming.None;
LineHeight = lineHeight;
MaxWidth = maxWidth;
MaxHeight = maxHeight;
MaxLines = maxLines;
TextLines = CreateTextLines();
}
///
/// Initializes a new instance of the class.
///
/// The text source.
/// The default text paragraph properties.
/// The text trimming.
/// The maximum width.
/// The maximum height.
/// The height of each line of text.
/// The maximum number of text lines.
public TextLayout(
ITextSource textSource,
TextParagraphProperties paragraphProperties,
TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
int maxLines = 0)
{
_textSource = textSource;
_paragraphProperties = paragraphProperties;
_textTrimming = textTrimming ?? TextTrimming.None;
LineHeight = lineHeight;
MaxWidth = maxWidth;
MaxHeight = maxHeight;
MaxLines = maxLines;
TextLines = CreateTextLines();
}
///
/// Gets or sets the height of each line of text.
///
///
/// A value of NaN (equivalent to an attribute value of "Auto") indicates that the line height
/// is determined automatically from the current font characteristics. The default is NaN.
///
public double LineHeight { get; }
///
/// Gets the maximum width.
///
public double MaxWidth { get; }
///
/// Gets the maximum height.
///
public double MaxHeight { get; }
///
/// Gets the maximum number of text lines.
///
public int MaxLines { 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(DrawingContext context, Point origin)
{
if (!TextLines.Any())
{
return;
}
var (currentX, currentY) = origin;
foreach (var textLine in TextLines)
{
textLine.Draw(context, new Point(currentX + textLine.Start, currentY));
currentY += textLine.Height;
}
}
///
/// Get the pixel location relative to the top-left of the layout box given the text position.
///
/// The text position.
///
public Rect HitTestTextPosition(int textPosition)
{
if (TextLines.Count == 0)
{
return new Rect();
}
if (textPosition < 0)
{
textPosition = _textSourceLength;
}
var currentY = 0.0;
foreach (var textLine in TextLines)
{
var end = textLine.FirstTextSourceIndex + textLine.Length;
if (end <= textPosition && end < _textSourceLength)
{
currentY += textLine.Height;
continue;
}
var characterHit = new CharacterHit(textPosition);
var startX = textLine.GetDistanceFromCharacterHit(characterHit);
var nextCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
var endX = textLine.GetDistanceFromCharacterHit(nextCharacterHit);
return new Rect(startX, currentY, endX - startX, textLine.Height);
}
return new Rect();
}
public IEnumerable HitTestTextRange(int start, int length)
{
if (start + length <= 0)
{
return Array.Empty();
}
var result = new List(TextLines.Count);
var currentY = 0d;
foreach (var textLine in TextLines)
{
//Current line isn't covered.
if (textLine.FirstTextSourceIndex + textLine.Length < start)
{
currentY += textLine.Height;
continue;
}
var textBounds = textLine.GetTextBounds(start, length);
if (textBounds.Count > 0)
{
foreach (var bounds in textBounds)
{
Rect? last = result.Count > 0 ? result[result.Count - 1] : null;
if (last.HasValue && MathUtilities.AreClose(last.Value.Right, bounds.Rectangle.Left) && MathUtilities.AreClose(last.Value.Top, currentY))
{
result[result.Count - 1] = last.Value.WithWidth(last.Value.Width + bounds.Rectangle.Width);
}
else
{
result.Add(bounds.Rectangle.WithY(currentY));
}
foreach (var runBounds in bounds.TextRunBounds)
{
start += runBounds.Length;
length -= runBounds.Length;
}
}
}
if (textLine.FirstTextSourceIndex + textLine.Length >= start + length)
{
break;
}
currentY += textLine.Height;
}
return result;
}
public TextHitTestResult HitTestPoint(in Point point)
{
var currentY = 0d;
var lineIndex = 0;
TextLine? currentLine = null;
CharacterHit characterHit;
for (; lineIndex < TextLines.Count; lineIndex++)
{
currentLine = TextLines[lineIndex];
if (currentY + currentLine.Height > point.Y)
{
characterHit = currentLine.GetCharacterHitFromDistance(point.X);
return GetHitTestResult(currentLine, characterHit, point);
}
currentY += currentLine.Height;
}
if (currentLine is null)
{
return new TextHitTestResult();
}
characterHit = currentLine.GetCharacterHitFromDistance(point.X);
return GetHitTestResult(currentLine, characterHit, point);
}
public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge)
{
if (charIndex < 0)
{
return 0;
}
if (charIndex > _textSourceLength)
{
return TextLines.Count - 1;
}
for (var index = 0; index < TextLines.Count; index++)
{
var textLine = TextLines[index];
if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
{
continue;
}
if (charIndex >= textLine.FirstTextSourceIndex &&
charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
{
return index;
}
}
return TextLines.Count - 1;
}
private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
{
var (x, y) = point;
var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
{
lastTrailingIndex -= textLine.NewLineLength;
}
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Bounds.Bottom;
if (textPosition == textLine.FirstTextSourceIndex + textLine.Length)
{
textPosition -= textLine.NewLineLength;
}
if (textLine.NewLineLength > 0 && textPosition + textLine.NewLineLength ==
characterHit.FirstCharacterIndex + characterHit.TrailingLength)
{
characterHit = new CharacterHit(characterHit.FirstCharacterIndex);
}
return new TextHitTestResult(characterHit, textPosition, isInside, isTrailing);
}
///
/// Creates the default that are used by the .
///
/// The typeface.
/// The font size.
/// The foreground.
/// The text alignment.
/// The text wrapping.
/// The text decorations.
/// The text flow direction.
/// The height of each line of text.
///
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight)
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
textRunStyle, textWrapping, lineHeight, 0);
}
///
/// Updates the current bounds.
///
/// The text line.
/// The current left.
/// The current width.
/// The current height.
private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height)
{
var lineWidth = textLine.WidthIncludingTrailingWhitespace;
if (width < lineWidth)
{
width = lineWidth;
}
if (left > textLine.Start)
{
left = textLine.Start;
}
height += textLine.Height;
}
private IReadOnlyList CreateTextLines()
{
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0, 0, 0, textLine.Height);
return new List { textLine };
}
var textLines = new List();
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
_textSourceLength = 0;
TextLine? previousLine = null;
while (true)
{
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
if (previousLine != null && previousLine.NewLineLength > 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref left, ref width, ref height);
}
break;
}
_textSourceLength += textLine.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
{
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
textLines[textLines.Count - 1] = collapsedLine;
}
break;
}
var hasOverflowed = textLine.HasOverflowed;
if (hasOverflowed && _textTrimming != TextTrimming.None)
{
textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
}
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
previousLine = textLine;
//Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines)
{
if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null)
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
}
break;
}
}
//Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
}
Bounds = new Rect(left, 0, width, height);
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
foreach (var line in textLines)
{
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
}
var justificationWidth = width - whitespaceWidth;
if (justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);
for (var i = 0; i < textLines.Count - 1; i++)
{
var line = textLines[i];
line.Justify(justificationProperties);
}
}
}
return textLines;
}
///
/// Gets the for current text trimming mode.
///
/// The collapsing width.
/// The .
private TextCollapsingProperties GetCollapsingProperties(double width)
{
return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
}
}
}