Browse Source

Adjust GetBackspaceCaretCharacterHit (#19586)

* Adjust GetBackspaceCaretCharacterHit implementation so it works with codepoint boundaries

* Add a unit test
Benedikt Stebner 1 week ago
parent
commit
c5efedb81a

+ 20 - 30
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -814,7 +814,7 @@ namespace Avalonia.Media.TextFormatting
             }
         }
 
-        private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries)
+        private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool isBackspaceDelete)
         {
             if (_textRuns.Length == 0 || _indexedTextRuns is null)
             {
@@ -833,8 +833,6 @@ namespace Avalonia.Media.TextFormatting
                 return new CharacterHit(FirstTextSourceIndex);
             }
 
-            var currentCharacterHit = characterHit;
-
             var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
 
             var previousCharacterHit = characterHit;
@@ -843,46 +841,38 @@ namespace Avalonia.Media.TextFormatting
             {
                 case ShapedTextRun shapedRun:
                     {
-                        var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
+                        //Determine the start of the first hit in local positions.
+                        var runOffset = Math.Max(0, characterIndex - currentPosition);
 
-                        if (offset > 0)
-                        {
-                            currentCharacterHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
-                        }
+                        var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
 
-                        previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterHit);
+                        //Current position is a text source index and first cluster is relative to the GlyphRun's buffer.
+                        var textSourceOffset = currentPosition - firstCluster;
 
-                        if (useGraphemeBoundaries)
+                        if (isBackspaceDelete)
                         {
-                            var textPosition = Math.Max(0, previousCharacterHit.FirstCharacterIndex - shapedRun.GlyphRun.Metrics.FirstCluster);
-
-                            var text = shapedRun.GlyphRun.Characters.Slice(textPosition);
-
-                            var graphemeEnumerator = new GraphemeEnumerator(text.Span);
-
                             var length = 0;
 
-                            var clusterLength = Math.Max(0, currentCharacterHit.FirstCharacterIndex + currentCharacterHit.TrailingLength - 
-                                previousCharacterHit.FirstCharacterIndex - previousCharacterHit.TrailingLength);
-
-                            while (graphemeEnumerator.MoveNext(out var grapheme))
+                            while (Codepoint.ReadAt(shapedRun.GlyphRun.Characters.Span, length, out var count) != Codepoint.ReplacementCodepoint)
                             {
-                                if (length + grapheme.Length < clusterLength)
+                                if (length + count >= runOffset)
                                 {
-                                    length += grapheme.Length;
-
-                                    continue;
+                                    break;
                                 }
 
-                                previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + length);
-
-                                break;
+                                length += count;
                             }
-                        }
 
-                        if (offset > 0)
+                            previousCharacterHit = new CharacterHit(characterIndex - runOffset + length);
+                        }
+                        else
                         {
-                            previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength);
+                            previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(new CharacterHit(firstCluster + runOffset));
+
+                            if(textSourceOffset > 0)
+                            {
+                                previousCharacterHit = new CharacterHit(textSourceOffset + previousCharacterHit.FirstCharacterIndex, previousCharacterHit.TrailingLength);
+                            }
                         }
 
                         break;

+ 35 - 13
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@@ -9,7 +9,6 @@ using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.UnitTests;
 using Xunit;
-using static System.Net.Mime.MediaTypeNames;
 
 namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 {
@@ -905,7 +904,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.NotNull(textLine);
 
                 Assert.Throws<ArgumentOutOfRangeException>(() => textLine.GetTextBounds(0, 0));
-            }     
+            }
         }
 
         [Fact]
@@ -983,12 +982,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var defaultProperties = new GenericTextRunProperties(typeface);
                 var textSource = new CustomTextBufferTextSource(
-                    new TextHidden(1), 
-                    new TextCharacters("Authenti", defaultProperties), 
-                    new TextHidden(1), 
+                    new TextHidden(1),
+                    new TextCharacters("Authenti", defaultProperties),
+                    new TextHidden(1),
                     new TextHidden(1),
                     new TextCharacters("ff", defaultProperties),
-                    new TextHidden(1), 
+                    new TextHidden(1),
                     new TextHidden(1));
 
                 var formatter = new TextFormatterImpl();
@@ -1138,7 +1137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
                 var defaultProperties = new GenericTextRunProperties(typeface);
                 var textSource = new CustomTextBufferTextSource(
-                    new TextCharacters("He", defaultProperties), 
+                    new TextCharacters("He", defaultProperties),
                     new TextCharacters("Wo", defaultProperties),
                     new TextCharacters("ff", defaultProperties));
 
@@ -1249,6 +1248,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             }
         }
 
+        [Fact]
+        public void Should_Get_In_Cluster_Backspace_Hit()
+        {
+            using (Start())
+            {
+                var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
+                var defaultProperties = new GenericTextRunProperties(typeface);
+                var textSource = new SingleBufferTextSource("ff", defaultProperties);
+
+                var formatter = new TextFormatterImpl();
+
+                var textLine =
+                    formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+                        new GenericTextParagraphProperties(defaultProperties));
+
+                Assert.NotNull(textLine);
+
+                var backspaceHit = textLine.GetBackspaceCaretCharacterHit(new CharacterHit(1, 1));
+
+                Assert.Equal(1, backspaceHit.FirstCharacterIndex);
+            }
+        }
+
         private class TextHidden : TextRun
         {
             public TextHidden(int length)
@@ -1272,11 +1294,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             {
                 var pos = 0;
 
-                for(var i = 0; i < _textRuns.Count; i++)
+                for (var i = 0; i < _textRuns.Count; i++)
                 {
                     var currentRun = _textRuns[i];
 
-                    if(pos + currentRun.Length > textSourceIndex)
+                    if (pos + currentRun.Length > textSourceIndex)
                     {
                         return currentRun;
                     }
@@ -1638,7 +1660,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.True(firstBounds.TextRunBounds.Count > 0);
             }
         }
-        
+
         [Fact]
         public void Should_GetTextBounds_NotInfiniteLoop()
         {
@@ -1809,7 +1831,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
 
-                var textSource = new TextFormatterTests.ListTextSource(new TextHidden(1) ,new TextCharacters(text, defaultProperties));
+                var textSource = new TextFormatterTests.ListTextSource(new TextHidden(1), new TextCharacters(text, defaultProperties));
 
                 var formatter = new TextFormatterImpl();
 
@@ -1925,7 +1947,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textPosition = 0;
 
-                while(textPosition < text.Length)
+                while (textPosition < text.Length)
                 {
                     var bounds = textLine.GetTextBounds(textPosition, 1);
 
@@ -2081,7 +2103,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 foreach (var glyphInfo in firstRun.ShapedBuffer)
                 {
-                    if(lastCluster != glyphInfo.GlyphCluster)
+                    if (lastCluster != glyphInfo.GlyphCluster)
                     {
                         clusterWidth.Add(currentAdvance);
                         distances.Add(currentDistance);