diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs
index 0bab473442..28757b1a1d 100644
--- a/src/Avalonia.Base/Media/FormattedText.cs
+++ b/src/Avalonia.Base/Media/FormattedText.cs
@@ -741,6 +741,11 @@ namespace Avalonia.Media
null // no previous line break
);
+ if(Current is null)
+ {
+ return false;
+ }
+
// check if this line fits the text height
if (_totalHeight + Current.Height > _that._maxTextHeight)
{
@@ -779,7 +784,7 @@ namespace Avalonia.Media
// maybe there is no next line at all
if (Position + Current.Length < _that._text.Length)
{
- bool nextLineFits;
+ bool nextLineFits = false;
if (_lineCount + 1 >= _that._maxLineCount)
{
@@ -795,7 +800,10 @@ namespace Avalonia.Media
currentLineBreak
);
- nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
+ if(_nextLine != null)
+ {
+ nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
+ }
}
if (!nextLineFits)
@@ -819,16 +827,22 @@ namespace Avalonia.Media
_previousLineBreak
);
- currentLineBreak = Current.TextLineBreak;
+ if(Current != null)
+ {
+ currentLineBreak = Current.TextLineBreak;
+ }
_that._defaultParaProps.SetTextWrapping(currentWrap);
}
}
}
- _previousHeight = Current.Height;
+ if(Current != null)
+ {
+ _previousHeight = Current.Height;
- Length = Current.Length;
+ Length = Current.Length;
+ }
_previousLineBreak = currentLineBreak;
@@ -838,7 +852,7 @@ namespace Avalonia.Media
///
/// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed.
///
- private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
+ private TextLine? FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
{
var line = _formatter.FormatLine(
textSource,
@@ -848,7 +862,7 @@ namespace Avalonia.Media
lineBreak
);
- if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
+ if (line != null && _that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
{
// what I really need here is the last displayed text run of the line
// textSourcePosition + line.Length - 1 works except the end of paragraph case,
@@ -1601,11 +1615,11 @@ namespace Avalonia.Media
}
///
- public TextRun? GetTextRun(int textSourceCharacterIndex)
+ public TextRun GetTextRun(int textSourceCharacterIndex)
{
if (textSourceCharacterIndex >= _that._text.Length)
{
- return null;
+ return new TextEndOfParagraph();
}
var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);
diff --git a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
index 26966b37bc..32012ab8e9 100644
--- a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
@@ -1,6 +1,4 @@
-using Avalonia.Metadata;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
{
///
/// Produces objects that are used by the .
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
index 0b5d7649d7..ff8c1c4860 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
@@ -38,7 +38,7 @@
/// A value that specifies the text formatter state,
/// in terms of where the previous line in the paragraph was broken by the text formatting process.
/// The formatted line.
- public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+ public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null);
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 812c4e9eb8..7f74f49982 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
[ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm;
///
- public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+ public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
{
TextLineBreak? nextLineBreak = null;
@@ -41,6 +41,11 @@ namespace Avalonia.Media.TextFormatting
fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine,
out var textSourceLength);
+ if (fetchedRuns.Count == 0)
+ {
+ return null;
+ }
+
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
out var resolvedFlowDirection);
@@ -491,16 +496,7 @@ namespace Avalonia.Media.TextFormatting
while (textRunEnumerator.MoveNext())
{
- var textRun = textRunEnumerator.Current;
-
- if (textRun == null)
- {
- textRuns.Add(new TextEndOfParagraph());
-
- textSourceLength += TextRun.DefaultTextSourceLength;
-
- break;
- }
+ TextRun textRun = textRunEnumerator.Current!;
if (textRun is TextEndOfLine textEndOfLine)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 8e85c10bba..4dbc472133 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -238,7 +238,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textLine in _textLines)
{
//Current line isn't covered.
- if (textLine.FirstTextSourceIndex + textLine.Length < start)
+ if (textLine.FirstTextSourceIndex + textLine.Length <= start)
{
currentY += textLine.Height;
@@ -348,14 +348,36 @@ namespace Avalonia.Media.TextFormatting
{
var (x, y) = point;
- var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
-
var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
- if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+ var lastTrailingIndex = 0;
+
+ if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight)
{
- lastTrailingIndex -= textLine.NewLineLength;
+ lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
+
+ if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+ {
+ lastTrailingIndex -= textLine.NewLineLength;
+ }
+
+ if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine)
+ {
+ lastTrailingIndex -= textEndOfLine.Length;
+ }
}
+ else
+ {
+ if (x <= textLine.WidthIncludingTrailingWhitespace - textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+ {
+ lastTrailingIndex += textLine.NewLineLength;
+ }
+
+ if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine)
+ {
+ lastTrailingIndex += textEndOfLine.Length;
+ }
+ }
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
@@ -391,7 +413,7 @@ namespace Avalonia.Media.TextFormatting
///
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
- TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
+ TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
double letterSpacing)
{
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
@@ -456,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
- if (textLine.Length == 0)
+ if (textLine is null)
{
if (previousLine != null && previousLine.NewLineLength > 0)
{
@@ -518,7 +540,6 @@ namespace Avalonia.Media.TextFormatting
}
}
- //Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index d29063e07d..187b3154ad 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
+ private TextLineBreak? _textLineBreak;
private readonly FlowDirection _resolvedFlowDirection;
public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
@@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
{
FirstTextSourceIndex = firstTextSourceIndex;
Length = length;
- TextLineBreak = lineBreak;
+ _textLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
_textRuns = textRuns;
@@ -38,7 +39,7 @@ namespace Avalonia.Media.TextFormatting
public override int Length { get; }
///
- public override TextLineBreak? TextLineBreak { get; }
+ public override TextLineBreak? TextLineBreak => _textLineBreak;
///
public override bool HasCollapsed { get; }
@@ -167,50 +168,54 @@ namespace Avalonia.Media.TextFormatting
{
if (_textRuns.Length == 0)
{
- return new CharacterHit();
+ return new CharacterHit(FirstTextSourceIndex);
}
distance -= Start;
- var firstRunIndex = 0;
+ var lastIndex = _textRuns.Length - 1;
- if (_textRuns[firstRunIndex] is TextEndOfLine)
+ if (_textRuns[lastIndex] is TextEndOfLine)
{
- firstRunIndex++;
+ lastIndex--;
}
- if(firstRunIndex >= _textRuns.Length)
+ var currentPosition = FirstTextSourceIndex;
+
+ if (lastIndex < 0)
{
- return new CharacterHit(FirstTextSourceIndex);
+ return new CharacterHit(currentPosition);
}
if (distance <= 0)
{
- var firstRun = _textRuns[firstRunIndex];
+ var firstRun = _textRuns[0];
- return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0);
+ if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft)
+ {
+ currentPosition = Length - firstRun.Length;
+ }
+
+ return GetRunCharacterHit(firstRun, currentPosition, 0);
}
if (distance >= WidthIncludingTrailingWhitespace)
{
- var lastRun = _textRuns[_textRuns.Length - 1];
-
- var size = 0.0;
+ var lastRun = _textRuns[lastIndex];
- if (lastRun is DrawableTextRun drawableTextRun)
+ if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
- size = drawableTextRun.Size.Width;
+ currentPosition = Length - lastRun.Length;
}
- return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size);
+ return GetRunCharacterHit(lastRun, currentPosition, distance);
}
// process hit that happens within the line
var characterHit = new CharacterHit();
- var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0;
- for (var i = 0; i < _textRuns.Length; i++)
+ for (var i = 0; i <= lastIndex; i++)
{
var currentRun = _textRuns[i];
@@ -242,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
currentRun = _textRuns[j];
- if(currentRun is not ShapedTextRun)
+ if (currentRun is not ShapedTextRun)
{
continue;
}
@@ -274,10 +279,6 @@ namespace Avalonia.Media.TextFormatting
continue;
}
}
- else
- {
- continue;
- }
break;
}
@@ -422,10 +423,10 @@ namespace Avalonia.Media.TextFormatting
{
if (currentGlyphRun != null)
{
- distance = currentGlyphRun.Size.Width - distance;
+ currentDistance -= currentGlyphRun.Size.Width;
}
- return Math.Max(0, currentDistance - distance);
+ return currentDistance + distance;
}
if (currentRun is DrawableTextRun drawableTextRun)
@@ -575,386 +576,505 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
- private IReadOnlyList GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength)
+ public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength)
{
- var characterIndex = firstTextSourceIndex + textLength;
+ if (_textRuns.Length == 0)
+ {
+ return Array.Empty();
+ }
- var result = new List(_textRuns.Length);
- var lastDirection = FlowDirection.LeftToRight;
- var currentDirection = lastDirection;
+ var result = new List();
var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength;
- var startX = Start;
- double currentWidth = 0;
- var currentRect = default(Rect);
-
- TextRunBounds lastRunBounds = default;
-
- for (var index = 0; index < _textRuns.Length; index++)
+ static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
{
- if (_textRuns[index] is not DrawableTextRun currentRun)
+ if (textRun is ShapedTextRun shapedTextRun)
{
- continue;
+ return shapedTextRun.ShapedBuffer.IsLeftToRight ?
+ FlowDirection.LeftToRight :
+ FlowDirection.RightToLeft;
}
- var characterLength = 0;
- var endX = startX;
-
- TextRunBounds currentRunBounds;
+ return currentDirection;
+ }
- double combinedWidth;
+ if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
+ {
+ var currentX = Start;
- if (currentRun is ShapedTextRun currentShapedRun)
+ for (int i = 0; i < _textRuns.Length; i++)
{
- var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
+ var currentRun = _textRuns[i];
+
+ var firstRunIndex = i;
+ var lastRunIndex = firstRunIndex;
+ var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight);
+ var directionalWidth = 0.0;
- if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ if (currentRun is DrawableTextRun currentDrawable)
{
- startX += currentRun.Size.Width;
+ directionalWidth = currentDrawable.Size.Width;
+ }
- currentPosition += currentRun.Length;
+ // Find consecutive runs of same direction
+ for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
+ {
+ var nextRun = _textRuns[lastRunIndex + 1];
- continue;
+ var nextDirection = GetDirection(nextRun, currentDirection);
+
+ if (currentDirection != nextDirection)
+ {
+ break;
+ }
+
+ if (nextRun is DrawableTextRun nextDrawable)
+ {
+ directionalWidth += nextDrawable.Size.Width;
+ }
}
- if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+ //Skip runs that are not part of the hit test range
+ switch (currentDirection)
{
- var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
+ case FlowDirection.RightToLeft:
+ {
+ for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
+ {
+ currentRun = _textRuns[lastRunIndex];
+
+ if (currentPosition + currentRun.Length > firstTextSourceIndex)
+ {
+ break;
+ }
+
+ currentPosition += currentRun.Length;
- double startOffset;
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ directionalWidth -= drawableTextRun.Size.Width;
+ currentX += drawableTextRun.Size.Width;
+ }
- double endOffset;
+ if(lastRunIndex - 1 < 0)
+ {
+ break;
+ }
+ }
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ break;
+ }
+ default:
+ {
+ for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
+ {
+ currentRun = _textRuns[firstRunIndex];
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ if (currentPosition + currentRun.Length > firstTextSourceIndex)
+ {
+ break;
+ }
- startX += startOffset;
+ currentPosition += currentRun.Length;
- endX += endOffset;
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX += drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+ if(firstRunIndex + 1 == _textRuns.Length)
+ {
+ break;
+ }
+ }
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ break;
+ }
+ }
- characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+ i = lastRunIndex;
- currentDirection = FlowDirection.LeftToRight;
+ if (directionalWidth == 0)
+ {
+ continue;
}
- else
+
+ var coveredLength = 0;
+ TextBounds? textBounds = null;
+
+ switch (currentDirection)
{
- var rightToLeftIndex = index;
- var rightToLeftWidth = currentShapedRun.Size.Width;
- while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
- {
- if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+ case FlowDirection.RightToLeft:
{
+ textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
+ currentPosition, remainingLength, out coveredLength, out currentPosition);
+
+ currentX += directionalWidth;
+
break;
}
+ default:
+ {
+ textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
+ currentPosition, remainingLength, out coveredLength, out currentPosition);
- rightToLeftIndex++;
-
- rightToLeftWidth += nextShapedRun.Size.Width;
+ currentX = textBounds.Rectangle.Right;
- if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
- {
break;
}
+ }
- currentShapedRun = nextShapedRun;
- }
+ if (coveredLength > 0)
+ {
+ result.Add(textBounds);
+
+ remainingLength -= coveredLength;
+ }
+
+ if (remainingLength <= 0)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ var currentX = Start + WidthIncludingTrailingWhitespace;
- startX += rightToLeftWidth;
+ for (int i = _textRuns.Length - 1; i >= 0; i--)
+ {
+ var currentRun = _textRuns[i];
+ var firstRunIndex = i;
+ var lastRunIndex = firstRunIndex;
+ var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft);
+ var directionalWidth = 0.0;
- currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+ if (currentRun is DrawableTextRun currentDrawable)
+ {
+ directionalWidth = currentDrawable.Size.Width;
+ }
- remainingLength -= currentRunBounds.Length;
- currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
- endX = currentRunBounds.Rectangle.Right;
- startX = currentRunBounds.Rectangle.Left;
+ // Find consecutive runs of same direction
+ for (; firstRunIndex - 1 > 0; firstRunIndex--)
+ {
+ var previousRun = _textRuns[firstRunIndex - 1];
- var rightToLeftRunBounds = new List { currentRunBounds };
+ var previousDirection = GetDirection(previousRun, currentDirection);
- for (int i = rightToLeftIndex - 1; i >= index; i--)
+ if (currentDirection != previousDirection)
{
- if (_textRuns[i] is not ShapedTextRun shapedRun)
+ break;
+ }
+
+ if (currentRun is DrawableTextRun previousDrawable)
+ {
+ directionalWidth += previousDrawable.Size.Width;
+ }
+ }
+
+ //Skip runs that are not part of the hit test range
+ switch (currentDirection)
+ {
+ case FlowDirection.RightToLeft:
{
- continue;
- }
+ for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
+ {
+ currentRun = _textRuns[lastRunIndex];
- currentShapedRun = shapedRun;
+ if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ {
+ currentPosition += currentRun.Length;
- currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX -= drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- rightToLeftRunBounds.Insert(0, currentRunBounds);
+ continue;
+ }
- remainingLength -= currentRunBounds.Length;
- startX = currentRunBounds.Rectangle.Left;
+ break;
+ }
- currentPosition += currentRunBounds.Length;
- }
+ break;
+ }
+ default:
+ {
+ for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
+ {
+ currentRun = _textRuns[firstRunIndex];
- combinedWidth = endX - startX;
+ if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ {
+ currentPosition += currentRun.Length;
- currentRect = new Rect(startX, 0, combinedWidth, Height);
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX += drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- currentDirection = FlowDirection.RightToLeft;
+ continue;
+ }
- if (!MathUtilities.IsZero(combinedWidth))
- {
- result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
- }
+ break;
+ }
- startX = endX;
+ break;
+ }
}
- }
- else
- {
- if (currentPosition + currentRun.Length <= firstTextSourceIndex)
- {
- startX += currentRun.Size.Width;
- currentPosition += currentRun.Length;
+ i = firstRunIndex;
+ if (directionalWidth == 0)
+ {
continue;
}
- if (currentPosition < firstTextSourceIndex)
- {
- startX += currentRun.Size.Width;
- }
+ var coveredLength = 0;
- if (currentPosition + currentRun.Length <= characterIndex)
+ TextBounds? textBounds = null;
+
+ switch (currentDirection)
{
- endX += currentRun.Size.Width;
+ case FlowDirection.LeftToRight:
+ {
+ textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex,
+ currentPosition, remainingLength, out coveredLength, out currentPosition);
+
+ currentX -= directionalWidth;
- characterLength = currentRun.Length;
+ break;
+ }
+ default:
+ {
+ textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
+ currentPosition, remainingLength, out coveredLength, out currentPosition);
+
+ currentX = textBounds.Rectangle.Left;
+
+ break;
+ }
}
- }
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
- }
+ //Visual order is always left to right so we need to insert
+ result.Insert(0, textBounds);
- //Lines that only contain a linebreak need to be covered here
- if (characterLength == 0)
- {
- characterLength = NewLineLength;
+ remainingLength -= coveredLength;
+
+ if (remainingLength <= 0)
+ {
+ break;
+ }
}
+ }
- combinedWidth = endX - startX;
+ return result;
+ }
- currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
+ private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX,
+ int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
+ {
+ coveredLength = 0;
+ var textRunBounds = new List();
+ var startX = endX;
- currentPosition += characterLength;
+ for (int i = lastRunIndex; i >= firstRunIndex; i--)
+ {
+ var currentRun = _textRuns[i];
- remainingLength -= characterLength;
+ if (currentRun is ShapedTextRun shapedTextRun)
+ {
+ var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
- startX = endX;
+ textRunBounds.Insert(0, runBounds);
- if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
- {
- if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
+ if (offset > 0)
{
- currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
+ endX = runBounds.Rectangle.Right;
- var textBounds = result[result.Count - 1];
+ startX = endX;
+ }
- textBounds.Rectangle = currentRect;
+ startX -= runBounds.Rectangle.Width;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
+ currentPosition += runBounds.Length + offset;
+
+ coveredLength += runBounds.Length;
+
+ remainingLength -= runBounds.Length;
+ }
+ else
+ {
+ if (currentRun is DrawableTextRun drawableTextRun)
{
- currentRect = currentRunBounds.Rectangle;
+ startX -= drawableTextRun.Size.Width;
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ textRunBounds.Insert(0,
+ new TextRunBounds(
+ new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
}
- }
- lastRunBounds = currentRunBounds;
+ currentPosition += currentRun.Length;
+
+ coveredLength += currentRun.Length;
- currentWidth += combinedWidth;
+ remainingLength -= currentRun.Length;
+ }
- if (remainingLength <= 0 || currentPosition >= characterIndex)
+ if (remainingLength <= 0)
{
break;
}
-
- lastDirection = currentDirection;
}
- return result;
- }
+ newPosition = currentPosition;
- private IReadOnlyList GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
- {
- var characterIndex = firstTextSourceIndex + textLength;
+ var runWidth = endX - startX;
- var result = new List(_textRuns.Length);
- var lastDirection = FlowDirection.LeftToRight;
- var currentDirection = lastDirection;
+ var bounds = new Rect(startX, 0, runWidth, Height);
- var currentPosition = FirstTextSourceIndex;
- var remainingLength = textLength;
+ return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds);
+ }
- var startX = WidthIncludingTrailingWhitespace;
- double currentWidth = 0;
- var currentRect = default(Rect);
+ private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX,
+ int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
+ {
+ coveredLength = 0;
+ var textRunBounds = new List();
+ var endX = startX;
- for (var index = _textRuns.Length - 1; index >= 0; index--)
+ for (int i = firstRunIndex; i <= lastRunIndex; i++)
{
- if (_textRuns[index] is not DrawableTextRun currentRun)
- {
- continue;
- }
-
- if (currentPosition + currentRun.Length < firstTextSourceIndex)
- {
- startX -= currentRun.Size.Width;
-
- currentPosition += currentRun.Length;
-
- continue;
- }
-
- var characterLength = 0;
- var endX = startX;
+ var currentRun = _textRuns[i];
- if (currentRun is ShapedTextRun currentShapedRun)
+ if (currentRun is ShapedTextRun shapedTextRun)
{
- var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
-
- currentPosition += offset;
-
- var startIndex = currentPosition;
- double startOffset;
- double endOffset;
+ var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
- if (currentShapedRun.ShapedBuffer.IsLeftToRight)
- {
- if (currentPosition < startIndex)
- {
- startOffset = endOffset = 0;
- }
- else
- {
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ textRunBounds.Add(runBounds);
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
- }
- }
- else
+ if (offset > 0)
{
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ startX = runBounds.Rectangle.Left;
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ endX = startX;
}
- startX -= currentRun.Size.Width - startOffset;
- endX -= currentRun.Size.Width - endOffset;
+ currentPosition += runBounds.Length + offset;
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ endX += runBounds.Rectangle.Width;
- characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+ coveredLength += runBounds.Length;
- currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
- FlowDirection.LeftToRight :
- FlowDirection.RightToLeft;
+ remainingLength -= runBounds.Length;
}
else
{
- if (currentPosition + currentRun.Length <= characterIndex)
+ if (currentRun is DrawableTextRun drawableTextRun)
{
- endX -= currentRun.Size.Width;
+ textRunBounds.Add(
+ new TextRunBounds(
+ new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
+
+ endX += drawableTextRun.Size.Width;
}
- if (currentPosition < firstTextSourceIndex)
- {
- startX -= currentRun.Size.Width;
+ currentPosition += currentRun.Length;
- characterLength = currentRun.Length;
- }
- }
+ coveredLength += currentRun.Length;
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
+ remainingLength -= currentRun.Length;
}
- //Lines that only contain a linebreak need to be covered here
- if (characterLength == 0)
+ if (remainingLength <= 0)
{
- characterLength = NewLineLength;
+ break;
}
+ }
- var runWidth = endX - startX;
+ newPosition = currentPosition;
- var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+ var runWidth = endX - startX;
- if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
- {
- if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
- {
- currentRect = currentRect.WithWidth(currentWidth + runWidth);
+ var bounds = new Rect(startX, 0, runWidth, Height);
- var textBounds = result[result.Count - 1];
+ return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds);
+ }
- textBounds.Rectangle = currentRect;
+ private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX,
+ int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
+ {
+ var startIndex = currentPosition;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
- {
- currentRect = currentRunBounds.Rectangle;
+ offset = Math.Max(0, firstTextSourceIndex - currentPosition);
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
- }
- }
+ var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
- currentWidth += runWidth;
- currentPosition += characterLength;
+ if (currentPosition != firstCluster)
+ {
+ startIndex = firstCluster + offset;
+ }
+ else
+ {
+ startIndex += offset;
+ }
- if (currentPosition > characterIndex)
- {
- break;
- }
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
- lastDirection = currentDirection;
- remainingLength -= characterLength;
+ var endX = startX + endOffset;
+ startX += startOffset;
- if (remainingLength <= 0)
- {
- break;
- }
+ var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+
+ var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+
+ if (endX < startX)
+ {
+ (endX, startX) = (startX, endX);
+ }
+
+ //Lines that only contain a linebreak need to be covered here
+ if (characterLength == 0)
+ {
+ characterLength = NewLineLength;
}
- result.Reverse();
+ var runWidth = endX - startX;
- return result;
+ return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
- private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
+ private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX,
+ int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
{
var startX = endX;
- var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+ var startIndex = currentPosition;
- currentPosition += offset;
+ offset = Math.Max(0, firstTextSourceIndex - currentPosition);
- var startIndex = currentPosition;
+ var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
- double startOffset;
- double endOffset;
+ if (currentPosition != firstCluster)
+ {
+ startIndex = firstCluster + offset;
+ }
+ else
+ {
+ startIndex += offset;
+ }
- endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
- startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
@@ -980,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting
return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
- public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength)
- {
- if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
- {
- return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength);
- }
-
- return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength);
- }
-
public override void Dispose()
{
for (int i = 0; i < _textRuns.Length; i++)
@@ -1005,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting
{
_textLineMetrics = CreateLineMetrics();
+ if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
+ {
+ _textLineBreak = new TextLineBreak(textEndOfLine);
+ }
+
BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
}
@@ -1328,7 +1443,7 @@ namespace Avalonia.Media.TextFormatting
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
+ newLineLength += textRun.GlyphRun.Metrics.NewLineLength;
}
widthIncludingWhitespace += textRun.Size.Width;
@@ -1340,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting
{
widthIncludingWhitespace += drawableTextRun.Size.Width;
- switch (_paragraphProperties.FlowDirection)
+ if (index == lastRunIndex)
{
- case FlowDirection.LeftToRight:
- {
- if (index == lastRunIndex)
- {
- width = widthIncludingWhitespace;
- trailingWhitespaceLength = 0;
- newLineLength = 0;
- }
-
- break;
- }
-
- case FlowDirection.RightToLeft:
- {
- if (index == lastRunIndex)
- {
- width = widthIncludingWhitespace;
- trailingWhitespaceLength = 0;
- newLineLength = 0;
- }
-
- break;
- }
+ width = widthIncludingWhitespace;
+ trailingWhitespaceLength = 0;
}
if (drawableTextRun.Size.Height > height)
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 9bd1dc95f9..df9a3eb8f3 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -720,6 +720,16 @@ namespace Avalonia.Controls
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
+ if (HasComplexContent)
+ {
+ ArrangeComplexContent(TextLayout, padding);
+ }
+
+ if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width))
+ {
+ return finalSize;
+ }
+
_constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
_textLayout?.Dispose();
@@ -727,31 +737,36 @@ namespace Avalonia.Controls
if (HasComplexContent)
{
- var currentY = padding.Top;
+ ArrangeComplexContent(TextLayout, padding);
+ }
- foreach (var textLine in TextLayout.TextLines)
- {
- var currentX = padding.Left + textLine.Start;
+ return finalSize;
+ }
- foreach (var run in textLine.TextRuns)
+ private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding)
+ {
+ var currentY = padding.Top;
+
+ foreach (var textLine in textLayout.TextLines)
+ {
+ var currentX = padding.Left + textLine.Start;
+
+ foreach (var run in textLine.TextRuns)
+ {
+ if (run is DrawableTextRun drawable)
{
- if (run is DrawableTextRun drawable)
+ if (drawable is EmbeddedControlRun controlRun
+ && controlRun.Control is Control control)
{
- if (drawable is EmbeddedControlRun controlRun
- && controlRun.Control is Control control)
- {
- control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
- }
-
- currentX += drawable.Size.Width;
+ control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
}
- }
- currentY += textLine.Height;
+ currentX += drawable.Size.Width;
+ }
}
- }
- return finalSize;
+ currentY += textLine.Height;
+ }
}
protected override AutomationPeer OnCreateAutomationPeer()
@@ -892,7 +907,7 @@ namespace Avalonia.Controls
return textRun;
}
- return null;
+ return new TextEndOfParagraph();
}
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index 954169f975..8a2d4ecc6b 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -660,6 +660,90 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [Fact]
+ public void Should_Return_Null_For_Empty_TextSource()
+ {
+ using (Start())
+ {
+ var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
+ var textSource = new EmptyTextSource();
+
+ var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
+
+ Assert.Null(textLine);
+ }
+ }
+
+ [Fact]
+ public void Should_Retain_TextEndOfParagraph_With_TextWrapping()
+ {
+ using (Start())
+ {
+ var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
+ var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
+
+ var text = "Hello World";
+
+ var textSource = new SimpleTextSource(text, defaultRunProperties);
+
+ var pos = 0;
+
+ TextLineBreak previousLineBreak = null;
+ TextLine textLine = null;
+
+ while (pos < text.Length)
+ {
+ textLine = TextFormatter.Current.FormatLine(textSource, pos, 30, paragraphProperties, previousLineBreak);
+
+ pos += textLine.Length;
+
+ previousLineBreak = textLine.TextLineBreak;
+ }
+
+ Assert.NotNull(textLine);
+
+ Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
+ }
+ }
+
+ protected readonly record struct SimpleTextSource : ITextSource
+ {
+ private readonly string _text;
+ private readonly TextRunProperties _defaultProperties;
+
+ public SimpleTextSource(string text, TextRunProperties defaultProperties)
+ {
+ _text = text;
+ _defaultProperties = defaultProperties;
+ }
+
+ public TextRun? GetTextRun(int textSourceIndex)
+ {
+ if (textSourceIndex > _text.Length)
+ {
+ return new TextEndOfParagraph();
+ }
+
+ var runText = _text.AsMemory(textSourceIndex);
+
+ if (runText.IsEmpty)
+ {
+ return new TextEndOfParagraph();
+ }
+
+ return new TextCharacters(runText, _defaultProperties);
+ }
+ }
+
+ private class EmptyTextSource : ITextSource
+ {
+ public TextRun GetTextRun(int textSourceIndex)
+ {
+ return null;
+ }
+ }
+
private class EndOfLineTextSource : ITextSource
{
public TextRun GetTextRun(int textSourceIndex)
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
index 2b63f24cf6..3735e9f6d7 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
@@ -9,7 +9,6 @@ using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Xunit;
-
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
public class TextLayoutTests
@@ -1028,6 +1027,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
+ [InlineData("mgfg🧐df f sdf", "g🧐d", 20, 40)]
+ [InlineData("وه. وقد تعرض لانتقادات", "دات", 5, 30)]
+ [InlineData("وه. وقد تعرض لانتقادات", "تعرض", 20, 50)]
+ [InlineData(" علمية 😱ومضللة ،", " علمية 😱ومضللة ،", 40, 100)]
+ [InlineData("في عام 2018 ، رفعت ل", "في عام 2018 ، رفعت ل", 100, 120)]
+ [Theory]
+ public void HitTestTextRange_Range_ValidLength(string text, string textToSelect, double minWidth, double maxWidth)
+ {
+ using (Start())
+ {
+ var layout = new TextLayout(text, Typeface.Default, 12, Brushes.Black);
+ var start = text.IndexOf(textToSelect);
+ var selectionRectangles = layout.HitTestTextRange(start, textToSelect.Length);
+ Assert.Equal(1, selectionRectangles.Count());
+ var rect = selectionRectangles.First();
+ Assert.InRange(rect.Width, minWidth, maxWidth);
+ }
+ }
+
+ [InlineData("012🧐210", 2, 4, FlowDirection.LeftToRight, "14.40234375,40.8046875")]
+ [InlineData("210🧐012", 2, 4, FlowDirection.RightToLeft, "0,7.201171875;21.603515625,33.603515625;48.005859375,55.20703125")]
+ [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.63671875,39.779296875")]
+ [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.63671875,39.779296875")]
+ [Theory]
+ public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length,
+ FlowDirection flowDirection, string expected)
+ {
+ using (Start())
+ {
+ var expectedRects = expected.Split(';').Select(x =>
+ {
+ var startEnd = x.Split(',');
+
+ var start = double.Parse(startEnd[0], CultureInfo.InvariantCulture);
+
+ var end = double.Parse(startEnd[1], CultureInfo.InvariantCulture);
+
+ return new Rect(start, 0, end - start, 0);
+ }).ToArray();
+
+ var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: flowDirection);
+
+ var rects = textLayout.HitTestTextRange(start, length).ToArray();
+
+ Assert.Equal(expectedRects.Length, rects.Length);
+
+ var endX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(2));
+ var startX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(5, 1));
+
+ for (int i = 0; i < expectedRects.Length; i++)
+ {
+ var expectedRect = expectedRects[i];
+
+ Assert.Equal(expectedRect.Left, rects[i].Left);
+
+ Assert.Equal(expectedRect.Right, rects[i].Right);
+ }
+ }
+ }
private static IDisposable Start()
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index 544b84912e..e47542af7a 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -604,19 +604,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, 20);
- Assert.Equal(2, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 30);
- Assert.Equal(3, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 40);
- Assert.Equal(4, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
@@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textBounds = textLine.GetTextBounds(0, textLine.Length);
- Assert.Equal(6, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 1);
@@ -857,7 +857,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, firstRun.Length + 1);
- Assert.Equal(2, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(1, firstRun.Length);
@@ -867,7 +867,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length);
- Assert.Equal(2, textBounds.Count);
+ Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
}
}
@@ -958,6 +958,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width);
Assert.Equal(7.201171875, textBounds[0].Rectangle.Width);
+
Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right);
Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left);