|
|
@@ -1,11 +1,9 @@
|
|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
using System.Linq;
|
|
|
-using Avalonia.Media.Immutable;
|
|
|
using Avalonia.Media.TextFormatting.Unicode;
|
|
|
-using Avalonia.Platform;
|
|
|
using Avalonia.Utilities;
|
|
|
-using Avalonia.Utility;
|
|
|
+using Avalonia.Platform;
|
|
|
|
|
|
namespace Avalonia.Media.TextFormatting
|
|
|
{
|
|
|
@@ -14,11 +12,11 @@ namespace Avalonia.Media.TextFormatting
|
|
|
/// </summary>
|
|
|
public class TextLayout
|
|
|
{
|
|
|
- private static readonly ReadOnlySlice<char> s_empty = new ReadOnlySlice<char>(new[] { '\u200B' });
|
|
|
+ private static readonly char[] s_empty = { '\u200B' };
|
|
|
|
|
|
private readonly ReadOnlySlice<char> _text;
|
|
|
private readonly TextParagraphProperties _paragraphProperties;
|
|
|
- private readonly IReadOnlyList<TextStyleRun> _textStyleOverrides;
|
|
|
+ private readonly IReadOnlyList<ValueSpan<TextRunProperties>> _textStyleOverrides;
|
|
|
|
|
|
/// <summary>
|
|
|
/// Initializes a new instance of the <see cref="TextLayout" /> class.
|
|
|
@@ -33,6 +31,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
/// <param name="textDecorations">The text decorations.</param>
|
|
|
/// <param name="maxWidth">The maximum width.</param>
|
|
|
/// <param name="maxHeight">The maximum height.</param>
|
|
|
+ /// <param name="lineHeight">The height of each line of text.</param>
|
|
|
/// <param name="maxLines">The maximum number of text lines.</param>
|
|
|
/// <param name="textStyleOverrides">The text style overrides.</param>
|
|
|
public TextLayout(
|
|
|
@@ -46,18 +45,22 @@ namespace Avalonia.Media.TextFormatting
|
|
|
TextDecorationCollection textDecorations = null,
|
|
|
double maxWidth = double.PositiveInfinity,
|
|
|
double maxHeight = double.PositiveInfinity,
|
|
|
+ double lineHeight = double.NaN,
|
|
|
int maxLines = 0,
|
|
|
- IReadOnlyList<TextStyleRun> textStyleOverrides = null)
|
|
|
+ IReadOnlyList<ValueSpan<TextRunProperties>> textStyleOverrides = null)
|
|
|
{
|
|
|
_text = string.IsNullOrEmpty(text) ?
|
|
|
new ReadOnlySlice<char>() :
|
|
|
new ReadOnlySlice<char>(text.AsMemory());
|
|
|
|
|
|
_paragraphProperties =
|
|
|
- CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, textDecorations?.ToImmutable());
|
|
|
+ CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming,
|
|
|
+ textDecorations, lineHeight);
|
|
|
|
|
|
_textStyleOverrides = textStyleOverrides;
|
|
|
|
|
|
+ LineHeight = lineHeight;
|
|
|
+
|
|
|
MaxWidth = maxWidth;
|
|
|
|
|
|
MaxHeight = maxHeight;
|
|
|
@@ -67,22 +70,29 @@ namespace Avalonia.Media.TextFormatting
|
|
|
UpdateLayout();
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Gets or sets the height of each line of text.
|
|
|
+ /// </summary>
|
|
|
+ /// <remarks>
|
|
|
+ /// 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.
|
|
|
+ /// </remarks>
|
|
|
+ public double LineHeight { get; }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// Gets the maximum width.
|
|
|
/// </summary>
|
|
|
public double MaxWidth { get; }
|
|
|
|
|
|
-
|
|
|
/// <summary>
|
|
|
/// Gets the maximum height.
|
|
|
/// </summary>
|
|
|
public double MaxHeight { get; }
|
|
|
|
|
|
-
|
|
|
/// <summary>
|
|
|
/// Gets the maximum number of text lines.
|
|
|
/// </summary>
|
|
|
- public double MaxLines { get; }
|
|
|
+ public int MaxLines { get; }
|
|
|
|
|
|
/// <summary>
|
|
|
/// Gets the text lines.
|
|
|
@@ -105,7 +115,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
/// </summary>
|
|
|
/// <param name="context">The drawing context.</param>
|
|
|
/// <param name="origin">The origin.</param>
|
|
|
- public void Draw(IDrawingContextImpl context, Point origin)
|
|
|
+ public void Draw(DrawingContext context, Point origin)
|
|
|
{
|
|
|
if (!TextLines.Any())
|
|
|
{
|
|
|
@@ -132,14 +142,16 @@ namespace Avalonia.Media.TextFormatting
|
|
|
/// <param name="textWrapping">The text wrapping.</param>
|
|
|
/// <param name="textTrimming">The text trimming.</param>
|
|
|
/// <param name="textDecorations">The text decorations.</param>
|
|
|
+ /// <param name="lineHeight">The height of each line of text.</param>
|
|
|
/// <returns></returns>
|
|
|
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
|
|
|
IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming,
|
|
|
- ImmutableTextDecoration[] textDecorations)
|
|
|
+ TextDecorationCollection textDecorations, double lineHeight)
|
|
|
{
|
|
|
- var textRunStyle = new TextStyle(typeface, fontSize, foreground, textDecorations);
|
|
|
+ var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
|
|
|
|
|
|
- return new TextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming);
|
|
|
+ return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming,
|
|
|
+ lineHeight);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -170,14 +182,15 @@ namespace Avalonia.Media.TextFormatting
|
|
|
/// <returns>The empty text line.</returns>
|
|
|
private TextLine CreateEmptyTextLine(int startingIndex)
|
|
|
{
|
|
|
- var textFormat = _paragraphProperties.DefaultTextStyle.TextFormat;
|
|
|
+ var properties = _paragraphProperties.DefaultTextRunProperties;
|
|
|
|
|
|
- var glyphRun = TextShaper.Current.ShapeText(s_empty, textFormat);
|
|
|
+ var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice<char>(s_empty, startingIndex, 1),
|
|
|
+ properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo);
|
|
|
|
|
|
- var textRuns = new[] { new ShapedTextRun(glyphRun, _paragraphProperties.DefaultTextStyle) };
|
|
|
+ var textRuns = new[] { new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) };
|
|
|
|
|
|
- return new SimpleTextLine(new TextPointer(startingIndex, 0), textRuns,
|
|
|
- TextLineMetrics.Create(textRuns, MaxWidth, _paragraphProperties.TextAlignment));
|
|
|
+ return new TextLineImpl(textRuns,
|
|
|
+ TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties));
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -199,77 +212,38 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
double left = 0.0, right = 0.0, bottom = 0.0;
|
|
|
|
|
|
- var lineBreaker = new LineBreakEnumerator(_text);
|
|
|
-
|
|
|
var currentPosition = 0;
|
|
|
|
|
|
+ var textSource = new FormattedTextSource(_text,
|
|
|
+ _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
|
|
|
+
|
|
|
+ TextLineBreak previousLineBreak = null;
|
|
|
+
|
|
|
while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines))
|
|
|
{
|
|
|
- int length;
|
|
|
+ var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
|
|
|
+ _paragraphProperties, previousLineBreak);
|
|
|
|
|
|
- if (lineBreaker.MoveNext())
|
|
|
- {
|
|
|
- if (!lineBreaker.Current.Required)
|
|
|
- {
|
|
|
- continue;
|
|
|
- }
|
|
|
+ previousLineBreak = textLine.LineBreak;
|
|
|
|
|
|
- length = lineBreaker.Current.PositionWrap - currentPosition;
|
|
|
+ textLines.Add(textLine);
|
|
|
|
|
|
- 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
|
|
|
+ UpdateBounds(textLine, ref left, ref right, ref bottom);
|
|
|
+
|
|
|
+ if (!double.IsPositiveInfinity(MaxHeight) && bottom > MaxHeight)
|
|
|
{
|
|
|
- length = _text.Length - currentPosition;
|
|
|
+ break;
|
|
|
}
|
|
|
|
|
|
- var remainingLength = length;
|
|
|
+ currentPosition += textLine.TextRange.Length;
|
|
|
|
|
|
- while (remainingLength > 0 && (MaxLines == 0 || textLines.Count < MaxLines))
|
|
|
+ if (currentPosition != _text.Length || textLine.LineBreak == null)
|
|
|
{
|
|
|
- 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 (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight)
|
|
|
- {
|
|
|
- currentPosition = _text.Length;
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- if (_paragraphProperties.TextTrimming != TextTrimming.None)
|
|
|
- {
|
|
|
- currentPosition += remainingLength;
|
|
|
-
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- remainingLength -= textLine.Text.Length;
|
|
|
-
|
|
|
- currentPosition += textLine.Text.Length;
|
|
|
+ continue;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- if (lineBreaker.Current.Required && currentPosition == _text.Length)
|
|
|
- {
|
|
|
var emptyTextLine = CreateEmptyTextLine(currentPosition);
|
|
|
|
|
|
- UpdateBounds(emptyTextLine, ref left, ref right, ref bottom);
|
|
|
-
|
|
|
textLines.Add(emptyTextLine);
|
|
|
}
|
|
|
|
|
|
@@ -279,22 +253,27 @@ namespace Avalonia.Media.TextFormatting
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private struct FormattedTextSource : ITextSource
|
|
|
+ private readonly struct FormattedTextSource : ITextSource
|
|
|
{
|
|
|
private readonly ReadOnlySlice<char> _text;
|
|
|
- private readonly TextStyle _defaultStyle;
|
|
|
- private readonly IReadOnlyList<TextStyleRun> _textStyleOverrides;
|
|
|
+ private readonly TextRunProperties _defaultProperties;
|
|
|
+ private readonly IReadOnlyList<ValueSpan<TextRunProperties>> _textModifier;
|
|
|
|
|
|
- public FormattedTextSource(ReadOnlySlice<char> text, TextStyle defaultStyle,
|
|
|
- IReadOnlyList<TextStyleRun> textStyleOverrides)
|
|
|
+ public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
|
|
|
+ IReadOnlyList<ValueSpan<TextRunProperties>> textModifier)
|
|
|
{
|
|
|
_text = text;
|
|
|
- _defaultStyle = defaultStyle;
|
|
|
- _textStyleOverrides = textStyleOverrides;
|
|
|
+ _defaultProperties = defaultProperties;
|
|
|
+ _textModifier = textModifier;
|
|
|
}
|
|
|
|
|
|
public TextRun GetTextRun(int textSourceIndex)
|
|
|
{
|
|
|
+ if (textSourceIndex > _text.End)
|
|
|
+ {
|
|
|
+ return new TextEndOfLine();
|
|
|
+ }
|
|
|
+
|
|
|
var runText = _text.Skip(textSourceIndex);
|
|
|
|
|
|
if (runText.IsEmpty)
|
|
|
@@ -302,30 +281,29 @@ namespace Avalonia.Media.TextFormatting
|
|
|
return new TextEndOfLine();
|
|
|
}
|
|
|
|
|
|
- var textStyleRun = CreateTextStyleRunWithOverride(runText, _defaultStyle, _textStyleOverrides);
|
|
|
+ var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
|
|
|
|
|
|
- return new TextCharacters(runText.Take(textStyleRun.TextPointer.Length), textStyleRun.Style);
|
|
|
+ return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Creates a text style run that has overrides applied. Only overrides with equal TextStyle.
|
|
|
- /// If optimizeForShaping is <c>true</c> Foreground is ignored.
|
|
|
+ /// Creates a span of text run properties that has modifier applied.
|
|
|
/// </summary>
|
|
|
- /// <param name="text">The text to create the run for.</param>
|
|
|
- /// <param name="defaultTextStyle">The default text style for segments that don't have an override.</param>
|
|
|
- /// <param name="textStyleOverrides">The text style overrides.</param>
|
|
|
+ /// <param name="text">The text to create the properties for.</param>
|
|
|
+ /// <param name="defaultProperties">The default text properties.</param>
|
|
|
+ /// <param name="textModifier">The text properties modifier.</param>
|
|
|
/// <returns>
|
|
|
/// The created text style run.
|
|
|
/// </returns>
|
|
|
- private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice<char> text,
|
|
|
- TextStyle defaultTextStyle, IReadOnlyList<TextStyleRun> textStyleOverrides)
|
|
|
+ private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text,
|
|
|
+ TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>> textModifier)
|
|
|
{
|
|
|
- if(textStyleOverrides == null || textStyleOverrides.Count == 0)
|
|
|
+ if (textModifier == null || textModifier.Count == 0)
|
|
|
{
|
|
|
- return new TextStyleRun(new TextPointer(text.Start, text.Length), defaultTextStyle);
|
|
|
+ return new ValueSpan<TextRunProperties>(text.Start, text.Length, defaultProperties);
|
|
|
}
|
|
|
|
|
|
- var currentTextStyle = defaultTextStyle;
|
|
|
+ var currentProperties = defaultProperties;
|
|
|
|
|
|
var hasOverride = false;
|
|
|
|
|
|
@@ -333,35 +311,34 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
var length = 0;
|
|
|
|
|
|
- for (; i < textStyleOverrides.Count; i++)
|
|
|
+ for (; i < textModifier.Count; i++)
|
|
|
{
|
|
|
- var styleOverride = textStyleOverrides[i];
|
|
|
+ var propertiesOverride = textModifier[i];
|
|
|
|
|
|
- var textPointer = styleOverride.TextPointer;
|
|
|
+ var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
|
|
|
|
|
|
- if (textPointer.End < text.Start)
|
|
|
+ if (textRange.End < text.Start)
|
|
|
{
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- if (textPointer.Start > text.End)
|
|
|
+ if (textRange.Start > text.End)
|
|
|
{
|
|
|
length = text.Length;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- if (textPointer.Start > text.Start)
|
|
|
+ if (textRange.Start > text.Start)
|
|
|
{
|
|
|
- if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat ||
|
|
|
- !currentTextStyle.Foreground.Equals(styleOverride.Style.Foreground))
|
|
|
+ if (propertiesOverride.Value != currentProperties)
|
|
|
{
|
|
|
- length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length);
|
|
|
+ length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
|
|
|
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- length += Math.Min(text.Length - length, textPointer.Length);
|
|
|
+ length += Math.Min(text.Length - length, textRange.Length);
|
|
|
|
|
|
if (hasOverride)
|
|
|
{
|
|
|
@@ -370,13 +347,12 @@ namespace Avalonia.Media.TextFormatting
|
|
|
|
|
|
hasOverride = true;
|
|
|
|
|
|
- currentTextStyle = styleOverride.Style;
|
|
|
+ currentProperties = propertiesOverride.Value;
|
|
|
}
|
|
|
|
|
|
- if (length < text.Length && i == textStyleOverrides.Count)
|
|
|
+ if (length < text.Length && i == textModifier.Count)
|
|
|
{
|
|
|
- if (currentTextStyle.Foreground.Equals(defaultTextStyle.Foreground) &&
|
|
|
- currentTextStyle.TextFormat == defaultTextStyle.TextFormat)
|
|
|
+ if (currentProperties == defaultProperties)
|
|
|
{
|
|
|
length = text.Length;
|
|
|
}
|
|
|
@@ -387,7 +363,7 @@ namespace Avalonia.Media.TextFormatting
|
|
|
text = text.Take(length);
|
|
|
}
|
|
|
|
|
|
- return new TextStyleRun(new TextPointer(text.Start, length), currentTextStyle);
|
|
|
+ return new ValueSpan<TextRunProperties>(text.Start, length, currentProperties);
|
|
|
}
|
|
|
}
|
|
|
}
|