diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index a8898782e5..24dbb65492 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/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; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index b7b6390242..fd48e3112a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/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(() => 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);