Browse Source

More RTL hit testing fixes

Benedikt Stebner 3 years ago
parent
commit
678620422d

+ 117 - 132
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;
             }

+ 1 - 1
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

+ 12 - 2
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);
+                }
             }
         }
 

+ 72 - 11
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));
             }
         }