Browse Source

Removed some temporary List<T> from text layout

pull/10013/head
Julien Lebosquain 4 years ago
parent
commit
658fd804af
  1. 11
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  2. 9
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  3. 6
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  4. 62
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  5. 53
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  6. 51
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  7. 50
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  8. 56
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  9. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  10. 8
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

11
src/Avalonia.Base/Media/TextFormatting/SplitResult.cs

@ -26,5 +26,16 @@
/// The second part. /// The second part.
/// </value> /// </value>
public T? Second { get; } public T? Second { get; }
/// <summary>
/// Deconstructs the split results into its components.
/// </summary>
/// <param name="first">On return, contains the first part.</param>
/// <param name="second">On return, contains the second part.</param>
public void Deconstruct(out T first, out T? second)
{
first = First;
second = Second;
}
} }
} }

9
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="UnshapedTextRun"/>. /// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary> /// </summary>
/// <returns>The shapeable text characters.</returns> /// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel, internal void GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
ref TextRunProperties? previousProperties) ref TextRunProperties? previousProperties, List<TextRun> results)
{ {
var shapeableCharacters = new List<UnshapedTextRun>(2);
var properties = Properties; var properties = Properties;
while (!text.IsEmpty) while (!text.IsEmpty)
{ {
var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun); results.Add(shapeableRun);
text = text.Slice(shapeableRun.Length); text = text.Slice(shapeableRun.Length);
previousProperties = shapeableRun.Properties; previousProperties = shapeableRun.Properties;
} }
return shapeableCharacters;
} }
/// <summary> /// <summary>

6
src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs

@ -1,6 +1,4 @@
using System.Collections.Generic; namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{ {
/// <summary> /// <summary>
/// Properties of text collapsing. /// Properties of text collapsing.
@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line. /// Collapses given text line.
/// </summary> /// </summary>
/// <param name="textLine">Text line to collapse.</param> /// <param name="textLine">Text line to collapse.</param>
public abstract List<TextRun>? Collapse(TextLine textLine); public abstract TextRun[]? Collapse(TextLine textLine);
} }
} }

62
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -1,15 +1,18 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
internal static class TextEllipsisHelper internal static class TextEllipsisHelper
{ {
public static List<TextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{ {
var textRuns = textLine.TextRuns; var textRuns = textLine.TextRuns;
if (textRuns == null || textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return null; return null;
} }
@ -22,7 +25,7 @@ namespace Avalonia.Media.TextFormatting
if (properties.Width < shapedSymbol.GlyphRun.Size.Width) if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{ {
//Not enough space to fit in the symbol //Not enough space to fit in the symbol
return new List<TextRun>(0); return Array.Empty<TextRun>();
} }
var availableWidth = properties.Width - shapedSymbol.Size.Width; var availableWidth = properties.Width - shapedSymbol.Size.Width;
@ -70,18 +73,7 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength; collapsedLength += measuredLength;
var collapsedRuns = new List<TextRun>(textRuns.Count); return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
} }
availableWidth -= shapedRun.Size.Width; availableWidth -= shapedRun.Size.Width;
@ -94,18 +86,7 @@ namespace Avalonia.Media.TextFormatting
//The whole run needs to fit into available space //The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth) if (currentWidth + drawableRun.Size.Width > availableWidth)
{ {
var collapsedRuns = new List<TextRun>(textRuns.Count); return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
} }
availableWidth -= drawableRun.Size.Width; availableWidth -= drawableRun.Size.Width;
@ -121,5 +102,30 @@ namespace Avalonia.Media.TextFormatting
return null; return null;
} }
private static TextRun[] CreateCollapsedRuns(IReadOnlyList<TextRun> textRuns, int collapsedLength,
TextRun shapedSymbol)
{
if (collapsedLength <= 0)
{
return new[] { shapedSymbol };
}
// perf note: the runs are very likely to come from TextLineImpl
// which already uses an array: ToArray() won't ever be called in this case
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
var (preSplitRuns, _) = TextFormatterImpl.SplitTextRuns(textRunArray, collapsedLength);
var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
for (var i = 0; i < preSplitRuns.Count; ++i)
{
collapsedRuns[i] = preSplitRuns[i];
}
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
}
} }
} }

53
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -23,10 +24,10 @@ namespace Avalonia.Media.TextFormatting
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex,
out var textEndOfLine, out var textSourceLength); out var textEndOfLine, out var textSourceLength);
if (previousLineBreak?.RemainingRuns != null) if (previousLineBreak?.RemainingRuns is { } remainingRuns)
{ {
resolvedFlowDirection = previousLineBreak.FlowDirection; resolvedFlowDirection = previousLineBreak.FlowDirection;
textRuns = previousLineBreak.RemainingRuns; textRuns = remainingRuns;
nextLineBreak = previousLineBreak; nextLineBreak = previousLineBreak;
} }
else else
@ -45,7 +46,7 @@ namespace Avalonia.Media.TextFormatting
{ {
case TextWrapping.NoWrap: case TextWrapping.NoWrap:
{ {
textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, textLine = new TextLineImpl(textRuns.ToArray(), firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine(); textLine.FinalizeLine();
@ -160,6 +161,14 @@ namespace Avalonia.Media.TextFormatting
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var shapedRuns = new List<TextRun>(); var shapedRuns = new List<TextRun>();
if (textRuns.Count == 0)
{
resolvedFlowDirection = flowDirection;
return shapedRuns;
}
using var biDiData = new BidiData((sbyte)flowDirection); using var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns) foreach (var textRun in textRuns)
@ -224,7 +233,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns);
break; break;
} }
@ -309,11 +318,9 @@ namespace Avalonia.Media.TextFormatting
&& x.Typeface == y.Typeface && x.Typeface == y.Typeface
&& x.BaselineAlignment == y.BaselineAlignment; && x.BaselineAlignment == y.BaselineAlignment;
private static IReadOnlyList<ShapedTextRun> ShapeTogether( private static void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text,
IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, TextShaperOptions options) TextShaperOptions options, List<TextRun> results)
{ {
var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, options); var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++) for (var i = 0; i < textRuns.Count; i++)
@ -322,12 +329,10 @@ namespace Avalonia.Media.TextFormatting
var splitResult = shapedBuffer.Split(currentRun.Length); var splitResult = shapedBuffer.Split(currentRun.Length);
shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
shapedBuffer = splitResult.Second!; shapedBuffer = splitResult.Second!;
} }
return shapedRuns;
} }
/// <summary> /// <summary>
@ -335,7 +340,7 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param> /// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
/// <param name="levels">The bidi levels.</param> /// <param name="levels">The bidi levels.</param>
/// <param name="processedRuns"></param> /// <param name="processedRuns">A list that will be filled with the processed runs.</param>
/// <returns></returns> /// <returns></returns>
private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels, private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels,
List<TextRun> processedRuns) List<TextRun> processedRuns)
@ -385,7 +390,8 @@ namespace Avalonia.Media.TextFormatting
if (j == runTextSpan.Length) if (j == runTextSpan.Length)
{ {
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties,
processedRuns);
runLevel = levels[levelIndex]; runLevel = levels[levelIndex];
@ -398,7 +404,8 @@ namespace Avalonia.Media.TextFormatting
} }
// End of this run // End of this run
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties,
processedRuns);
runText = runText.Slice(j); runText = runText.Slice(j);
runTextSpan = runText.Span; runTextSpan = runText.Span;
@ -415,7 +422,7 @@ namespace Avalonia.Media.TextFormatting
return; return;
} }
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns);
} }
/// <summary> /// <summary>
@ -423,8 +430,8 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <param name="textSource">The text source.</param> /// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param> /// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="endOfLine"></param> /// <param name="endOfLine">On return, the end of line, if any.</param>
/// <param name="textSourceLength"></param> /// <param name="textSourceLength">On return, the processed text source length.</param>
/// <returns> /// <returns>
/// The formatted text runs. /// The formatted text runs.
/// </returns> /// </returns>
@ -602,7 +609,7 @@ namespace Avalonia.Media.TextFormatting
var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection); (sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) }; var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
} }
@ -749,12 +756,10 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
var splitResult = SplitTextRuns(textRuns, measuredLength); var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength);
var remainingCharacters = splitResult.Second;
var lineBreak = remainingCharacters?.Count > 0 ? var lineBreak = postSplitRuns?.Count > 0 ?
new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : new TextLineBreak(null, resolvedFlowDirection, postSplitRuns) :
null; null;
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
@ -762,7 +767,7 @@ namespace Avalonia.Media.TextFormatting
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
} }
var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, paragraphWidth, paragraphProperties, resolvedFlowDirection,
lineBreak); lineBreak);

51
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting
private readonly ITextSource _textSource; private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
private readonly TextTrimming _textTrimming; private readonly TextTrimming _textTrimming;
private readonly TextLine[] _textLines;
private int _textSourceLength; private int _textSourceLength;
@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
MaxLines = maxLines; MaxLines = maxLines;
TextLines = CreateTextLines(); _textLines = CreateTextLines();
} }
/// <summary> /// <summary>
@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting
MaxLines = maxLines; MaxLines = maxLines;
TextLines = CreateTextLines(); _textLines = CreateTextLines();
} }
/// <summary> /// <summary>
@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting
/// <value> /// <value>
/// The text lines. /// The text lines.
/// </value> /// </value>
public IReadOnlyList<TextLine> TextLines { get; private set; } public IReadOnlyList<TextLine> TextLines
=> _textLines;
/// <summary> /// <summary>
/// Gets the bounds of the layout. /// Gets the bounds of the layout.
@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting
/// <param name="origin">The origin.</param> /// <param name="origin">The origin.</param>
public void Draw(DrawingContext context, Point origin) public void Draw(DrawingContext context, Point origin)
{ {
if (!TextLines.Any()) if (_textLines.Length == 0)
{ {
return; return;
} }
var (currentX, currentY) = origin; var (currentX, currentY) = origin;
foreach (var textLine in TextLines) foreach (var textLine in _textLines)
{ {
textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); textLine.Draw(context, new Point(currentX + textLine.Start, currentY));
@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns></returns> /// <returns></returns>
public Rect HitTestTextPosition(int textPosition) public Rect HitTestTextPosition(int textPosition)
{ {
if (TextLines.Count == 0) if (_textLines.Length == 0)
{ {
return new Rect(); return new Rect();
} }
@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting
var currentY = 0.0; var currentY = 0.0;
foreach (var textLine in TextLines) foreach (var textLine in _textLines)
{ {
var end = textLine.FirstTextSourceIndex + textLine.Length; var end = textLine.FirstTextSourceIndex + textLine.Length;
@ -230,11 +231,11 @@ namespace Avalonia.Media.TextFormatting
return Array.Empty<Rect>(); return Array.Empty<Rect>();
} }
var result = new List<Rect>(TextLines.Count); var result = new List<Rect>(_textLines.Length);
var currentY = 0d; var currentY = 0d;
foreach (var textLine in TextLines) foreach (var textLine in _textLines)
{ {
//Current line isn't covered. //Current line isn't covered.
if (textLine.FirstTextSourceIndex + textLine.Length < start) if (textLine.FirstTextSourceIndex + textLine.Length < start)
@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting
{ {
var currentY = 0d; var currentY = 0d;
var lineIndex = 0;
TextLine? currentLine = null; TextLine? currentLine = null;
CharacterHit characterHit; CharacterHit characterHit;
for (; lineIndex < TextLines.Count; lineIndex++) for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++)
{ {
currentLine = TextLines[lineIndex]; currentLine = _textLines[lineIndex];
if (currentY + currentLine.Height > point.Y) if (currentY + currentLine.Height > point.Y)
{ {
@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting
if (charIndex > _textSourceLength) if (charIndex > _textSourceLength)
{ {
return TextLines.Count - 1; return _textLines.Length - 1;
} }
for (var index = 0; index < TextLines.Count; index++) for (var index = 0; index < _textLines.Length; index++)
{ {
var textLine = TextLines[index]; var textLine = _textLines[index];
if (textLine.FirstTextSourceIndex + textLine.Length < charIndex) if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
{ {
@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
return TextLines.Count - 1; return _textLines.Length - 1;
} }
private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point) private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
@ -424,7 +424,7 @@ namespace Avalonia.Media.TextFormatting
height += textLine.Height; height += textLine.Height;
} }
private IReadOnlyList<TextLine> CreateTextLines() private TextLine[] CreateTextLines()
{ {
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
@ -432,7 +432,7 @@ namespace Avalonia.Media.TextFormatting
Bounds = new Rect(0, 0, 0, textLine.Height); Bounds = new Rect(0, 0, 0, textLine.Height);
return new List<TextLine> { textLine }; return new TextLine[] { textLine };
} }
var textLines = new List<TextLine>(); var textLines = new List<TextLine>();
@ -443,12 +443,14 @@ namespace Avalonia.Media.TextFormatting
TextLine? previousLine = null; TextLine? previousLine = null;
var textFormatter = TextFormatter.Current;
while (true) while (true)
{ {
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak); _paragraphProperties, previousLine?.TextLineBreak);
if(textLine == null || textLine.Length == 0) if (textLine.Length == 0)
{ {
if (previousLine != null && previousLine.NewLineLength > 0) if (previousLine != null && previousLine.NewLineLength > 0)
{ {
@ -524,8 +526,9 @@ namespace Avalonia.Media.TextFormatting
{ {
var whitespaceWidth = 0d; var whitespaceWidth = 0d;
foreach (var line in textLines) for (var i = 0; i < textLines.Count; i++)
{ {
var line = textLines[i];
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if (lineWhitespaceWidth > whitespaceWidth) if (lineWhitespaceWidth > whitespaceWidth)
@ -549,7 +552,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
return textLines; return textLines.ToArray();
} }
/// <summary> /// <summary>
@ -569,7 +572,7 @@ namespace Avalonia.Media.TextFormatting
public void Dispose() public void Dispose()
{ {
foreach (var line in TextLines) foreach (var line in _textLines)
{ {
line.Dispose(); line.Dispose();
} }

50
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -1,5 +1,7 @@
using System; // ReSharper disable ForCanBeConvertedToForeach
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextRun Symbol { get; } public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine) /// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{ {
var textRuns = textLine.TextRuns; var textRuns = textLine.TextRuns;
if (textRuns == null || textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return null; return null;
} }
@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width) if (Width < shapedSymbol.GlyphRun.Size.Width)
{ {
return new List<TextRun>(0); return Array.Empty<TextRun>();
} }
// Overview of ellipsis structure // Overview of ellipsis structure
@ -75,41 +78,48 @@ namespace Avalonia.Media.TextFormatting
{ {
shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
var collapsedRuns = new List<TextRun>(textRuns.Count);
if (measuredLength > 0) if (measuredLength > 0)
{ {
IReadOnlyList<TextRun>? preSplitRuns = null; var collapsedRuns = new List<TextRun>(textRuns.Count + 1);
// perf note: the runs are very likely to come from TextLineImpl,
// which already uses an array: ToArray() won't ever be called in this case
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
IReadOnlyList<TextRun>? preSplitRuns;
IReadOnlyList<TextRun>? postSplitRuns; IReadOnlyList<TextRun>? postSplitRuns;
if (_prefixLength > 0) if (_prefixLength > 0)
{ {
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(
Math.Min(_prefixLength, measuredLength)); textRunArray, Math.Min(_prefixLength, measuredLength));
collapsedRuns.AddRange(splitResult.First); for (var i = 0; i < preSplitRuns.Count; i++)
{
preSplitRuns = splitResult.First; var preSplitRun = preSplitRuns[i];
postSplitRuns = splitResult.Second; collapsedRuns.Add(preSplitRun);
}
} }
else else
{ {
postSplitRuns = textRuns; preSplitRuns = null;
postSplitRuns = textRunArray;
} }
collapsedRuns.Add(shapedSymbol); collapsedRuns.Add(shapedSymbol);
if (measuredLength <= _prefixLength || postSplitRuns is null) if (measuredLength <= _prefixLength || postSplitRuns is null)
{ {
return collapsedRuns; return collapsedRuns.ToArray();
} }
var availableSuffixWidth = availableWidth; var availableSuffixWidth = availableWidth;
if (preSplitRuns is not null) if (preSplitRuns is not null)
{ {
foreach (var run in preSplitRuns) for (var i = 0; i < preSplitRuns.Count; i++)
{ {
var run = preSplitRuns[i];
if (run is DrawableTextRun drawableTextRun) if (run is DrawableTextRun drawableTextRun)
{ {
availableSuffixWidth -= drawableTextRun.Size.Width; availableSuffixWidth -= drawableTextRun.Size.Width;
@ -143,13 +153,11 @@ namespace Avalonia.Media.TextFormatting
} }
} }
} }
}
else return collapsedRuns.ToArray();
{
collapsedRuns.Add(shapedSymbol);
} }
return collapsedRuns; return new TextRun[] { shapedSymbol };
} }
availableWidth -= shapedRun.Size.Width; availableWidth -= shapedRun.Size.Width;

56
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -1,19 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
internal class TextLineImpl : TextLine internal sealed class TextLineImpl : TextLine
{ {
private IReadOnlyList<TextRun> _textRuns; private TextRun[] _textRuns;
private readonly double _paragraphWidth; private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics; private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _resolvedFlowDirection; private readonly FlowDirection _resolvedFlowDirection;
public TextLineImpl(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth, public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false) TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{ {
@ -147,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties, var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
_resolvedFlowDirection, TextLineBreak, true); _resolvedFlowDirection, TextLineBreak, true);
if (collapsedRuns.Count > 0) if (collapsedRuns.Length > 0)
{ {
collapsedLine.FinalizeLine(); collapsedLine.FinalizeLine();
} }
@ -166,7 +165,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetCharacterHitFromDistance(double distance) public override CharacterHit GetCharacterHitFromDistance(double distance)
{ {
if (_textRuns.Count == 0) if (_textRuns.Length == 0)
{ {
return new CharacterHit(); return new CharacterHit();
} }
@ -182,7 +181,7 @@ namespace Avalonia.Media.TextFormatting
if (distance >= WidthIncludingTrailingWhitespace) if (distance >= WidthIncludingTrailingWhitespace)
{ {
var lastRun = _textRuns[_textRuns.Count - 1]; var lastRun = _textRuns[_textRuns.Length - 1];
var size = 0.0; var size = 0.0;
@ -199,7 +198,7 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0; var currentDistance = 0.0;
for (var i = 0; i < _textRuns.Count; i++) for (var i = 0; i < _textRuns.Length; i++)
{ {
var currentRun = _textRuns[i]; var currentRun = _textRuns[i];
@ -208,7 +207,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftIndex = i; var rightToLeftIndex = i;
currentPosition += currentRun.Length; currentPosition += currentRun.Length;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1) while (rightToLeftIndex + 1 <= _textRuns.Length - 1)
{ {
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun; var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun;
@ -224,7 +223,7 @@ namespace Avalonia.Media.TextFormatting
for (var j = i; i <= rightToLeftIndex; j++) for (var j = i; i <= rightToLeftIndex; j++)
{ {
if (j > _textRuns.Count - 1) if (j > _textRuns.Length - 1)
{ {
break; break;
} }
@ -254,7 +253,7 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is DrawableTextRun drawableTextRun) if (currentRun is DrawableTextRun drawableTextRun)
{ {
if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance)
{ {
currentDistance += drawableTextRun.Size.Width; currentDistance += drawableTextRun.Size.Width;
@ -328,7 +327,7 @@ namespace Avalonia.Media.TextFormatting
if (flowDirection == FlowDirection.LeftToRight) if (flowDirection == FlowDirection.LeftToRight)
{ {
for (var index = 0; index < _textRuns.Count; index++) for (var index = 0; index < _textRuns.Length; index++)
{ {
var currentRun = _textRuns[index]; var currentRun = _textRuns[index];
@ -338,7 +337,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftWidth = shapedRun.Size.Width; var rightToLeftWidth = shapedRun.Size.Width;
while (i + 1 <= _textRuns.Count - 1) while (i + 1 <= _textRuns.Length - 1)
{ {
var nextRun = _textRuns[i + 1]; var nextRun = _textRuns[i + 1];
@ -402,7 +401,7 @@ namespace Avalonia.Media.TextFormatting
{ {
currentDistance += WidthIncludingTrailingWhitespace; currentDistance += WidthIncludingTrailingWhitespace;
for (var index = _textRuns.Count - 1; index >= 0; index--) for (var index = _textRuns.Length - 1; index >= 0; index--)
{ {
var currentRun = _textRuns[index]; var currentRun = _textRuns[index];
@ -502,7 +501,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{ {
if (_textRuns.Count == 0) if (_textRuns.Length == 0)
{ {
return new CharacterHit(); return new CharacterHit();
} }
@ -637,7 +636,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftIndex = index; var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width; var rightToLeftWidth = currentShapedRun.Size.Width;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
{ {
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{ {
@ -981,7 +980,7 @@ namespace Avalonia.Media.TextFormatting
public override void Dispose() public override void Dispose()
{ {
for (int i = 0; i < _textRuns.Count; i++) for (int i = 0; i < _textRuns.Length; i++)
{ {
if (_textRuns[i] is ShapedTextRun shapedTextRun) if (_textRuns[i] is ShapedTextRun shapedTextRun)
{ {
@ -1013,7 +1012,7 @@ namespace Avalonia.Media.TextFormatting
private void BidiReorder() private void BidiReorder()
{ {
if (_textRuns.Count == 0) if (_textRuns.Length == 0)
{ {
return; return;
} }
@ -1025,7 +1024,7 @@ namespace Avalonia.Media.TextFormatting
var current = orderedRun; var current = orderedRun;
for (var i = 1; i < _textRuns.Count; i++) for (var i = 1; i < _textRuns.Length; i++)
{ {
run = _textRuns[i]; run = _textRuns[i];
@ -1044,7 +1043,7 @@ namespace Avalonia.Media.TextFormatting
sbyte max = 0; sbyte max = 0;
var min = sbyte.MaxValue; var min = sbyte.MaxValue;
for (var i = 0; i < _textRuns.Count; i++) for (var i = 0; i < _textRuns.Length; i++)
{ {
var currentRun = _textRuns[i]; var currentRun = _textRuns[i];
@ -1095,13 +1094,14 @@ namespace Avalonia.Media.TextFormatting
minLevelToReverse--; minLevelToReverse--;
} }
var textRuns = new List<TextRun>(_textRuns.Count); var textRuns = new TextRun[_textRuns.Length];
var index = 0;
current = orderedRun; current = orderedRun;
while (current != null) while (current != null)
{ {
textRuns.Add(current.Run); textRuns[index++] = current.Run;
current = current.Next; current = current.Next;
} }
@ -1197,7 +1197,7 @@ namespace Avalonia.Media.TextFormatting
var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Count) while (runIndex < _textRuns.Length)
{ {
var currentRun = _textRuns[runIndex]; var currentRun = _textRuns[runIndex];
@ -1346,7 +1346,7 @@ namespace Avalonia.Media.TextFormatting
textPosition = FirstTextSourceIndex; textPosition = FirstTextSourceIndex;
TextRun? previousRun = null; TextRun? previousRun = null;
while (runIndex < _textRuns.Count) while (runIndex < _textRuns.Length)
{ {
var currentRun = _textRuns[runIndex]; var currentRun = _textRuns[runIndex];
@ -1395,7 +1395,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
if (runIndex + 1 >= _textRuns.Count) if (runIndex + 1 >= _textRuns.Length)
{ {
return runIndex; return runIndex;
} }
@ -1411,7 +1411,7 @@ namespace Avalonia.Media.TextFormatting
return runIndex; return runIndex;
} }
if (runIndex + 1 >= _textRuns.Count) if (runIndex + 1 >= _textRuns.Length)
{ {
return runIndex; return runIndex;
} }
@ -1448,14 +1448,14 @@ namespace Avalonia.Media.TextFormatting
var lineHeight = _paragraphProperties.LineHeight; var lineHeight = _paragraphProperties.LineHeight;
var lastRunIndex = _textRuns.Count - 1; var lastRunIndex = _textRuns.Length - 1;
if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine)
{ {
lastRunIndex--; lastRunIndex--;
} }
for (var index = 0; index < _textRuns.Count; index++) for (var index = 0; index < _textRuns.Length; index++)
{ {
switch (_textRuns[index]) switch (_textRuns[index])
{ {

7
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -1,6 +1,4 @@
using System.Collections.Generic; namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{ {
/// <summary> /// <summary>
/// A collapsing properties to collapse whole line toward the end /// A collapsing properties to collapse whole line toward the end
@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextRun Symbol { get; } public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine) /// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{ {
return TextEllipsisHelper.Collapse(textLine, this, false); return TextEllipsisHelper.Collapse(textLine, this, false);
} }

8
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -1,7 +1,4 @@
using System.Collections.Generic; namespace Avalonia.Media.TextFormatting
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{ {
/// <summary> /// <summary>
/// a collapsing properties to collapse whole line toward the end /// a collapsing properties to collapse whole line toward the end
@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextRun Symbol { get; } public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine) /// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{ {
return TextEllipsisHelper.Collapse(textLine, this, true); return TextEllipsisHelper.Collapse(textLine, this, true);
} }

Loading…
Cancel
Save