|
|
|
@ -2,7 +2,6 @@ |
|
|
|
using System; |
|
|
|
using System.Buffers; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Linq; |
|
|
|
using System.Runtime.InteropServices; |
|
|
|
using Avalonia.Media.TextFormatting.Unicode; |
|
|
|
using Avalonia.Utilities; |
|
|
|
@ -22,68 +21,55 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, |
|
|
|
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) |
|
|
|
{ |
|
|
|
var textWrapping = paragraphProperties.TextWrapping; |
|
|
|
FlowDirection resolvedFlowDirection; |
|
|
|
TextLineBreak? nextLineBreak = null; |
|
|
|
IReadOnlyList<TextRun>? textRuns; |
|
|
|
var objectPool = FormattingObjectPool.Instance; |
|
|
|
var fontManager = FontManager.Current; |
|
|
|
|
|
|
|
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, |
|
|
|
out var textEndOfLine, out var textSourceLength); |
|
|
|
// we've wrapped the previous line and need to continue wrapping: ignore the textSource and do that instead
|
|
|
|
if (previousLineBreak is WrappingTextLineBreak wrappingTextLineBreak |
|
|
|
&& wrappingTextLineBreak.AcquireRemainingRuns() is { } remainingRuns |
|
|
|
&& paragraphProperties.TextWrapping != TextWrapping.NoWrap) |
|
|
|
{ |
|
|
|
return PerformTextWrapping(remainingRuns, true, firstTextSourceIndex, paragraphWidth, |
|
|
|
paragraphProperties, previousLineBreak.FlowDirection, previousLineBreak, objectPool); |
|
|
|
} |
|
|
|
|
|
|
|
RentedList<TextRun>? fetchedRuns = null; |
|
|
|
RentedList<TextRun>? shapedTextRuns = null; |
|
|
|
|
|
|
|
try |
|
|
|
{ |
|
|
|
if (previousLineBreak?.RemainingRuns is { } remainingRuns) |
|
|
|
{ |
|
|
|
resolvedFlowDirection = previousLineBreak.FlowDirection; |
|
|
|
textRuns = remainingRuns; |
|
|
|
nextLineBreak = previousLineBreak; |
|
|
|
shapedTextRuns = null; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, |
|
|
|
out resolvedFlowDirection); |
|
|
|
textRuns = shapedTextRuns; |
|
|
|
fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, |
|
|
|
out var textSourceLength); |
|
|
|
|
|
|
|
if (nextLineBreak == null && textEndOfLine != null) |
|
|
|
{ |
|
|
|
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); |
|
|
|
} |
|
|
|
} |
|
|
|
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, |
|
|
|
out var resolvedFlowDirection); |
|
|
|
|
|
|
|
TextLineImpl textLine; |
|
|
|
if (nextLineBreak == null && textEndOfLine != null) |
|
|
|
{ |
|
|
|
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); |
|
|
|
} |
|
|
|
|
|
|
|
switch (textWrapping) |
|
|
|
switch (paragraphProperties.TextWrapping) |
|
|
|
{ |
|
|
|
case TextWrapping.NoWrap: |
|
|
|
{ |
|
|
|
// 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 = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, |
|
|
|
var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex, |
|
|
|
textSourceLength, |
|
|
|
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); |
|
|
|
|
|
|
|
textLine.FinalizeLine(); |
|
|
|
|
|
|
|
break; |
|
|
|
return textLine; |
|
|
|
} |
|
|
|
case TextWrapping.WrapWithOverflow: |
|
|
|
case TextWrapping.Wrap: |
|
|
|
{ |
|
|
|
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, |
|
|
|
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); |
|
|
|
break; |
|
|
|
return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth, |
|
|
|
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); |
|
|
|
} |
|
|
|
default: |
|
|
|
throw new ArgumentOutOfRangeException(nameof(textWrapping)); |
|
|
|
throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping)); |
|
|
|
} |
|
|
|
|
|
|
|
return textLine; |
|
|
|
} |
|
|
|
finally |
|
|
|
{ |
|
|
|
@ -108,15 +94,16 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
for (var i = 0; i < textRuns.Count; i++) |
|
|
|
{ |
|
|
|
var currentRun = textRuns[i]; |
|
|
|
var currentRunLength = currentRun.Length; |
|
|
|
|
|
|
|
if (currentLength + currentRun.Length < length) |
|
|
|
if (currentLength + currentRunLength < length) |
|
|
|
{ |
|
|
|
currentLength += currentRun.Length; |
|
|
|
currentLength += currentRunLength; |
|
|
|
|
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
var firstCount = currentRun.Length >= 1 ? i + 1 : i; |
|
|
|
var firstCount = currentRunLength >= 1 ? i + 1 : i; |
|
|
|
|
|
|
|
if (firstCount > 1) |
|
|
|
{ |
|
|
|
@ -128,13 +115,13 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
var secondCount = textRuns.Count - firstCount; |
|
|
|
|
|
|
|
if (currentLength + currentRun.Length == length) |
|
|
|
if (currentLength + currentRunLength == length) |
|
|
|
{ |
|
|
|
var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; |
|
|
|
|
|
|
|
if (second != null) |
|
|
|
{ |
|
|
|
var offset = currentRun.Length >= 1 ? 1 : 0; |
|
|
|
var offset = currentRunLength >= 1 ? 1 : 0; |
|
|
|
|
|
|
|
for (var j = 0; j < secondCount; j++) |
|
|
|
{ |
|
|
|
@ -653,7 +640,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// </summary>
|
|
|
|
/// <returns>The empty text line.</returns>
|
|
|
|
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, |
|
|
|
TextParagraphProperties paragraphProperties, FontManager fontManager) |
|
|
|
TextParagraphProperties paragraphProperties) |
|
|
|
{ |
|
|
|
var flowDirection = paragraphProperties.FlowDirection; |
|
|
|
var properties = paragraphProperties.DefaultTextRunProperties; |
|
|
|
@ -675,21 +662,21 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// Performs text wrapping returns a list of text lines.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="textRuns"></param>
|
|
|
|
/// <param name="canReuseTextRunList">Whether <see cref="textRuns"/> can be reused to store the split runs.</param>
|
|
|
|
/// <param name="firstTextSourceIndex">The first text source index.</param>
|
|
|
|
/// <param name="paragraphWidth">The paragraph width.</param>
|
|
|
|
/// <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>
|
|
|
|
/// <param name="fontManager">The font manager to use.</param>
|
|
|
|
/// <returns>The wrapped text line.</returns>
|
|
|
|
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, |
|
|
|
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, |
|
|
|
TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager) |
|
|
|
private static TextLineImpl PerformTextWrapping(List<TextRun> textRuns, bool canReuseTextRunList, |
|
|
|
int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, |
|
|
|
FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) |
|
|
|
{ |
|
|
|
if (textRuns.Count == 0) |
|
|
|
{ |
|
|
|
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager); |
|
|
|
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); |
|
|
|
} |
|
|
|
|
|
|
|
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) |
|
|
|
@ -819,13 +806,37 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
try |
|
|
|
{ |
|
|
|
var textLineBreak = postSplitRuns?.Count > 0 ? |
|
|
|
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : |
|
|
|
null; |
|
|
|
TextLineBreak? textLineBreak; |
|
|
|
if (postSplitRuns?.Count > 0) |
|
|
|
{ |
|
|
|
List<TextRun> remainingRuns; |
|
|
|
|
|
|
|
// reuse the list as much as possible:
|
|
|
|
// if canReuseTextRunList == true it's coming from previous remaining runs
|
|
|
|
if (canReuseTextRunList) |
|
|
|
{ |
|
|
|
remainingRuns = textRuns; |
|
|
|
remainingRuns.Clear(); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
remainingRuns = new List<TextRun>(); |
|
|
|
} |
|
|
|
|
|
|
|
if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null) |
|
|
|
for (var i = 0; i < postSplitRuns.Count; ++i) |
|
|
|
{ |
|
|
|
remainingRuns.Add(postSplitRuns[i]); |
|
|
|
} |
|
|
|
|
|
|
|
textLineBreak = new WrappingTextLineBreak(null, resolvedFlowDirection, remainingRuns); |
|
|
|
} |
|
|
|
else if (currentLineBreak?.TextEndOfLine is { } textEndOfLine) |
|
|
|
{ |
|
|
|
textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); |
|
|
|
textLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
textLineBreak = null; |
|
|
|
} |
|
|
|
|
|
|
|
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, |
|
|
|
@ -833,6 +844,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
textLineBreak); |
|
|
|
|
|
|
|
textLine.FinalizeLine(); |
|
|
|
|
|
|
|
return textLine; |
|
|
|
} |
|
|
|
finally |
|
|
|
|