Browse Source

Some more hit testing fixes

pull/7946/head
Benedikt Stebner 4 years ago
parent
commit
3e6bc0b48d
  1. 44
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  3. 74
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  4. 97
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

44
src/Avalonia.Base/Media/GlyphRun.cs

@ -49,7 +49,7 @@ namespace Avalonia.Media
IReadOnlyList<int>? glyphClusters = null,
int biDiLevel = 0)
{
_glyphTypeface = glyphTypeface;
_glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
@ -204,7 +204,7 @@ namespace Avalonia.Media
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var distance = 0.0;
if (IsLeftToRight)
@ -223,7 +223,7 @@ namespace Avalonia.Media
}
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
var currentCluster = GlyphClusters[glyphIndex];
@ -249,7 +249,7 @@ namespace Avalonia.Media
{
//RightToLeft
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
if (characterIndex > GlyphClusters[0])
@ -284,13 +284,13 @@ namespace Avalonia.Media
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
var characterIndex = 0;
// Before
if (distance <= 0)
{
isInside = false;
if(GlyphClusters != null)
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
@ -307,11 +307,11 @@ namespace Avalonia.Media
characterIndex = GlyphIndices.Count - 1;
if(GlyphClusters != null)
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
@ -327,7 +327,7 @@ namespace Avalonia.Media
var advance = GetGlyphAdvance(index, out var cluster);
characterIndex = cluster;
if (distance > currentX && distance <= currentX + advance)
{
break;
@ -345,7 +345,7 @@ namespace Avalonia.Media
var advance = GetGlyphAdvance(index, out var cluster);
characterIndex = cluster;
if (currentX - advance < distance)
{
break;
@ -554,6 +554,16 @@ namespace Avalonia.Media
nextCluster = GlyphClusters[currentIndex];
}
if (nextCluster < Characters.Start)
{
nextCluster = Characters.Start;
}
if (cluster < Characters.Start)
{
cluster = Characters.Start;
}
int trailingLength;
if (nextCluster == cluster)
@ -577,7 +587,7 @@ namespace Avalonia.Media
private double GetGlyphAdvance(int index, out int cluster)
{
cluster = GlyphClusters != null ? GlyphClusters[index] : index;
if (GlyphAdvances != null)
{
return GlyphAdvances[index];
@ -603,7 +613,7 @@ namespace Avalonia.Media
var widthIncludingTrailingWhitespace = 0d;
var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
for (var index = 0; index < GlyphIndices.Count; index++)
{
var advance = GetGlyphAdvance(index, out _);
@ -615,7 +625,7 @@ namespace Avalonia.Media
if (IsLeftToRight)
{
for (var index = GlyphIndices.Count - glyphCount; index <GlyphIndices.Count; index++)
for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
{
width -= GetGlyphAdvance(index, out _);
}
@ -677,12 +687,12 @@ namespace Avalonia.Media
if (codepointIndex < 0)
{
trailingWhitespaceLength = _characters.Length;
glyphCount = GlyphClusters.Count;
break;
}
var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
if (!codepoint.IsWhiteSpace)
@ -696,7 +706,7 @@ namespace Avalonia.Media
}
trailingWhitespaceLength++;
glyphCount++;
}
}

2
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -544,7 +544,7 @@ namespace Avalonia.Media.TextFormatting
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 1, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine();
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine();
}
/// <summary>

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

@ -183,6 +183,7 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextCharacters shapedRun:
{
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
break;
}
default:
@ -426,31 +427,42 @@ namespace Avalonia.Media.TextFormatting
if (nextRun != null)
{
if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
switch (nextRun)
{
goto skip;
}
case ShapedTextCharacters when currentRun is ShapedTextCharacters:
{
if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
{
goto skip;
}
if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
{
goto skip;
}
if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
{
goto skip;
}
if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
{
goto skip;
}
if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
{
goto skip;
}
if (currentRun.Text.End < firstTextSourceCharacterIndex)
{
goto skip;
}
if (currentRun.Text.End < firstTextSourceCharacterIndex)
{
goto skip;
}
goto noop;
goto noop;
}
default:
{
goto noop;
}
}
skip:
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
}
continue;
@ -460,7 +472,6 @@ namespace Avalonia.Media.TextFormatting
}
}
var endX = startX;
var endOffset = 0d;
@ -520,11 +531,13 @@ namespace Avalonia.Media.TextFormatting
}
default:
{
if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length)
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength)
{
endX += currentRun.Size.Width;
}
currentPosition += currentRun.TextSourceLength;
break;
}
}
@ -538,7 +551,9 @@ namespace Avalonia.Media.TextFormatting
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection);
currentRect = currentRect.WithWidth(currentRect.Width + width);
var textBounds = new TextBounds(currentRect, currentDirection);
result[result.Count - 1] = textBounds;
}
@ -551,21 +566,9 @@ namespace Avalonia.Media.TextFormatting
if (currentDirection == FlowDirection.LeftToRight)
{
if (nextRun != null)
{
if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
{
break;
}
currentPosition = nextRun.Text.End;
}
else
if (currentPosition >= firstTextSourceCharacterIndex + textLength)
{
if (currentPosition >= firstTextSourceCharacterIndex + textLength)
{
break;
}
break;
}
}
else
@ -575,10 +578,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (currentPosition != currentRun.Text.Start)
{
endX += currentRun.Size.Width - endOffset;
}
endX += currentRun.Size.Width - endOffset;
}
lastDirection = currentDirection;

97
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -70,12 +70,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
}
[Fact]
public void Should_Get_Next_Caret_CharacterHit_Bidi()
{
const string text = "אבג 1 ABC";
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List<int>();
foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
{
var shapedRun = (ShapedTextCharacters)textRun;
@ -98,7 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
shapedRun.ShapedBuffer.GlyphClusters);
}
var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
foreach (var cluster in clusters)
@ -122,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public void Should_Get_Previous_Caret_CharacterHit_Bidi()
{
const string text = "אבג 1 ABC";
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List<int>();
foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
{
var shapedRun = (ShapedTextCharacters)textRun;
@ -147,13 +147,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
clusters.Reverse();
var nextCharacterHit = new CharacterHit(text.Length - 1);
foreach (var cluster in clusters)
{
var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
Assert.Equal(cluster, currentCaretIndex);
nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit);
@ -168,7 +168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
}
}
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
[InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
@ -324,7 +324,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
}
}
@ -371,7 +371,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026");
yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026");
yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, "");
object[] CreateData(string text, double width, TextTrimming mode, string expected)
{
return new object[]
@ -424,7 +424,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new DrawableRunTextSource();
var formatter = new TextFormatterImpl();
var textLine =
@ -471,7 +471,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(4, textLine.TextRuns.Count);
var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1));
var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
Assert.Equal(3, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
@ -552,11 +552,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
switch (textSourceIndex)
{
case 0:
return new CustomDrawableRun();
return new CustomDrawableRun();
case 1:
return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default));
case 2:
return new CustomDrawableRun();
return new CustomDrawableRun();
case 3:
return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default));
default:
@ -564,14 +564,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
}
private class CustomDrawableRun : DrawableTextRun
{
public override Size Size => new(14, 14);
public override double Baseline => 14;
public override void Draw(DrawingContext drawingContext, Point origin)
{
}
}
@ -587,29 +587,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var shapedTextRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
var lastCluster = -1;
foreach (var textRun in shapedTextRuns)
{
var shapedBuffer = textRun.ShapedBuffer;
var currentClusters = shapedBuffer.GlyphClusters.ToList();
foreach (var currentCluster in currentClusters)
foreach (var currentCluster in currentClusters)
{
if (lastCluster == currentCluster)
{
continue;
}
glyphClusters.Add(currentCluster);
lastCluster = currentCluster;
}
}
return glyphClusters;
}
private static List<Rect> BuildRects(TextLine textLine)
{
var rects = new List<Rect>();
@ -624,11 +624,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var textRun in shapedTextRuns)
{
var shapedBuffer = textRun.ShapedBuffer;
for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++)
{
var currentCluster = shapedBuffer.GlyphClusters[index];
var advance = shapedBuffer.GlyphAdvances[index];
if (lastCluster != currentCluster)
@ -642,10 +642,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
rects.Remove(rect);
rect = rect.WithWidth(rect.Width + advance);
rects.Add(rect);
}
currentX += advance;
lastCluster = currentCluster;
@ -655,8 +655,43 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return rects;
}
[Fact]
public void Should_Get_TextBounds()
public void Should_Get_TextBounds_Mixed()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var text = "0123".AsMemory();
var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
var textRuns = new List<TextRun>
{
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text), shaperOption), defaultProperties),
new CustomDrawableRun(),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length + 1, text.Length), shaperOption), defaultProperties),
new CustomDrawableRun(),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 2 + 2, text.Length), shaperOption), defaultProperties),
new CustomDrawableRun(),
};
var textSource = new FixedRunsTextSource(textRuns);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
}
}
[Fact]
public void Should_Get_TextBounds_BiDi()
{
using (Start())
{
@ -673,7 +708,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 3, text.Length), ltrOptions), defaultProperties)
};
var textSource = new FixedRunsTextSource(textRuns);
var formatter = new TextFormatterImpl();
@ -700,12 +735,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if(textRun.Text.Start == textSourceIndex)
if (currentPosition == textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;

Loading…
Cancel
Save