|
|
|
@ -1,6 +1,5 @@ |
|
|
|
using System; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Linq; |
|
|
|
using Avalonia.Media.TextFormatting.Unicode; |
|
|
|
using Avalonia.Utilities; |
|
|
|
|
|
|
|
@ -17,20 +16,20 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
var textWrapping = paragraphProperties.TextWrapping; |
|
|
|
FlowDirection resolvedFlowDirection; |
|
|
|
TextLineBreak? nextLineBreak = null; |
|
|
|
List<DrawableTextRun> drawableTextRuns; |
|
|
|
IReadOnlyList<TextRun> textRuns; |
|
|
|
|
|
|
|
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, |
|
|
|
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, |
|
|
|
out var textEndOfLine, out var textSourceLength); |
|
|
|
|
|
|
|
if (previousLineBreak?.RemainingRuns != null) |
|
|
|
{ |
|
|
|
resolvedFlowDirection = previousLineBreak.FlowDirection; |
|
|
|
drawableTextRuns = previousLineBreak.RemainingRuns.ToList(); |
|
|
|
textRuns = previousLineBreak.RemainingRuns; |
|
|
|
nextLineBreak = previousLineBreak; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection); |
|
|
|
textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); |
|
|
|
|
|
|
|
if (nextLineBreak == null && textEndOfLine != null) |
|
|
|
{ |
|
|
|
@ -44,7 +43,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
{ |
|
|
|
case TextWrapping.NoWrap: |
|
|
|
{ |
|
|
|
textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength, |
|
|
|
textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, |
|
|
|
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); |
|
|
|
|
|
|
|
textLine.FinalizeLine(); |
|
|
|
@ -54,7 +53,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
case TextWrapping.WrapWithOverflow: |
|
|
|
case TextWrapping.Wrap: |
|
|
|
{ |
|
|
|
textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, |
|
|
|
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, |
|
|
|
resolvedFlowDirection, nextLineBreak); |
|
|
|
break; |
|
|
|
} |
|
|
|
@ -71,7 +70,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// <param name="textRuns">The text run's.</param>
|
|
|
|
/// <param name="length">The length to split at.</param>
|
|
|
|
/// <returns>The split text runs.</returns>
|
|
|
|
internal static SplitResult<List<DrawableTextRun>> SplitDrawableRuns(List<DrawableTextRun> textRuns, int length) |
|
|
|
internal static SplitResult<IReadOnlyList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length) |
|
|
|
{ |
|
|
|
var currentLength = 0; |
|
|
|
|
|
|
|
@ -88,7 +87,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
var firstCount = currentRun.Length >= 1 ? i + 1 : i; |
|
|
|
|
|
|
|
var first = new List<DrawableTextRun>(firstCount); |
|
|
|
var first = new List<TextRun>(firstCount); |
|
|
|
|
|
|
|
if (firstCount > 1) |
|
|
|
{ |
|
|
|
@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
if (currentLength + currentRun.Length == length) |
|
|
|
{ |
|
|
|
var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null; |
|
|
|
var second = secondCount > 0 ? new List<TextRun>(secondCount) : null; |
|
|
|
|
|
|
|
if (second != null) |
|
|
|
{ |
|
|
|
@ -116,13 +115,13 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
first.Add(currentRun); |
|
|
|
|
|
|
|
return new SplitResult<List<DrawableTextRun>>(first, second); |
|
|
|
return new SplitResult<IReadOnlyList<TextRun>>(first, second); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
secondCount++; |
|
|
|
|
|
|
|
var second = new List<DrawableTextRun>(secondCount); |
|
|
|
var second = new List<TextRun>(secondCount); |
|
|
|
|
|
|
|
if (currentRun is ShapedTextRun shapedTextCharacters) |
|
|
|
{ |
|
|
|
@ -131,18 +130,18 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
first.Add(split.First); |
|
|
|
|
|
|
|
second.Add(split.Second!); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
for (var j = 1; j < secondCount; j++) |
|
|
|
{ |
|
|
|
second.Add(textRuns[i + j]); |
|
|
|
} |
|
|
|
|
|
|
|
return new SplitResult<List<DrawableTextRun>>(first, second); |
|
|
|
return new SplitResult<IReadOnlyList<TextRun>>(first, second); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return new SplitResult<List<DrawableTextRun>>(textRuns, null); |
|
|
|
return new SplitResult<IReadOnlyList<TextRun>>(textRuns, null); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@ -154,11 +153,11 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// <returns>
|
|
|
|
/// A list of shaped text characters.
|
|
|
|
/// </returns>
|
|
|
|
private static List<DrawableTextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties, |
|
|
|
private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties, |
|
|
|
out FlowDirection resolvedFlowDirection) |
|
|
|
{ |
|
|
|
var flowDirection = paragraphProperties.FlowDirection; |
|
|
|
var drawableTextRuns = new List<DrawableTextRun>(); |
|
|
|
var shapedRuns = new List<TextRun>(); |
|
|
|
var biDiData = new BidiData((sbyte)flowDirection); |
|
|
|
|
|
|
|
foreach (var textRun in textRuns) |
|
|
|
@ -199,13 +198,6 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
switch (currentRun) |
|
|
|
{ |
|
|
|
case DrawableTextRun drawableRun: |
|
|
|
{ |
|
|
|
drawableTextRuns.Add(drawableRun); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
case UnshapedTextRun shapeableRun: |
|
|
|
{ |
|
|
|
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun }; |
|
|
|
@ -245,17 +237,23 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, |
|
|
|
currentRun.Properties.FontRenderingEmSize, |
|
|
|
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, |
|
|
|
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, |
|
|
|
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); |
|
|
|
|
|
|
|
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); |
|
|
|
shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions)); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
default: |
|
|
|
{ |
|
|
|
shapedRuns.Add(currentRun); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return drawableTextRuns; |
|
|
|
return shapedRuns; |
|
|
|
} |
|
|
|
|
|
|
|
private static IReadOnlyList<ShapedTextRun> ShapeTogether( |
|
|
|
@ -390,6 +388,10 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
if (textRun == null) |
|
|
|
{ |
|
|
|
textRuns.Add(new TextEndOfParagraph()); |
|
|
|
|
|
|
|
textSourceLength += TextRun.DefaultTextSourceLength; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
@ -465,7 +467,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, double paragraphWidth, out int measuredLength) |
|
|
|
private static bool TryMeasureLength(IReadOnlyList<TextRun> textRuns, double paragraphWidth, out int measuredLength) |
|
|
|
{ |
|
|
|
measuredLength = 0; |
|
|
|
var currentWidth = 0.0; |
|
|
|
@ -476,7 +478,7 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
{ |
|
|
|
case ShapedTextRun shapedTextCharacters: |
|
|
|
{ |
|
|
|
if(shapedTextCharacters.ShapedBuffer.Length > 0) |
|
|
|
if (shapedTextCharacters.ShapedBuffer.Length > 0) |
|
|
|
{ |
|
|
|
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; |
|
|
|
var lastCluster = firstCluster; |
|
|
|
@ -497,12 +499,12 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
|
|
|
|
measuredLength += currentRun.Length; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
case { } drawableTextRun: |
|
|
|
case DrawableTextRun drawableTextRun: |
|
|
|
{ |
|
|
|
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) |
|
|
|
{ |
|
|
|
@ -510,14 +512,20 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
} |
|
|
|
|
|
|
|
measuredLength += currentRun.Length; |
|
|
|
currentWidth += currentRun.Size.Width; |
|
|
|
currentWidth += drawableTextRun.Size.Width; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
default: |
|
|
|
{ |
|
|
|
measuredLength += currentRun.Length; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
found: |
|
|
|
found: |
|
|
|
|
|
|
|
return measuredLength != 0; |
|
|
|
} |
|
|
|
@ -553,13 +561,13 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
/// <param name="resolvedFlowDirection"></param>
|
|
|
|
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
|
|
|
|
/// <returns>The wrapped text line.</returns>
|
|
|
|
private static TextLineImpl PerformTextWrapping(List<DrawableTextRun> textRuns, int firstTextSourceIndex, |
|
|
|
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, |
|
|
|
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, |
|
|
|
TextLineBreak? currentLineBreak) |
|
|
|
{ |
|
|
|
if(textRuns.Count == 0) |
|
|
|
if (textRuns.Count == 0) |
|
|
|
{ |
|
|
|
return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties); |
|
|
|
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); |
|
|
|
} |
|
|
|
|
|
|
|
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) |
|
|
|
@ -575,46 +583,24 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
|
|
|
|
for (var index = 0; index < textRuns.Count; index++) |
|
|
|
{ |
|
|
|
var currentRun = textRuns[index]; |
|
|
|
|
|
|
|
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); |
|
|
|
|
|
|
|
var lineBreaker = new LineBreakEnumerator(runText); |
|
|
|
|
|
|
|
var breakFound = false; |
|
|
|
|
|
|
|
while (lineBreaker.MoveNext()) |
|
|
|
{ |
|
|
|
if (lineBreaker.Current.Required && |
|
|
|
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) |
|
|
|
{ |
|
|
|
//Explicit break found
|
|
|
|
breakFound = true; |
|
|
|
|
|
|
|
currentPosition = currentLength + lineBreaker.Current.PositionWrap; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
var currentRun = textRuns[index]; |
|
|
|
|
|
|
|
if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) |
|
|
|
{ |
|
|
|
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) |
|
|
|
switch (currentRun) |
|
|
|
{ |
|
|
|
case ShapedTextRun: |
|
|
|
{ |
|
|
|
if (lastWrapPosition > 0) |
|
|
|
{ |
|
|
|
currentPosition = lastWrapPosition; |
|
|
|
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); |
|
|
|
|
|
|
|
breakFound = true; |
|
|
|
var lineBreaker = new LineBreakEnumerator(runText); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
//Find next possible wrap position (overflow)
|
|
|
|
if (index < textRuns.Count - 1) |
|
|
|
while (lineBreaker.MoveNext()) |
|
|
|
{ |
|
|
|
if (lineBreaker.Current.PositionWrap != currentRun.Length) |
|
|
|
if (lineBreaker.Current.Required && |
|
|
|
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) |
|
|
|
{ |
|
|
|
//We already found the next possible wrap position.
|
|
|
|
//Explicit break found
|
|
|
|
breakFound = true; |
|
|
|
|
|
|
|
currentPosition = currentLength + lineBreaker.Current.PositionWrap; |
|
|
|
@ -622,51 +608,81 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
while (lineBreaker.MoveNext() && index < textRuns.Count) |
|
|
|
if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) |
|
|
|
{ |
|
|
|
currentPosition += lineBreaker.Current.PositionWrap; |
|
|
|
|
|
|
|
if (lineBreaker.Current.PositionWrap != currentRun.Length) |
|
|
|
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
if (lastWrapPosition > 0) |
|
|
|
{ |
|
|
|
currentPosition = lastWrapPosition; |
|
|
|
|
|
|
|
index++; |
|
|
|
breakFound = true; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
//Find next possible wrap position (overflow)
|
|
|
|
if (index < textRuns.Count - 1) |
|
|
|
{ |
|
|
|
if (lineBreaker.Current.PositionWrap != currentRun.Length) |
|
|
|
{ |
|
|
|
//We already found the next possible wrap position.
|
|
|
|
breakFound = true; |
|
|
|
|
|
|
|
currentPosition = currentLength + lineBreaker.Current.PositionWrap; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
while (lineBreaker.MoveNext() && index < textRuns.Count) |
|
|
|
{ |
|
|
|
currentPosition += lineBreaker.Current.PositionWrap; |
|
|
|
|
|
|
|
if (lineBreaker.Current.PositionWrap != currentRun.Length) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
index++; |
|
|
|
|
|
|
|
if (index >= textRuns.Count) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
currentRun = textRuns[index]; |
|
|
|
|
|
|
|
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); |
|
|
|
|
|
|
|
lineBreaker = new LineBreakEnumerator(runText); |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
currentPosition = currentLength + lineBreaker.Current.PositionWrap; |
|
|
|
} |
|
|
|
|
|
|
|
breakFound = true; |
|
|
|
|
|
|
|
if (index >= textRuns.Count) |
|
|
|
{ |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
currentRun = textRuns[index]; |
|
|
|
//We overflowed so we use the last available wrap position.
|
|
|
|
currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition; |
|
|
|
|
|
|
|
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length); |
|
|
|
breakFound = true; |
|
|
|
|
|
|
|
lineBreaker = new LineBreakEnumerator(runText); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
currentPosition = currentLength + lineBreaker.Current.PositionWrap; |
|
|
|
} |
|
|
|
|
|
|
|
breakFound = true; |
|
|
|
if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) |
|
|
|
{ |
|
|
|
lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
//We overflowed so we use the last available wrap position.
|
|
|
|
currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition; |
|
|
|
|
|
|
|
breakFound = true; |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) |
|
|
|
{ |
|
|
|
lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!breakFound) |
|
|
|
@ -681,12 +697,12 @@ namespace Avalonia.Media.TextFormatting |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
var splitResult = SplitDrawableRuns(textRuns, measuredLength); |
|
|
|
var splitResult = SplitTextRuns(textRuns, measuredLength); |
|
|
|
|
|
|
|
var remainingCharacters = splitResult.Second; |
|
|
|
|
|
|
|
var lineBreak = remainingCharacters?.Count > 0 ? |
|
|
|
new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) : |
|
|
|
new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : |
|
|
|
null; |
|
|
|
|
|
|
|
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) |
|
|
|
|