diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 6e7e39fe59..bebbe5d190 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1098,23 +1098,39 @@ namespace Avalonia.Media.TextFormatting var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset); - if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) + remainingLength -= characterLength; + + var runOffset = startIndex - firstCluster; + + //Make sure we are properly dealing with zero width space runs + if (remainingLength > 0 && currentRun.Text.Length > 0 && runOffset + characterLength < currentRun.Text.Length) { - //Make sure we are properly dealing with zero width space runs - var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(startIndex)); + var glyphInfos = currentRun.GlyphRun.GlyphInfos; - while (remainingLength > 0 && codepointEnumerator.MoveNext(out var codepoint)) + for (int i = runOffset + characterLength; i < glyphInfos.Count; i++) { - if (codepoint.IsWhiteSpace) + var glyphInfo = glyphInfos[i]; + + if(glyphInfo.GlyphAdvance > 0) { - characterLength++; - remainingLength--; + break; } - else + + var graphemeEnumerator = new GraphemeEnumerator(currentRun.Text.Span.Slice(runOffset + characterLength)); + + if(!graphemeEnumerator.MoveNext(out var grapheme)) { break; } - } + + characterLength += grapheme.Length - clusterOffset; + remainingLength -= grapheme.Length; + + if(remainingLength <= 0) + { + break; + } + } } if (endX < startX) @@ -1181,10 +1197,12 @@ namespace Avalonia.Media.TextFormatting var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset); - if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) + var runOffset = startIndex - offset; + + if (characterLength == 0 && currentRun.Text.Length > 0 && runOffset < currentRun.Text.Length) { //Make sure we are properly dealing with zero width space runs - var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(startIndex)); + var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(runOffset)); while (remainingLength > 0 && codepointEnumerator.MoveNext(out var codepoint)) { diff --git a/tests/Avalonia.Skia.UnitTests/Fonts/DF7segHMI.ttf b/tests/Avalonia.Skia.UnitTests/Fonts/DF7segHMI.ttf new file mode 100644 index 0000000000..7c2b47bb0f Binary files /dev/null and b/tests/Avalonia.Skia.UnitTests/Fonts/DF7segHMI.ttf differ diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index b807e1389b..1cad489f0d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.UnitTests; @@ -191,6 +192,94 @@ namespace Avalonia.Skia.UnitTests.Media } } + [Fact] + public void Should_Get_Distance_From_CharacterHit_Non_Trailing_RightToLeft() + { + const string text = "נִקּוּד"; + + using (Start()) + { + var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Inter"); + var options = new TextShaperOptions(typeface.GlyphTypeface, 14, 1); + var shapedBuffer = TextShaper.Current.ShapeText(text, options); + + var glyphRun = CreateGlyphRun(shapedBuffer); + + //Get the distance to the left side of the first glyph + var actualDistance = glyphRun.GetDistanceFromCharacterHit(new CharacterHit(text.Length)); + + var expectedDistance = 0.0; + + Assert.Equal(expectedDistance, actualDistance, 2); + + //Get the distance to the right side of the first glyph + actualDistance = glyphRun.GetDistanceFromCharacterHit(new CharacterHit(text.Length - 1)); + + expectedDistance = shapedBuffer[0].GlyphAdvance; + + Assert.Equal(expectedDistance, actualDistance, 2); + } + } + + [Fact] + public void Should_Get_Distance_From_CharacterHit_Zero_Width() + { + const string text = "נִקּוּד"; + + using (Start()) + { + var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Inter"); + var options = new TextShaperOptions(typeface.GlyphTypeface, 14, 1); + var shapedBuffer = TextShaper.Current.ShapeText(text, options); + + var glyphRun = CreateGlyphRun(shapedBuffer); + + var clusters = glyphRun.GlyphInfos.GroupBy(x => x.GlyphCluster); + + var rightSideDistances = new List(); + + var leftSideDistances = new List(); + + var currentX = 0.0; + + foreach (var cluster in clusters) + { + leftSideDistances.Add(currentX); + + currentX += cluster.Sum(x => x.GlyphAdvance); + + rightSideDistances.Add(currentX); + } + + var characterIndices = clusters.Select(x => x.First().GlyphCluster).ToList(); + + var characterHit = new CharacterHit(text.Length); + + for (var i = 0; i < characterIndices.Count; i++) + { + var characterIndex = characterIndices[i]; + + var leftSideDistance = leftSideDistances[i]; + + var leftSideCharacterHit = glyphRun.GetCharacterHitFromDistance(leftSideDistance, out _); + + var distance = glyphRun.GetDistanceFromCharacterHit(leftSideCharacterHit); + + Assert.Equal(leftSideDistance, distance, 2); + + var previousCharacterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit); + + distance = glyphRun.GetDistanceFromCharacterHit(new CharacterHit(characterIndex)); + + var rightSideDistance = rightSideDistances[i]; + + Assert.Equal(rightSideDistance, distance, 2); + + characterHit = previousCharacterHit; + } + } + } + private static List BuildRects(GlyphRun glyphRun) { var height = glyphRun.Bounds.Height; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index ae74c76194..3df6c92928 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1215,6 +1215,40 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Get_TextBounds_With_Trailing_Zero_Advance() + { + const string df7Font = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DF7segHMI"; + + using (Start()) + { + var typeface = new Typeface(df7Font); + var defaultProperties = new GenericTextRunProperties(typeface); + var textSource = new SingleBufferTextSource("3,47-=?:#", defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.NotNull(textLine); + + var textBounds = textLine.GetTextBounds(0, 2); + + Assert.NotEmpty(textBounds); + + var textRunBounds = textBounds.First().TextRunBounds; + + Assert.NotEmpty(textBounds); + + var first = textRunBounds.First(); + + Assert.Equal(0, first.TextSourceCharacterIndex); + Assert.Equal(2, first.Length); + } + } + private class TextHidden : TextRun { public TextHidden(int length)