Browse Source

Fix buffer index calculation for the zero width character handling during hit testing (#19488)

release/11.3.5
Benedikt Stebner 6 months ago
parent
commit
4b83190afe
  1. 40
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  2. BIN
      tests/Avalonia.Skia.UnitTests/Fonts/DF7segHMI.ttf
  3. 89
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  4. 34
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

40
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))
{

BIN
tests/Avalonia.Skia.UnitTests/Fonts/DF7segHMI.ttf

Binary file not shown.

89
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<double>();
var leftSideDistances = new List<double>();
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<Rect> BuildRects(GlyphRun glyphRun)
{
var height = glyphRun.Bounds.Height;

34
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)

Loading…
Cancel
Save