|
|
|
@ -1,14 +1,16 @@ |
|
|
|
using System; |
|
|
|
// ReSharper disable ForCanBeConvertedToForeach
|
|
|
|
using System; |
|
|
|
using System.Buffers; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Linq; |
|
|
|
using System.Runtime.InteropServices; |
|
|
|
using Avalonia.Media.TextFormatting.Unicode; |
|
|
|
using Avalonia.Utilities; |
|
|
|
using static Avalonia.Media.TextFormatting.FormattingObjectPool; |
|
|
|
|
|
|
|
namespace Avalonia.Media.TextFormatting |
|
|
|
{ |
|
|
|
internal class TextFormatterImpl : TextFormatter |
|
|
|
internal sealed class TextFormatterImpl : TextFormatter |
|
|
|
{ |
|
|
|
private static readonly char[] s_empty = { ' ' }; |
|
|
|
private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength]; |
|
|
|
@ -23,20 +25,25 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
var textWrapping = paragraphProperties.TextWrapping; |
|
|
|
FlowDirection resolvedFlowDirection; |
|
|
|
TextLineBreak? nextLineBreak = null; |
|
|
|
IReadOnlyList<TextRun> textRuns; |
|
|
|
IReadOnlyList<TextRun>? textRuns; |
|
|
|
var objectPool = FormattingObjectPool.Instance; |
|
|
|
|
|
|
|
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, |
|
|
|
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, |
|
|
|
out var textEndOfLine, out var textSourceLength); |
|
|
|
|
|
|
|
RentedList<TextRun>? shapedTextRuns; |
|
|
|
|
|
|
|
if (previousLineBreak?.RemainingRuns is { } remainingRuns) |
|
|
|
{ |
|
|
|
resolvedFlowDirection = previousLineBreak.FlowDirection; |
|
|
|
textRuns = remainingRuns; |
|
|
|
nextLineBreak = previousLineBreak; |
|
|
|
shapedTextRuns = null; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); |
|
|
|
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); |
|
|
|
textRuns = shapedTextRuns; |
|
|
|
|
|
|
|
if (nextLineBreak == null && textEndOfLine != null) |
|
|
|
{ |
|
|
|
@ -49,25 +56,32 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
switch (textWrapping) |
|
|
|
{ |
|
|
|
case TextWrapping.NoWrap: |
|
|
|
{ |
|
|
|
textLine = new TextLineImpl(textRuns.ToArray(), firstTextSourceIndex, textSourceLength, |
|
|
|
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); |
|
|
|
{ |
|
|
|
// perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
|
|
|
|
// which already uses an array: ToArray() won't ever be called in this case
|
|
|
|
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); |
|
|
|
|
|
|
|
textLine.FinalizeLine(); |
|
|
|
textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, |
|
|
|
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
textLine.FinalizeLine(); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
case TextWrapping.WrapWithOverflow: |
|
|
|
case TextWrapping.Wrap: |
|
|
|
{ |
|
|
|
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, |
|
|
|
resolvedFlowDirection, nextLineBreak); |
|
|
|
break; |
|
|
|
} |
|
|
|
{ |
|
|
|
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, |
|
|
|
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); |
|
|
|
break; |
|
|
|
} |
|
|
|
default: |
|
|
|
throw new ArgumentOutOfRangeException(nameof(textWrapping)); |
|
|
|
} |
|
|
|
|
|
|
|
objectPool.TextRunLists.Return(ref shapedTextRuns); |
|
|
|
objectPool.TextRunLists.Return(ref fetchedRuns); |
|
|
|
|
|
|
|
return textLine; |
|
|
|
} |
|
|
|
|
|
|
|
@ -76,9 +90,12 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// </summary>
|
|
|
|
/// <param name="textRuns">The text run's.</param>
|
|
|
|
/// <param name="length">The length to split at.</param>
|
|
|
|
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
|
|
|
|
/// <returns>The split text runs.</returns>
|
|
|
|
internal static SplitResult<IReadOnlyList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length) |
|
|
|
internal static SplitResult<RentedList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length, |
|
|
|
FormattingObjectPool objectPool) |
|
|
|
{ |
|
|
|
var first = objectPool.TextRunLists.Rent(); |
|
|
|
var currentLength = 0; |
|
|
|
|
|
|
|
for (var i = 0; i < textRuns.Count; i++) |
|
|
|
@ -94,8 +111,6 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
var firstCount = currentRun.Length >= 1 ? i + 1 : i; |
|
|
|
|
|
|
|
var first = new List<TextRun>(firstCount); |
|
|
|
|
|
|
|
if (firstCount > 1) |
|
|
|
{ |
|
|
|
for (var j = 0; j < i; j++) |
|
|
|
@ -108,7 +123,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
if (currentLength + currentRun.Length == length) |
|
|
|
{ |
|
|
|
var second = secondCount > 0 ? new List<TextRun>(secondCount) : null; |
|
|
|
var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; |
|
|
|
|
|
|
|
if (second != null) |
|
|
|
{ |
|
|
|
@ -122,13 +137,13 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
first.Add(currentRun); |
|
|
|
|
|
|
|
return new SplitResult<IReadOnlyList<TextRun>>(first, second); |
|
|
|
return new SplitResult<RentedList<TextRun>>(first, second); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
secondCount++; |
|
|
|
|
|
|
|
var second = new List<TextRun>(secondCount); |
|
|
|
var second = objectPool.TextRunLists.Rent(); |
|
|
|
|
|
|
|
if (currentRun is ShapedTextRun shapedTextCharacters) |
|
|
|
{ |
|
|
|
@ -144,11 +159,16 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
second.Add(textRuns[i + j]); |
|
|
|
} |
|
|
|
|
|
|
|
return new SplitResult<IReadOnlyList<TextRun>>(first, second); |
|
|
|
return new SplitResult<RentedList<TextRun>>(first, second); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return new SplitResult<IReadOnlyList<TextRun>>(textRuns, null); |
|
|
|
for (var i = 0; i < textRuns.Count; i++) |
|
|
|
{ |
|
|
|
first.Add(textRuns[i]); |
|
|
|
} |
|
|
|
|
|
|
|
return new SplitResult<RentedList<TextRun>>(first, null); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -157,14 +177,16 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// <param name="textRuns">The text runs to shape.</param>
|
|
|
|
/// <param name="paragraphProperties">The default paragraph properties.</param>
|
|
|
|
/// <param name="resolvedFlowDirection">The resolved flow direction.</param>
|
|
|
|
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// A list of shaped text characters.
|
|
|
|
/// </returns>
|
|
|
|
private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties, |
|
|
|
private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns, |
|
|
|
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, |
|
|
|
out FlowDirection resolvedFlowDirection) |
|
|
|
{ |
|
|
|
var flowDirection = paragraphProperties.FlowDirection; |
|
|
|
var shapedRuns = new List<TextRun>(); |
|
|
|
var shapedRuns = objectPool.TextRunLists.Rent(); |
|
|
|
|
|
|
|
if (textRuns.Count == 0) |
|
|
|
{ |
|
|
|
@ -172,13 +194,14 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
return shapedRuns; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var bidiData = t_bidiData ??= new BidiData(); |
|
|
|
var bidiData = t_bidiData ??= new(); |
|
|
|
bidiData.Reset(); |
|
|
|
bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; |
|
|
|
|
|
|
|
foreach (var textRun in textRuns) |
|
|
|
for (var i = 0; i < textRuns.Count; ++i) |
|
|
|
{ |
|
|
|
var textRun = textRuns[i]; |
|
|
|
|
|
|
|
ReadOnlySpan<char> text; |
|
|
|
if (!textRun.Text.IsEmpty) |
|
|
|
text = textRun.Text.Span; |
|
|
|
@ -190,8 +213,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
bidiData.Append(text); |
|
|
|
} |
|
|
|
|
|
|
|
var bidiAlgorithm = t_bidiAlgorithm ??= new BidiAlgorithm(); |
|
|
|
|
|
|
|
var bidiAlgorithm = t_bidiAlgorithm ??= new(); |
|
|
|
bidiAlgorithm.Process(bidiData); |
|
|
|
|
|
|
|
var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes); |
|
|
|
@ -199,9 +221,11 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
resolvedFlowDirection = |
|
|
|
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; |
|
|
|
|
|
|
|
var processedRuns = new List<TextRun>(textRuns.Count); |
|
|
|
var processedRuns = objectPool.TextRunLists.Rent(); |
|
|
|
|
|
|
|
CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); |
|
|
|
|
|
|
|
CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels, processedRuns); |
|
|
|
var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); |
|
|
|
|
|
|
|
for (var index = 0; index < processedRuns.Count; index++) |
|
|
|
{ |
|
|
|
@ -210,8 +234,9 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
switch (currentRun) |
|
|
|
{ |
|
|
|
case UnshapedTextRun shapeableRun: |
|
|
|
{ |
|
|
|
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun }; |
|
|
|
{ |
|
|
|
groupedRuns.Clear(); |
|
|
|
groupedRuns.Add(shapeableRun); |
|
|
|
var text = shapeableRun.Text; |
|
|
|
|
|
|
|
while (index + 1 < processedRuns.Count) |
|
|
|
@ -253,6 +278,9 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
objectPool.TextRunLists.Return(ref processedRuns); |
|
|
|
objectPool.UnshapedTextRunLists.Return(ref groupedRuns); |
|
|
|
|
|
|
|
return shapedRuns; |
|
|
|
} |
|
|
|
|
|
|
|
@ -319,14 +347,13 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y) |
|
|
|
=> MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize) |
|
|
|
&& x.Typeface == y.Typeface |
|
|
|
&& x.BaselineAlignment == y.BaselineAlignment; |
|
|
|
|
|
|
|
private static void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, |
|
|
|
TextShaperOptions options, List<TextRun> results) |
|
|
|
TextShaperOptions options, RentedList<TextRun> results) |
|
|
|
{ |
|
|
|
var shapedBuffer = TextShaper.Current.ShapeText(text, options); |
|
|
|
|
|
|
|
@ -349,8 +376,8 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// <param name="levels">The bidi levels.</param>
|
|
|
|
/// <param name="processedRuns">A list that will be filled with the processed runs.</param>
|
|
|
|
/// <returns></returns>
|
|
|
|
private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels, |
|
|
|
List<TextRun> processedRuns) |
|
|
|
private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels, |
|
|
|
RentedList<TextRun> processedRuns) |
|
|
|
{ |
|
|
|
if (levels.Length == 0) |
|
|
|
{ |
|
|
|
@ -437,19 +464,20 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// </summary>
|
|
|
|
/// <param name="textSource">The text source.</param>
|
|
|
|
/// <param name="firstTextSourceIndex">The first text source index.</param>
|
|
|
|
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
|
|
|
|
/// <param name="endOfLine">On return, the end of line, if any.</param>
|
|
|
|
/// <param name="textSourceLength">On return, the processed text source length.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// The formatted text runs.
|
|
|
|
/// </returns>
|
|
|
|
private static List<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, |
|
|
|
out TextEndOfLine? endOfLine, out int textSourceLength) |
|
|
|
private static RentedList<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, |
|
|
|
FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength) |
|
|
|
{ |
|
|
|
textSourceLength = 0; |
|
|
|
|
|
|
|
endOfLine = null; |
|
|
|
|
|
|
|
var textRuns = new List<TextRun>(); |
|
|
|
var textRuns = objectPool.TextRunLists.Rent(); |
|
|
|
|
|
|
|
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); |
|
|
|
|
|
|
|
@ -543,8 +571,10 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
measuredLength = 0; |
|
|
|
var currentWidth = 0.0; |
|
|
|
|
|
|
|
foreach (var currentRun in textRuns) |
|
|
|
for (var i = 0; i < textRuns.Count; ++i) |
|
|
|
{ |
|
|
|
var currentRun = textRuns[i]; |
|
|
|
|
|
|
|
switch (currentRun) |
|
|
|
{ |
|
|
|
case ShapedTextRun shapedTextCharacters: |
|
|
|
@ -554,15 +584,15 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; |
|
|
|
var lastCluster = firstCluster; |
|
|
|
|
|
|
|
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) |
|
|
|
for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) |
|
|
|
{ |
|
|
|
var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; |
|
|
|
var glyphInfo = shapedTextCharacters.ShapedBuffer[j]; |
|
|
|
|
|
|
|
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) |
|
|
|
{ |
|
|
|
measuredLength += Math.Max(0, lastCluster - firstCluster); |
|
|
|
|
|
|
|
goto found; |
|
|
|
return measuredLength != 0; |
|
|
|
} |
|
|
|
|
|
|
|
lastCluster = glyphInfo.GlyphCluster; |
|
|
|
@ -579,7 +609,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
{ |
|
|
|
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) |
|
|
|
{ |
|
|
|
goto found; |
|
|
|
return measuredLength != 0; |
|
|
|
} |
|
|
|
|
|
|
|
measuredLength += currentRun.Length; |
|
|
|
@ -596,8 +626,6 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
found: |
|
|
|
|
|
|
|
return measuredLength != 0; |
|
|
|
} |
|
|
|
|
|
|
|
@ -605,7 +633,8 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// Creates an empty text line.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns>The empty text line.</returns>
|
|
|
|
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) |
|
|
|
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, |
|
|
|
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) |
|
|
|
{ |
|
|
|
var flowDirection = paragraphProperties.FlowDirection; |
|
|
|
var properties = paragraphProperties.DefaultTextRunProperties; |
|
|
|
@ -618,7 +647,9 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; |
|
|
|
|
|
|
|
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); |
|
|
|
var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); |
|
|
|
line.FinalizeLine(); |
|
|
|
return line; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -630,14 +661,15 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// <param name="paragraphProperties">The text paragraph properties.</param>
|
|
|
|
/// <param name="resolvedFlowDirection"></param>
|
|
|
|
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
|
|
|
|
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
|
|
|
|
/// <returns>The wrapped text line.</returns>
|
|
|
|
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, |
|
|
|
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, |
|
|
|
TextLineBreak? currentLineBreak) |
|
|
|
TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) |
|
|
|
{ |
|
|
|
if (textRuns.Count == 0) |
|
|
|
{ |
|
|
|
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); |
|
|
|
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); |
|
|
|
} |
|
|
|
|
|
|
|
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) |
|
|
|
@ -763,10 +795,10 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength); |
|
|
|
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); |
|
|
|
|
|
|
|
var lineBreak = postSplitRuns?.Count > 0 ? |
|
|
|
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns) : |
|
|
|
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : |
|
|
|
null; |
|
|
|
|
|
|
|
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) |
|
|
|
@ -778,7 +810,12 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
paragraphWidth, paragraphProperties, resolvedFlowDirection, |
|
|
|
lineBreak); |
|
|
|
|
|
|
|
return textLine.FinalizeLine(); |
|
|
|
textLine.FinalizeLine(); |
|
|
|
|
|
|
|
objectPool.TextRunLists.Return(ref preSplitRuns); |
|
|
|
objectPool.TextRunLists.Return(ref postSplitRuns); |
|
|
|
|
|
|
|
return textLine; |
|
|
|
} |
|
|
|
|
|
|
|
private struct TextRunEnumerator |
|
|
|
|