Browse Source

Some more hit testing fixes

Benedikt Stebner 3 years ago
parent
commit
3e6bc0b48d

+ 27 - 17
src/Avalonia.Base/Media/GlyphRun.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Media
             IReadOnlyList<int>? glyphClusters = null,
             int biDiLevel = 0)
         {
-            _glyphTypeface = glyphTypeface;  
+            _glyphTypeface = glyphTypeface;
 
             FontRenderingEmSize = fontRenderingEmSize;
 
@@ -204,7 +204,7 @@ namespace Avalonia.Media
         public double GetDistanceFromCharacterHit(CharacterHit characterHit)
         {
             var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
-           
+
             var distance = 0.0;
 
             if (IsLeftToRight)
@@ -223,7 +223,7 @@ namespace Avalonia.Media
                 }
 
                 var glyphIndex = FindGlyphIndex(characterIndex);
-                
+
                 if (GlyphClusters != null)
                 {
                     var currentCluster = GlyphClusters[glyphIndex];
@@ -249,7 +249,7 @@ namespace Avalonia.Media
             {
                 //RightToLeft
                 var glyphIndex = FindGlyphIndex(characterIndex);
-                
+
                 if (GlyphClusters != null)
                 {
                     if (characterIndex > GlyphClusters[0])
@@ -284,13 +284,13 @@ namespace Avalonia.Media
         public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
         {
             var characterIndex = 0;
-            
+
             // Before
             if (distance <= 0)
             {
                 isInside = false;
 
-                if(GlyphClusters != null)
+                if (GlyphClusters != null)
                 {
                     characterIndex = GlyphClusters[characterIndex];
                 }
@@ -307,11 +307,11 @@ namespace Avalonia.Media
 
                 characterIndex = GlyphIndices.Count - 1;
 
-                if(GlyphClusters != null)
+                if (GlyphClusters != null)
                 {
                     characterIndex = GlyphClusters[characterIndex];
                 }
-                
+
                 var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
 
                 return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
@@ -327,7 +327,7 @@ namespace Avalonia.Media
                     var advance = GetGlyphAdvance(index, out var cluster);
 
                     characterIndex = cluster;
-                    
+
                     if (distance > currentX && distance <= currentX + advance)
                     {
                         break;
@@ -345,7 +345,7 @@ namespace Avalonia.Media
                     var advance = GetGlyphAdvance(index, out var cluster);
 
                     characterIndex = cluster;
-                    
+
                     if (currentX - advance < distance)
                     {
                         break;
@@ -554,6 +554,16 @@ namespace Avalonia.Media
                 nextCluster = GlyphClusters[currentIndex];
             }
 
+            if (nextCluster < Characters.Start)
+            {
+                nextCluster = Characters.Start;
+            }
+
+            if (cluster < Characters.Start)
+            {
+                cluster = Characters.Start;
+            }
+
             int trailingLength;
 
             if (nextCluster == cluster)
@@ -577,7 +587,7 @@ namespace Avalonia.Media
         private double GetGlyphAdvance(int index, out int cluster)
         {
             cluster = GlyphClusters != null ? GlyphClusters[index] : index;
-            
+
             if (GlyphAdvances != null)
             {
                 return GlyphAdvances[index];
@@ -603,7 +613,7 @@ namespace Avalonia.Media
             var widthIncludingTrailingWhitespace = 0d;
 
             var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
-            
+
             for (var index = 0; index < GlyphIndices.Count; index++)
             {
                 var advance = GetGlyphAdvance(index, out _);
@@ -615,7 +625,7 @@ namespace Avalonia.Media
 
             if (IsLeftToRight)
             {
-                for (var index = GlyphIndices.Count - glyphCount; index <GlyphIndices.Count; index++)
+                for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
                 {
                     width -= GetGlyphAdvance(index, out _);
                 }
@@ -677,12 +687,12 @@ namespace Avalonia.Media
                     if (codepointIndex < 0)
                     {
                         trailingWhitespaceLength = _characters.Length;
-                        
+
                         glyphCount = GlyphClusters.Count;
-                        
+
                         break;
                     }
-                    
+
                     var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
 
                     if (!codepoint.IsWhiteSpace)
@@ -696,7 +706,7 @@ namespace Avalonia.Media
                     }
 
                     trailingWhitespaceLength++;
-                    
+
                     glyphCount++;
                 }
             }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -544,7 +544,7 @@ namespace Avalonia.Media.TextFormatting
 
             var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
 
-            return new TextLineImpl(textRuns, firstTextSourceIndex, 1, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine();
+            return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine();
         }
 
         /// <summary>

+ 37 - 37
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -183,6 +183,7 @@ namespace Avalonia.Media.TextFormatting
                     case ShapedTextCharacters shapedRun:
                         {
                             characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+
                             break;
                         }
                     default:
@@ -426,31 +427,42 @@ namespace Avalonia.Media.TextFormatting
 
                 if (nextRun != null)
                 {
-                    if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
+                    switch (nextRun)
                     {
-                        goto skip;
-                    }
+                        case ShapedTextCharacters when currentRun is ShapedTextCharacters:
+                            {
+                                if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
+                                {
+                                    goto skip;
+                                }
 
-                    if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
-                    {
-                        goto skip;
-                    }
+                                if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
+                                {
+                                    goto skip;
+                                }
 
-                    if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
-                    {
-                        goto skip;
-                    }
+                                if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
+                                {
+                                    goto skip;
+                                }
 
-                    if (currentRun.Text.End < firstTextSourceCharacterIndex)
-                    {
-                        goto skip;
-                    }
+                                if (currentRun.Text.End < firstTextSourceCharacterIndex)
+                                {
+                                    goto skip;
+                                }
 
-                    goto noop;
+                                goto noop;
+                            }
+                        default:
+                            {
+                               goto noop;
+                            }
+                    }
 
                 skip:
                     {
                         startX += currentRun.Size.Width;
+                        currentPosition += currentRun.TextSourceLength;
                     }
 
                     continue;
@@ -460,7 +472,6 @@ namespace Avalonia.Media.TextFormatting
                     }
                 }
 
-
                 var endX = startX;
                 var endOffset = 0d;
 
@@ -520,11 +531,13 @@ namespace Avalonia.Media.TextFormatting
                         }
                     default:
                         {
-                            if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length)
+                            if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength)
                             {
                                 endX += currentRun.Size.Width;
                             }
 
+                            currentPosition += currentRun.TextSourceLength;
+
                             break;
                         }
                 }
@@ -538,7 +551,9 @@ namespace Avalonia.Media.TextFormatting
 
                 if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
                 {
-                    var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection);
+                    currentRect = currentRect.WithWidth(currentRect.Width + width);
+
+                    var textBounds = new TextBounds(currentRect, currentDirection);
 
                     result[result.Count - 1] = textBounds;
                 }
@@ -551,21 +566,9 @@ namespace Avalonia.Media.TextFormatting
 
                 if (currentDirection == FlowDirection.LeftToRight)
                 {
-                    if (nextRun != null)
-                    {
-                        if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
-                        {
-                            break;
-                        }
-
-                        currentPosition = nextRun.Text.End;
-                    }
-                    else
+                    if (currentPosition >= firstTextSourceCharacterIndex + textLength)
                     {
-                        if (currentPosition >= firstTextSourceCharacterIndex + textLength)
-                        {
-                            break;
-                        }
+                        break;
                     }
                 }
                 else
@@ -575,10 +578,7 @@ namespace Avalonia.Media.TextFormatting
                         break;
                     }
 
-                    if (currentPosition != currentRun.Text.Start)
-                    {
-                        endX += currentRun.Size.Width - endOffset;
-                    }
+                    endX += currentRun.Size.Width - endOffset;
                 }
 
                 lastDirection = currentDirection;

+ 68 - 29
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@@ -70,12 +70,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 }
             }
         }
-        
+
         [Fact]
         public void Should_Get_Next_Caret_CharacterHit_Bidi()
         {
             const string text = "אבג 1 ABC";
-            
+
             using (Start())
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var clusters = new List<int>();
 
-                foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
+                foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
                 {
                     var shapedRun = (ShapedTextCharacters)textRun;
 
@@ -98,7 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
                         shapedRun.ShapedBuffer.GlyphClusters);
                 }
-                
+
                 var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
 
                 foreach (var cluster in clusters)
@@ -122,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         public void Should_Get_Previous_Caret_CharacterHit_Bidi()
         {
             const string text = "אבג 1 ABC";
-            
+
             using (Start())
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var clusters = new List<int>();
 
-                foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
+                foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
                 {
                     var shapedRun = (ShapedTextCharacters)textRun;
 
@@ -147,13 +147,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 }
 
                 clusters.Reverse();
-                
+
                 var nextCharacterHit = new CharacterHit(text.Length - 1);
 
                 foreach (var cluster in clusters)
                 {
                     var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
-                    
+
                     Assert.Equal(cluster, currentCaretIndex);
 
                     nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit);
@@ -168,7 +168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
             }
         }
-        
+
         [InlineData("𐐷𐐷𐐷𐐷𐐷")]
         [InlineData("01234567🎉\n")]
         [InlineData("𐐷1234")]
@@ -324,7 +324,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     }
                 }
 
-                Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
+                Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
             }
         }
 
@@ -371,7 +371,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026");
                 yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026");
                 yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, "");
-                
+
                 object[] CreateData(string text, double width, TextTrimming mode, string expected)
                 {
                     return new object[]
@@ -424,7 +424,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
                 var textSource = new DrawableRunTextSource();
-                
+
                 var formatter = new TextFormatterImpl();
 
                 var textLine =
@@ -471,7 +471,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(4, textLine.TextRuns.Count);
 
-                var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1));
+                var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
 
                 Assert.Equal(3, currentHit.FirstCharacterIndex);
                 Assert.Equal(0, currentHit.TrailingLength);
@@ -552,11 +552,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 switch (textSourceIndex)
                 {
                     case 0:
-                        return new CustomDrawableRun();                     
+                        return new CustomDrawableRun();
                     case 1:
                         return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default));
                     case 2:
-                        return new CustomDrawableRun();                      
+                        return new CustomDrawableRun();
                     case 3:
                         return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default));
                     default:
@@ -564,14 +564,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 }
             }
         }
-        
+
         private class CustomDrawableRun : DrawableTextRun
         {
             public override Size Size => new(14, 14);
             public override double Baseline => 14;
             public override void Draw(DrawingContext drawingContext, Point origin)
             {
-               
+
             }
         }
 
@@ -587,29 +587,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             var shapedTextRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
 
             var lastCluster = -1;
-            
+
             foreach (var textRun in shapedTextRuns)
             {
                 var shapedBuffer = textRun.ShapedBuffer;
 
                 var currentClusters = shapedBuffer.GlyphClusters.ToList();
 
-                foreach (var currentCluster in currentClusters) 
+                foreach (var currentCluster in currentClusters)
                 {
                     if (lastCluster == currentCluster)
                     {
                         continue;
                     }
-                    
+
                     glyphClusters.Add(currentCluster);
 
                     lastCluster = currentCluster;
                 }
             }
-            
+
             return glyphClusters;
         }
-        
+
         private static List<Rect> BuildRects(TextLine textLine)
         {
             var rects = new List<Rect>();
@@ -624,11 +624,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             foreach (var textRun in shapedTextRuns)
             {
                 var shapedBuffer = textRun.ShapedBuffer;
-            
+
                 for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++)
                 {
                     var currentCluster = shapedBuffer.GlyphClusters[index];
-                
+
                     var advance = shapedBuffer.GlyphAdvances[index];
 
                     if (lastCluster != currentCluster)
@@ -642,10 +642,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         rects.Remove(rect);
 
                         rect = rect.WithWidth(rect.Width + advance);
-                    
+
                         rects.Add(rect);
                     }
-                    
+
                     currentX += advance;
 
                     lastCluster = currentCluster;
@@ -655,8 +655,43 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             return rects;
         }
 
+
         [Fact]
-        public void Should_Get_TextBounds()
+        public void Should_Get_TextBounds_Mixed()
+        {
+            using (Start())
+            {
+                var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+                var text = "0123".AsMemory();
+                var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
+
+                var textRuns = new List<TextRun>
+                {
+                    new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text), shaperOption), defaultProperties),
+                    new CustomDrawableRun(),
+                    new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length + 1, text.Length), shaperOption), defaultProperties),
+                    new CustomDrawableRun(),
+                    new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 2 + 2, text.Length), shaperOption), defaultProperties),
+                    new CustomDrawableRun(),
+                };
+
+                var textSource = new FixedRunsTextSource(textRuns);
+
+                var formatter = new TextFormatterImpl();
+
+                var textLine =
+                    formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+                        new GenericTextParagraphProperties(defaultProperties));
+
+                var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
+
+                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
+            }
+        }
+
+        [Fact]
+        public void Should_Get_TextBounds_BiDi()
         {
             using (Start())
             {
@@ -673,7 +708,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 3, text.Length), ltrOptions), defaultProperties)
                 };
 
-             
+
                 var textSource = new FixedRunsTextSource(textRuns);
 
                 var formatter = new TextFormatterImpl();
@@ -700,12 +735,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
             public TextRun? GetTextRun(int textSourceIndex)
             {
+                var currentPosition = 0;
+
                 foreach (var textRun in _textRuns)
                 {
-                    if(textRun.Text.Start == textSourceIndex)
+                    if (currentPosition == textSourceIndex)
                     {
                         return textRun;
                     }
+
+                    currentPosition += textRun.TextSourceLength;
                 }
 
                 return null;