Browse Source

Fix text hit testing for invisible runs (#13135)

* Repro unit test for GetCharacterHitFromDistance being broken with hidden runs

* Repro for infinite loop in GetTextBounds

* Fix failing tests

* Fix GetRunBoundsRightToLeft

---------

Co-authored-by: Nikita Tsukanov <keks9n@gmail.com>
pull/13137/head
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
f8ec196e2f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  2. 93
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

31
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -290,6 +290,12 @@ namespace Avalonia.Media.TextFormatting
continue;
}
}
else
{
currentPosition += currentRun.Length;
continue;
}
break;
}
@ -990,6 +996,12 @@ namespace Avalonia.Media.TextFormatting
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
//Make sure we properly deal with zero width space runs
if (characterLength == 0 && currentRun.Length > 0 && currentRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace == 0)
{
characterLength = currentRun.Length;
}
if (endX < startX)
{
(endX, startX) = (startX, endX);
@ -1003,7 +1015,9 @@ namespace Avalonia.Media.TextFormatting
var runWidth = endX - startX;
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
var textSourceIndex = offset + startHit.FirstCharacterIndex;
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
}
private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX,
@ -1038,6 +1052,17 @@ namespace Avalonia.Media.TextFormatting
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
//Make sure we properly deal with zero width space runs
if (characterLength == 0 && currentRun.Length > 0 && currentRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace == 0)
{
characterLength = currentRun.Length;
}
if(startHit.FirstCharacterIndex > endHit.FirstCharacterIndex)
{
startHit = endHit;
}
if (endX < startX)
{
(endX, startX) = (startX, endX);
@ -1051,7 +1076,9 @@ namespace Avalonia.Media.TextFormatting
var runWidth = endX - startX;
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
var textSourceIndex = offset + startHit.FirstCharacterIndex;
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), textSourceIndex, characterLength, currentRun);
}
public override void Dispose()

93
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -706,6 +706,64 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
}
}
[Fact]
public void Should_HitTestStringWithInvisibleRuns()
{
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
//var textSource = new ListTextSource(
using (Start())
{
var hello = new TextCharacters("Hello",
new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
var world = new TextCharacters("world",
new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Red));
var source = new ListTextSource(new InvisibleRun(1), hello, new InvisibleRun(1), world);
var textLine =
TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties);
void VerifyHit(int offset)
{
var glyphCenter = textLine.GetTextBounds(offset, 1)[0].Rectangle.Center;
var hit = textLine.GetCharacterHitFromDistance(glyphCenter.X);
Assert.Equal(offset, hit.FirstCharacterIndex);
}
VerifyHit(3);
VerifyHit(8);
}
}
[Fact]
public void GetTextBounds_For_TextLine_With_ZeroWidthSpaces_Does_Not_Freeze()
{
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
using (Start())
{
var text = new TextCharacters("\u200B\u200B",
new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
var source = new ListTextSource(text, new InvisibleRun(1), new TextEndOfParagraph());
var textLine =
TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties);
var bounds = textLine.GetTextBounds(0, 3);
Assert.Equal(1, bounds.Count);
var runBounds = bounds[0].TextRunBounds;
Assert.Equal(2, runBounds.Count);
}
}
protected readonly record struct SimpleTextSource : ITextSource
{
@ -776,6 +834,32 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return new TextCharacters(_text, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
}
}
private class ListTextSource : ITextSource
{
private Dictionary<int, TextRun> _runs = new();
public ListTextSource(params TextRun[] runs) : this((IEnumerable<TextRun>)runs)
{
}
public ListTextSource(IEnumerable<TextRun> runs)
{
var off = 0;
foreach (var r in runs)
{
_runs[off] = r;
off += r.Length;
}
}
public TextRun GetTextRun(int textSourceIndex)
{
_runs.TryGetValue(textSourceIndex, out var rv);
return rv;
}
}
private class RectangleRun : DrawableTextRun
{
@ -798,6 +882,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
}
private class InvisibleRun : TextRun
{
public InvisibleRun(int length)
{
Length = length;
}
public override int Length { get; }
}
public static IDisposable Start()
{

Loading…
Cancel
Save