diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
index f5d39e4371..499026e8b3 100644
--- a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
@@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
- return CharacterBufferReference.CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
+ return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
}
}
@@ -157,8 +157,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Gets a span from the character buffer range
///
- public ReadOnlySpan Span =>
- CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
+ public ReadOnlySpan Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length);
///
/// Gets the character memory buffer
@@ -174,7 +173,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Indicate whether the character buffer range is empty
///
- internal bool IsEmpty => CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0;
+ internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0;
internal CharacterBufferRange Take(int length)
{
@@ -208,9 +207,7 @@ namespace Avalonia.Media.TextFormatting
return new CharacterBufferRange(new CharacterBufferReference(), 0);
}
- var characterBufferReference = new CharacterBufferReference(
- CharacterBufferReference.CharacterBuffer,
- CharacterBufferReference.OffsetToFirstChar + length);
+ var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length);
return new CharacterBufferRange(characterBufferReference, Length - length);
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
index f677617b14..01804e1ce3 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
@@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line.
///
/// Text line to collapse.
- public abstract List? Collapse(TextLine textLine);
+ public abstract List? Collapse(TextLine textLine);
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
index 086ea85d97..9c201bda22 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
@@ -5,9 +5,11 @@ namespace Avalonia.Media.TextFormatting
{
internal static class TextEllipsisHelper
{
- public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
+ public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{
- if (textLine.TextRuns is not List textRuns || textRuns.Count == 0)
+ var textRuns = textLine.TextRuns;
+
+ if (textRuns == null || textRuns.Count == 0)
{
return null;
}
@@ -20,7 +22,7 @@ namespace Avalonia.Media.TextFormatting
if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{
//Not enough space to fit in the symbol
- return new List(0);
+ return new List(0);
}
var availableWidth = properties.Width - shapedSymbol.Size.Width;
@@ -70,11 +72,11 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength;
- var collapsedRuns = new List(textRuns.Count);
+ var collapsedRuns = new List(textRuns.Count);
if (collapsedLength > 0)
{
- var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+ var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
@@ -84,22 +86,21 @@ namespace Avalonia.Media.TextFormatting
return collapsedRuns;
}
- availableWidth -= currentRun.Size.Width;
-
+ availableWidth -= shapedRun.Size.Width;
break;
}
- case { } drawableRun:
+ case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
- var collapsedRuns = new List(textRuns.Count);
+ var collapsedRuns = new List(textRuns.Count);
if (collapsedLength > 0)
{
- var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+ var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
@@ -109,6 +110,8 @@ namespace Avalonia.Media.TextFormatting
return collapsedRuns;
}
+ availableWidth -= drawableRun.Size.Width;
+
break;
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index ef2abdfea0..989bf7749d 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -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 drawableTextRuns;
+ IReadOnlyList 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
/// The text run's.
/// The length to split at.
/// The split text runs.
- internal static SplitResult> SplitDrawableRuns(List textRuns, int length)
+ internal static SplitResult> SplitTextRuns(IReadOnlyList 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(firstCount);
+ var first = new List(firstCount);
if (firstCount > 1)
{
@@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Length == length)
{
- var second = secondCount > 0 ? new List(secondCount) : null;
+ var second = secondCount > 0 ? new List(secondCount) : null;
if (second != null)
{
@@ -116,13 +115,13 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun);
- return new SplitResult>(first, second);
+ return new SplitResult>(first, second);
}
else
{
secondCount++;
- var second = new List(secondCount);
+ var second = new List(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>(first, second);
+ return new SplitResult>(first, second);
}
}
- return new SplitResult>(textRuns, null);
+ return new SplitResult>(textRuns, null);
}
///
@@ -154,11 +153,11 @@ namespace Avalonia.Media.TextFormatting
///
/// A list of shaped text characters.
///
- private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties,
+ private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties,
out FlowDirection resolvedFlowDirection)
{
var flowDirection = paragraphProperties.FlowDirection;
- var drawableTextRuns = new List();
+ var shapedRuns = new List();
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(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 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 textRuns, double paragraphWidth, out int measuredLength)
+ private static bool TryMeasureLength(IReadOnlyList 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
///
/// The current line break if the line was explicitly broken.
/// The wrapped text line.
- private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex,
+ private static TextLineImpl PerformTextWrapping(IReadOnlyList 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)
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index f803001481..ef0c726793 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -448,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
- if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+ if(textLine == null || textLine.Length == 0)
{
if (previousLine != null && previousLine.NewLineLength > 0)
{
@@ -501,6 +501,11 @@ namespace Avalonia.Media.TextFormatting
break;
}
+
+ if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+ {
+ break;
+ }
}
//Make sure the TextLayout always contains at least on empty line
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
index 7b80d5ce40..e30a0fe9f4 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
@@ -39,9 +39,11 @@ namespace Avalonia.Media.TextFormatting
///
public override TextRun Symbol { get; }
- public override List? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
- if (textLine.TextRuns is not List textRuns || textRuns.Count == 0)
+ var textRuns = textLine.TextRuns;
+
+ if (textRuns == null || textRuns.Count == 0)
{
return null;
}
@@ -52,7 +54,7 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width)
{
- return new List(0);
+ return new List(0);
}
// Overview of ellipsis structure
@@ -66,92 +68,101 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextRun shapedRun:
- {
- currentWidth += currentRun.Size.Width;
-
- if (currentWidth > availableWidth)
{
- shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
-
- var collapsedRuns = new List(textRuns.Count);
+ currentWidth += shapedRun.Size.Width;
- if (measuredLength > 0)
+ if (currentWidth > availableWidth)
{
- List? preSplitRuns = null;
- List? postSplitRuns;
-
- if (_prefixLength > 0)
- {
- var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns,
- Math.Min(_prefixLength, measuredLength));
+ shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
- collapsedRuns.AddRange(splitResult.First);
+ var collapsedRuns = new List(textRuns.Count);
- preSplitRuns = splitResult.First;
- postSplitRuns = splitResult.Second;
- }
- else
+ if (measuredLength > 0)
{
- postSplitRuns = textRuns;
- }
+ IReadOnlyList? preSplitRuns = null;
+ IReadOnlyList? postSplitRuns;
- collapsedRuns.Add(shapedSymbol);
+ if (_prefixLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitTextRuns(textRuns,
+ Math.Min(_prefixLength, measuredLength));
- if (measuredLength <= _prefixLength || postSplitRuns is null)
- {
- return collapsedRuns;
- }
+ collapsedRuns.AddRange(splitResult.First);
- var availableSuffixWidth = availableWidth;
+ preSplitRuns = splitResult.First;
+ postSplitRuns = splitResult.Second;
+ }
+ else
+ {
+ postSplitRuns = textRuns;
+ }
- if (preSplitRuns is not null)
- {
- foreach (var run in preSplitRuns)
+ collapsedRuns.Add(shapedSymbol);
+
+ if (measuredLength <= _prefixLength || postSplitRuns is null)
{
- availableSuffixWidth -= run.Size.Width;
+ return collapsedRuns;
}
- }
- for (var i = postSplitRuns.Count - 1; i >= 0; i--)
- {
- var run = postSplitRuns[i];
+ var availableSuffixWidth = availableWidth;
- switch (run)
+ if (preSplitRuns is not null)
{
- case ShapedTextRun endShapedRun:
+ foreach (var run in preSplitRuns)
{
- if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
- out var suffixCount, out var suffixWidth))
+ if (run is DrawableTextRun drawableTextRun)
{
- availableSuffixWidth -= suffixWidth;
+ availableSuffixWidth -= drawableTextRun.Size.Width;
+ }
+ }
+ }
- if (suffixCount > 0)
+ for (var i = postSplitRuns.Count - 1; i >= 0; i--)
+ {
+ var run = postSplitRuns[i];
+
+ switch (run)
+ {
+ case ShapedTextRun endShapedRun:
{
- var splitSuffix =
- endShapedRun.Split(run.Length - suffixCount);
+ if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
+ out var suffixCount, out var suffixWidth))
+ {
+ availableSuffixWidth -= suffixWidth;
- collapsedRuns.Add(splitSuffix.Second!);
- }
- }
+ if (suffixCount > 0)
+ {
+ var splitSuffix =
+ endShapedRun.Split(run.Length - suffixCount);
+
+ collapsedRuns.Add(splitSuffix.Second!);
+ }
+ }
- break;
+ break;
+ }
}
}
}
- }
- else
- {
- collapsedRuns.Add(shapedSymbol);
+ else
+ {
+ collapsedRuns.Add(shapedSymbol);
+ }
+
+ return collapsedRuns;
}
- return collapsedRuns;
- }
+ availableWidth -= shapedRun.Size.Width;
- break;
- }
- }
+ break;
+ }
+ case DrawableTextRun drawableTextRun:
+ {
+ availableWidth -= drawableTextRun.Size.Width;
- availableWidth -= currentRun.Size.Width;
+ break;
+ }
+ }
runIndex++;
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs
index ce35e47fbd..bf26ac5df4 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs
@@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting
public class TextLineBreak
{
public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight,
- IReadOnlyList? remainingRuns = null)
+ IReadOnlyList? remainingRuns = null)
{
TextEndOfLine = textEndOfLine;
FlowDirection = flowDirection;
@@ -25,6 +25,6 @@ namespace Avalonia.Media.TextFormatting
///
/// Get the remaining runs that were split up by the during the formatting process.
///
- public IReadOnlyList? RemainingRuns { get; }
+ public IReadOnlyList? RemainingRuns { get; }
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index 5fb1171221..a1f93bcd07 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -6,13 +6,13 @@ namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
- private readonly List _textRuns;
+ private IReadOnlyList _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _resolvedFlowDirection;
- public TextLineImpl(List textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
+ public TextLineImpl(IReadOnlyList textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{
@@ -86,11 +86,14 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in _textRuns)
{
- var offsetY = GetBaselineOffset(this, textRun);
+ if (textRun is DrawableTextRun drawable)
+ {
+ var offsetY = GetBaselineOffset(this, drawable);
- textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
+ drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY));
- currentX += textRun.Size.Width;
+ currentX += drawable.Size.Width;
+ }
}
}
@@ -180,7 +183,14 @@ namespace Avalonia.Media.TextFormatting
{
var lastRun = _textRuns[_textRuns.Count - 1];
- return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
+ var size = 0.0;
+
+ if (lastRun is DrawableTextRun drawableTextRun)
+ {
+ size = drawableTextRun.Size.Width;
+ }
+
+ return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size);
}
// process hit that happens within the line
@@ -220,9 +230,16 @@ namespace Avalonia.Media.TextFormatting
currentRun = _textRuns[j];
- if (currentDistance + currentRun.Size.Width <= distance)
+ if(currentRun is not ShapedTextRun)
+ {
+ continue;
+ }
+
+ shapedRun = (ShapedTextRun)currentRun;
+
+ if (currentDistance + shapedRun.Size.Width <= distance)
{
- currentDistance += currentRun.Size.Width;
+ currentDistance += shapedRun.Size.Width;
currentPosition -= currentRun.Length;
continue;
@@ -234,12 +251,19 @@ namespace Avalonia.Media.TextFormatting
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
- if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
+ if (currentRun is DrawableTextRun drawableTextRun)
{
- currentDistance += currentRun.Size.Width;
+ if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance)
+ {
+ currentDistance += drawableTextRun.Size.Width;
- currentPosition += currentRun.Length;
+ currentPosition += currentRun.Length;
+ continue;
+ }
+ }
+ else
+ {
continue;
}
@@ -249,7 +273,7 @@ namespace Avalonia.Media.TextFormatting
return characterHit;
}
- private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance)
+ private static CharacterHit GetRunCharacterHit(TextRun run, int currentPosition, double distance)
{
CharacterHit characterHit;
@@ -270,9 +294,9 @@ namespace Avalonia.Media.TextFormatting
break;
}
- default:
+ case DrawableTextRun drawableTextRun:
{
- if (distance < run.Size.Width / 2)
+ if (distance < drawableTextRun.Size.Width / 2)
{
characterHit = new CharacterHit(currentPosition);
}
@@ -282,6 +306,10 @@ namespace Avalonia.Media.TextFormatting
}
break;
}
+ default:
+ characterHit = new CharacterHit(currentPosition, run.Length);
+
+ break;
}
return characterHit;
@@ -307,7 +335,7 @@ namespace Avalonia.Media.TextFormatting
{
var i = index;
- var rightToLeftWidth = currentRun.Size.Width;
+ var rightToLeftWidth = shapedRun.Size.Width;
while (i + 1 <= _textRuns.Count - 1)
{
@@ -317,7 +345,7 @@ namespace Avalonia.Media.TextFormatting
{
i++;
- rightToLeftWidth += nextRun.Size.Width;
+ rightToLeftWidth += nextShapedRun.Size.Width;
continue;
}
@@ -331,7 +359,10 @@ namespace Avalonia.Media.TextFormatting
{
currentRun = _textRuns[i];
- rightToLeftWidth -= currentRun.Size.Width;
+ if (currentRun is DrawableTextRun drawable)
+ {
+ rightToLeftWidth -= drawable.Size.Width;
+ }
if (currentPosition + currentRun.Length >= characterIndex)
{
@@ -355,8 +386,13 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, currentDistance + distance);
}
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentDistance += drawableTextRun.Size.Width;
+ }
+
//No hit hit found so we add the full width
- currentDistance += currentRun.Size.Width;
+
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
@@ -380,8 +416,12 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, currentDistance - distance);
}
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentDistance -= drawableTextRun.Size.Width;
+ }
+
//No hit hit found so we add the full width
- currentDistance -= currentRun.Size.Width;
currentPosition += currentRun.Length;
remainingLength -= currentRun.Length;
}
@@ -391,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
}
private static bool TryGetDistanceFromCharacterHit(
- DrawableTextRun currentRun,
+ TextRun currentRun,
CharacterHit characterHit,
int currentPosition,
int remainingLength,
@@ -432,7 +472,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
- default:
+ case DrawableTextRun drawableTextRun:
{
if (characterIndex == currentPosition)
{
@@ -441,7 +481,7 @@ namespace Avalonia.Media.TextFormatting
if (characterIndex == currentPosition + currentRun.Length)
{
- distance = currentRun.Size.Width;
+ distance = drawableTextRun.Size.Width;
return true;
@@ -449,6 +489,10 @@ namespace Avalonia.Media.TextFormatting
break;
}
+ default:
+ {
+ return false;
+ }
}
return false;
@@ -943,7 +987,7 @@ namespace Avalonia.Media.TextFormatting
return this;
}
- private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
+ private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
{
if (run is ShapedTextRun shapedTextCharacters)
{
@@ -1039,16 +1083,18 @@ namespace Avalonia.Media.TextFormatting
minLevelToReverse--;
}
- _textRuns.Clear();
+ var textRuns = new List(_textRuns.Count);
current = orderedRun;
while (current != null)
{
- _textRuns.Add(current.Run);
+ textRuns.Add(current.Run);
current = current.Next;
}
+
+ _textRuns = textRuns;
}
///
@@ -1286,7 +1332,7 @@ namespace Avalonia.Media.TextFormatting
{
var runIndex = 0;
textPosition = FirstTextSourceIndex;
- DrawableTextRun? previousRun = null;
+ TextRun? previousRun = null;
while (runIndex < _textRuns.Count)
{
@@ -1346,7 +1392,6 @@ namespace Avalonia.Media.TextFormatting
break;
}
-
default:
{
if (codepointIndex == textPosition)
@@ -1363,6 +1408,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
+
}
runIndex++;
@@ -1436,7 +1482,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
- case { } drawableTextRun:
+ case DrawableTextRun drawableTextRun:
{
widthIncludingWhitespace += drawableTextRun.Size.Width;
@@ -1558,7 +1604,7 @@ namespace Avalonia.Media.TextFormatting
private sealed class OrderedBidiRun
{
- public OrderedBidiRun(DrawableTextRun run, sbyte level)
+ public OrderedBidiRun(TextRun run, sbyte level)
{
Run = run;
Level = level;
@@ -1566,7 +1612,7 @@ namespace Avalonia.Media.TextFormatting
public sbyte Level { get; }
- public DrawableTextRun Run { get; }
+ public TextRun Run { get; }
public OrderedBidiRun? Next { get; set; }
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs
index e79c2ed8b3..56232ec9c8 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs
@@ -40,11 +40,11 @@ namespace Avalonia.Media.TextFormatting
{
unsafe
{
- var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
+ var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length);
fixed (char* charsPtr = characterBuffer.Span)
{
- return new string(charsPtr, _textRun.CharacterBufferReference.OffsetToFirstChar, _textRun.Length);
+ return new string(charsPtr, 0, _textRun.Length);
}
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
index 1de04ad061..deecbbe476 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
@@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
///
public override TextRun Symbol { get; }
- public override List? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, false);
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
index 7c94715aa4..c291e1dfb9 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
@@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting
///
public override TextRun Symbol { get; }
- public override List? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, true);
}
diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs
index 5d7b8998e6..f64cf79127 100644
--- a/src/Avalonia.Controls/Documents/Run.cs
+++ b/src/Avalonia.Controls/Documents/Run.cs
@@ -54,6 +54,11 @@ namespace Avalonia.Controls.Documents
{
var text = Text ?? "";
+ if (string.IsNullOrEmpty(text))
+ {
+ return;
+ }
+
var textRunProperties = CreateTextRunProperties();
var textCharacters = new TextCharacters(text, textRunProperties);
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
index aa499bb135..81d7b1854b 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
@@ -1,6 +1,4 @@
-using System;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
+using Avalonia.Media.TextFormatting;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 7fc27b01f4..1b6fd537eb 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -62,6 +62,69 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ private class TextSourceWithDummyRuns : ITextSource
+ {
+ private readonly TextRunProperties _properties;
+ private readonly List> _textRuns;
+
+ public TextSourceWithDummyRuns(TextRunProperties properties)
+ {
+ _properties = properties;
+
+ _textRuns = new List>
+ {
+ new ValueSpan(0, 5, new TextCharacters("Hello", _properties)),
+ new ValueSpan(5, 1, new DummyRun()),
+ new ValueSpan(6, 1, new DummyRun()),
+ new ValueSpan(7, 6, new TextCharacters(" World", _properties))
+ };
+ }
+
+ public TextRun GetTextRun(int textSourceIndex)
+ {
+ foreach (var run in _textRuns)
+ {
+ if (textSourceIndex < run.Start + run.Length)
+ {
+ return run.Value;
+ }
+ }
+
+ return new TextEndOfParagraph();
+ }
+
+ private class DummyRun : TextRun
+ {
+ public DummyRun()
+ {
+ Length = DefaultTextSourceLength;
+ }
+
+ public override int Length { get; }
+ }
+ }
+
+ [Fact]
+ public void Should_Format_TextLine_With_Non_Text_TextRuns()
+ {
+ using (Start())
+ {
+ var defaultProperties =
+ new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
+
+ var textSource = new TextSourceWithDummyRuns(defaultProperties);
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ Assert.Equal(5, textLine.TextRuns.Count);
+
+ Assert.Equal(14, textLine.Length);
+ }
+ }
+
[Fact]
public void Should_Format_TextRuns_With_TextRunStyles()
{
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index ac2467407b..2c6ccfa896 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -326,7 +326,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
+ var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length));
+
+ Assert.Equal(currentDistance, actualDistance);
}
}