Browse Source

[Text] Fix hit testing issues (#13155)

* Repro for Line_Formatting_For_Oversized_Embedded_Runs_Does_Not_Freeze

* Repro for Line_With_IncrementalTab_Should_Return_Correct_Backspace_Position

* Fix GetBackspaceCaretCharacterHit
Fix GetPreviousCaretCharacterHit
Fix WrapWithOverflow for not text runs

* Fix custom font manager

* Move DejaVuSans to a different location to prevent using it as a fallback

---------

Co-authored-by: Nikita Tsukanov <keks9n@gmail.com>
pull/13193/head
Benedikt Stebner 2 years ago
committed by GitHub
parent
commit
b83a5eb8b7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 24
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  3. 184
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  4. 2
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  5. BIN
      tests/Avalonia.Skia.UnitTests/Fonts/DejaVuSans.ttf
  6. 57
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  7. 24
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

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

@ -424,14 +424,20 @@ namespace Avalonia.Media
/// </returns>
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
//Always produce a hit that is on the left edge
if (characterHit.TrailingLength != 0)
if (characterHit.TrailingLength > 0)
{
return previousCharacterHit;
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
}
else
{
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
}
}
/// <summary>

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

@ -713,9 +713,31 @@ namespace Avalonia.Media.TextFormatting
var measuredLength = MeasureLength(textRuns, paragraphWidth);
if(measuredLength == 0)
if(measuredLength == 0 && paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{
for (int i = 0; i < textRuns.Count; i++)
{
measuredLength += textRuns[i].Length;
}
TextLineBreak? textLineBreak;
if (currentLineBreak?.TextEndOfLine is { } textEndOfLine)
{
textLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
}
else
{
textLineBreak = null;
}
var textLine = new TextLineImpl(textRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
textLineBreak);
textLine.FinalizeLine();
return textLine;
}
var currentLength = 0;

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

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@ -567,82 +568,13 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{
if (_textRuns.Length == 0 || _indexedTextRuns is null)
{
return new CharacterHit();
}
if (characterHit.TrailingLength > 0 && characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
{
return new CharacterHit(FirstTextSourceIndex);
}
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (characterIndex <= FirstTextSourceIndex)
{
return new CharacterHit(FirstTextSourceIndex);
}
var currentCharacterrHit = characterHit;
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
if (currentPosition == characterHit.FirstCharacterIndex)
{
currentRun = GetRunAtCharacterIndex(characterHit.FirstCharacterIndex, LogicalDirection.Backward, out currentPosition);
}
var previousCharacterHit = characterHit;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
if (offset > 0)
{
currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterrHit);
if (offset > 0)
{
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength);
}
break;
}
case TextRun:
{
if (characterHit.TrailingLength > 0)
{
previousCharacterHit = new CharacterHit(currentPosition, currentRun.Length);
}
else
{
previousCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
}
break;
}
}
if (characterIndex == previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength)
{
return characterHit;
}
return previousCharacterHit;
return GetPreviousCharacterHit(characterHit, false);
}
/// <inheritdoc/>
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
{
// same operation as move-to-previous
return GetPreviousCaretCharacterHit(characterHit);
return GetPreviousCharacterHit(characterHit, true);
}
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
@ -823,6 +755,95 @@ namespace Avalonia.Media.TextFormatting
return result;
}
private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries)
{
if (_textRuns.Length == 0 || _indexedTextRuns is null)
{
return new CharacterHit();
}
if (characterHit.TrailingLength > 0 && characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
{
return new CharacterHit(FirstTextSourceIndex);
}
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (characterIndex <= FirstTextSourceIndex)
{
return new CharacterHit(FirstTextSourceIndex);
}
var currentCharacterHit = characterHit;
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
var previousCharacterHit = characterHit;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
if (offset > 0)
{
currentCharacterHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterHit);
if (useGraphemeBoundaries)
{
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))
{
if (length + grapheme.Length < clusterLength)
{
length += grapheme.Length;
continue;
}
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + length);
break;
}
}
if (offset > 0)
{
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength);
}
break;
}
case TextRun:
{
previousCharacterHit = new CharacterHit(currentPosition);
break;
}
}
if (characterIndex == previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength)
{
return characterHit;
}
return previousCharacterHit;
}
private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX,
int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
{
@ -1146,13 +1167,6 @@ namespace Avalonia.Media.TextFormatting
}
else
{
if (previousRun is not null && previousRun is not ShapedTextRun && codepointIndex == textPosition + firstCluster)
{
textPosition -= previousRun.Length;
return previousRun;
}
if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length)
{
return currentRun;
@ -1170,9 +1184,19 @@ namespace Avalonia.Media.TextFormatting
}
case TextRun:
{
if (codepointIndex == textPosition)
if(direction == LogicalDirection.Forward)
{
return currentRun;
if (textPosition == codepointIndex)
{
return currentRun;
}
}
else
{
if (textPosition + currentRun.Length == codepointIndex)
{
return currentRun;
}
}
if (runIndex + 1 >= _textRuns.Length)

2
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@ -10,6 +10,8 @@
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />
<None Remove="Fonts\DejaVuSans.ttf" />
<EmbeddedResource Include="Fonts\DejaVuSans.ttf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />

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

Binary file not shown.

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

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests;
@ -765,6 +766,62 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Line_Formatting_For_Oversized_Embedded_Runs_Does_Not_Produce_Empty_Lines()
{
var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties,
textWrap: TextWrapping.WrapWithOverflow);
using (Start())
{
var source = new ListTextSource(new RectangleRun(new Rect(0, 0, 200, 10), Brushes.Aqua));
var textLine = TextFormatter.Current.FormatLine(source, 0, 100, paragraphProperties);
Assert.Equal(200d, textLine.WidthIncludingTrailingWhitespace);
}
}
class IncrementalTabProperties : TextParagraphProperties
{
public IncrementalTabProperties(TextRunProperties defaultTextRunProperties)
{
DefaultTextRunProperties = defaultTextRunProperties;
}
public override FlowDirection FlowDirection { get; }
public override TextAlignment TextAlignment { get; }
public override double LineHeight { get; }
public override bool FirstLineInParagraph { get; }
public override TextRunProperties DefaultTextRunProperties { get; }
public override TextWrapping TextWrapping { get; }
public override double Indent { get; }
public override double DefaultIncrementalTab => 64;
}
[Fact]
public void Line_With_IncrementalTab_Should_Return_Correct_Backspace_Position()
{
using (Start())
{
var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DejaVu Sans"));
var defaultRunProperties = new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black);
var paragraphProperties = new IncrementalTabProperties(defaultRunProperties);
var text = new TextCharacters("ff",
new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black));
var source = new ListTextSource(text);
var textLine = TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties);
var backspaceHit = textLine.GetBackspaceCaretCharacterHit(new CharacterHit(2));
Assert.Equal(1, backspaceHit.FirstCharacterIndex);
Assert.Equal(0, backspaceHit.TrailingLength);
}
}
protected readonly record struct SimpleTextSource : ITextSource
{
private readonly string _text;

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

@ -469,18 +469,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
Assert.Equal(2, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength);
Assert.Equal(3, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
Assert.Equal(1, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength);
Assert.Equal(2, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
Assert.Equal(0, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength);
Assert.Equal(1, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
@ -786,21 +786,21 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(20, 1));
Assert.Equal(19, characterHit.FirstCharacterIndex);
Assert.Equal(20, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(10, 1));
Assert.Equal(9, characterHit.FirstCharacterIndex);
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
Assert.Equal(8, characterHit.FirstCharacterIndex);
Assert.Equal(9, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(21));

Loading…
Cancel
Save