Browse Source

More RTL hit testing fixes

pull/8671/head
Benedikt Stebner 4 years ago
parent
commit
678620422d
  1. 249
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  2. 2
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  3. 14
      src/Avalonia.Controls/Documents/InlineCollection.cs
  4. 83
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

249
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
var collapsingProperties = collapsingPropertiesList[0];
if(collapsingProperties is null)
if (collapsingProperties is null)
{
return this;
}
@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = _textRuns[i];
if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{
var rightToLeftIndex = i;
currentPosition += currentRun.TextSourceLength;
@ -213,14 +213,14 @@ namespace Avalonia.Media.TextFormatting
for (var j = i; i <= rightToLeftIndex; j++)
{
if(j > _textRuns.Count - 1)
if (j > _textRuns.Count - 1)
{
break;
}
currentRun = _textRuns[j];
if(currentDistance + currentRun.Size.Width <= distance)
if (currentDistance + currentRun.Size.Width <= distance)
{
currentDistance += currentRun.Size.Width;
currentPosition -= currentRun.TextSourceLength;
@ -322,11 +322,11 @@ namespace Avalonia.Media.TextFormatting
continue;
}
break;
}
if(i > index)
if (i > index)
{
while (i >= index)
{
@ -350,7 +350,7 @@ namespace Avalonia.Media.TextFormatting
}
}
if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
{
return Math.Max(0, currentDistance + distance);
@ -530,6 +530,8 @@ namespace Avalonia.Media.TextFormatting
double currentWidth = 0;
var currentRect = Rect.Empty;
TextRunBounds lastRunBounds = default;
for (var index = 0; index < TextRuns.Count; index++)
{
if (TextRuns[index] is not DrawableTextRun currentRun)
@ -539,53 +541,93 @@ namespace Avalonia.Media.TextFormatting
var characterLength = 0;
var endX = startX;
var runWidth = 0.0;
TextRunBounds? currentRunBounds = null;
var currentShapedRun = currentRun as ShapedTextCharacters;
TextRunBounds currentRunBounds;
double combinedWidth;
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
{
var rightToLeftIndex = index;
startX += currentShapedRun.Size.Width;
var rightToLeftWidth = currentShapedRun.Size.Width;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
{
var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{
break;
}
startX += nextShapedRun.Size.Width;
rightToLeftIndex++;
rightToLeftWidth += nextShapedRun.Size.Width;
if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength)
{
break;
}
currentShapedRun = nextShapedRun;
}
if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds))
startX = startX + rightToLeftWidth;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
remainingLength -= currentRunBounds.Length;
currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
endX = currentRunBounds.Rectangle.Right;
startX = currentRunBounds.Rectangle.Left;
var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
for (int i = rightToLeftIndex - 1; i >= index; i--)
{
startX = currentRunBounds!.Rectangle.Left;
endX = currentRunBounds.Rectangle.Right;
currentShapedRun = TextRuns[i] as ShapedTextCharacters;
if(currentShapedRun == null)
{
continue;
}
runWidth = currentRunBounds.Rectangle.Width;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
rightToLeftRunBounds.Insert(0, currentRunBounds);
remainingLength -= currentRunBounds.Length;
startX = currentRunBounds.Rectangle.Left;
currentPosition += currentRunBounds.Length;
}
combinedWidth = endX - startX;
currentRect = new Rect(startX, 0, combinedWidth, Height);
currentDirection = FlowDirection.RightToLeft;
if (!MathUtilities.IsZero(combinedWidth))
{
result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
}
startX = endX;
}
else
{
if (currentShapedRun != null)
{
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentPosition += offset;
@ -661,43 +703,46 @@ namespace Avalonia.Media.TextFormatting
characterLength = NewLineLength;
}
runWidth = endX - startX;
currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
combinedWidth = endX - startX;
currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
currentPosition += characterLength;
remainingLength -= characterLength;
}
if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
startX = endX;
if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
{
currentRect = currentRect.WithWidth(currentWidth + runWidth);
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{
currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
var textBounds = result[result.Count - 1];
var textBounds = result[result.Count - 1];
textBounds.Rectangle = currentRect;
textBounds.Rectangle = currentRect;
textBounds.TextRunBounds.Add(currentRunBounds!);
}
else
{
currentRect = currentRunBounds!.Rectangle;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
}
lastRunBounds = currentRunBounds;
}
currentWidth += runWidth;
currentWidth += combinedWidth;
if (remainingLength <= 0 || currentPosition >= characterIndex)
{
break;
}
startX = endX;
lastDirection = currentDirection;
}
@ -852,105 +897,45 @@ namespace Avalonia.Media.TextFormatting
return result;
}
private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds)
private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
{
textRunBounds = null;
var startX = endX;
for (var index = runIndex; index >= 0; index--)
{
if (TextRuns[index] is not DrawableTextRun currentRun)
{
continue;
}
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
currentPosition += offset;
currentPosition += currentRun.TextSourceLength;
var startIndex = currentRun.Text.Start + offset;
continue;
}
double startOffset;
double endOffset;
var characterLength = 0;
var endX = startX;
if (currentRun is ShapedTextCharacters currentShapedRun)
{
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
double startOffset;
double endOffset;
if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
if (currentPosition < startIndex)
{
startOffset = endOffset = 0;
}
else
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
}
}
else
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
}
else
{
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
{
endX -= currentRun.Size.Width;
}
startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
if (currentPosition < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
characterLength = currentRun.TextSourceLength;
}
}
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
var runWidth = endX - startX;
remainingLength -= characterLength;
currentPosition += characterLength;
textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
return true;
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
return false;
var runWidth = endX - startX;
return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
@ -1532,7 +1517,7 @@ namespace Avalonia.Media.TextFormatting
var textAlignment = _paragraphProperties.TextAlignment;
var paragraphFlowDirection = _paragraphProperties.FlowDirection;
if(textAlignment == TextAlignment.Justify)
if (textAlignment == TextAlignment.Justify)
{
textAlignment = TextAlignment.Start;
}

2
src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs

@ -3,7 +3,7 @@
/// <summary>
/// The bounding rectangle of text run
/// </summary>
public sealed class TextRunBounds
public readonly struct TextRunBounds
{
/// <summary>
/// Constructing TextRunBounds

14
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -111,7 +111,7 @@ namespace Avalonia.Controls.Documents
private void AddText(string text)
{
if(Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
{
textBlock._text += text;
}
@ -156,7 +156,17 @@ namespace Avalonia.Controls.Documents
{
foreach (var child in this)
{
((ISetLogicalParent)child).SetParent(parent);
var oldParent = child.Parent;
if (oldParent != parent)
{
if (oldParent != null)
{
((ISetLogicalParent)child).SetParent(null);
}
((ISetLogicalParent)child).SetParent(parent);
}
}
}

83
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -597,21 +597,82 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, 20);
Assert.Equal(1, textBounds.Count);
Assert.Equal(2, textBounds.Count);
Assert.Equal(144.0234375, textBounds[0].Rectangle.Width);
Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 30);
Assert.Equal(1, textBounds.Count);
Assert.Equal(3, textBounds.Count);
Assert.Equal(216.03515625, textBounds[0].Rectangle.Width);
Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 40);
Assert.Equal(1, textBounds.Count);
Assert.Equal(4, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
}
[Fact]
public void Should_GetTextRange()
{
var text = "שדגככעיחדגכAישדגשדגחייטYDASYWIWחיחלדשSAטויליHUHIUHUIDWKLאא'ק'קחליק/'וקןגגגלךשף'/קפוכדגכשדגשיח'/קטאגשד";
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource(text, defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var textRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds[0].Rectangle.Width);
var lineWidth = textLine.WidthIncludingTrailingWhitespace;
var textBounds = textLine.GetTextBounds(0, text.Length);
TextBounds lastBounds = null;
var runBounds = textBounds.SelectMany(x => x.TextRunBounds).ToList();
Assert.Equal(textRuns.Count, runBounds.Count);
for (var i = 0; i < textRuns.Count; i++)
{
var run = textRuns[i];
var bounds = runBounds[i];
Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex);
Assert.Equal(run, bounds.TextRun);
Assert.Equal(run.Size.Width, bounds.Rectangle.Width);
}
for (var i = 0; i < textBounds.Count; i++)
{
var currentBounds = textBounds[i];
if (lastBounds != null)
{
Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left);
}
var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width);
Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width);
lastBounds = currentBounds;
}
var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width);
Assert.Equal(lineWidth, sumOfBoundsWidth);
}
}
@ -779,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
Assert.Equal(1, textBounds.Count);
Assert.Equal(6, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 1);
@ -789,8 +850,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1);
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
Assert.Equal(2, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(1, firstRun.Text.Length);
@ -799,8 +860,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1);
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
Assert.Equal(2, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
}
}

Loading…
Cancel
Save