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 3 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> /// </returns>
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) 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> /// <summary>

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

@ -713,9 +713,31 @@ namespace Avalonia.Media.TextFormatting
var measuredLength = MeasureLength(textRuns, paragraphWidth); 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; var currentLength = 0;

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

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
@ -567,82 +568,13 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{ {
if (_textRuns.Length == 0 || _indexedTextRuns is null) return GetPreviousCharacterHit(characterHit, false);
{
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;
} }
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
{ {
// same operation as move-to-previous return GetPreviousCharacterHit(characterHit, true);
return GetPreviousCaretCharacterHit(characterHit);
} }
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength) public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
@ -823,6 +755,95 @@ namespace Avalonia.Media.TextFormatting
return result; 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, private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX,
int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
{ {
@ -1146,13 +1167,6 @@ namespace Avalonia.Media.TextFormatting
} }
else 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) if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length)
{ {
return currentRun; return currentRun;
@ -1170,9 +1184,19 @@ namespace Avalonia.Media.TextFormatting
} }
case TextRun: 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) if (runIndex + 1 >= _textRuns.Length)

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

@ -10,6 +10,8 @@
<Import Project="..\..\build\SharedVersion.props" /> <Import Project="..\..\build\SharedVersion.props" />
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" /> <EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />
<None Remove="Fonts\DejaVuSans.ttf" />
<EmbeddedResource Include="Fonts\DejaVuSans.ttf" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" /> <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.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.UnitTests; 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 protected readonly record struct SimpleTextSource : ITextSource
{ {
private readonly string _text; 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)); var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
Assert.Equal(2, currentHit.FirstCharacterIndex); Assert.Equal(3, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength); Assert.Equal(0, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
Assert.Equal(1, currentHit.FirstCharacterIndex); Assert.Equal(2, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength); Assert.Equal(0, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
Assert.Equal(0, currentHit.FirstCharacterIndex); Assert.Equal(1, currentHit.FirstCharacterIndex);
Assert.Equal(1, currentHit.TrailingLength); Assert.Equal(0, currentHit.TrailingLength);
currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
@ -786,21 +786,21 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(20, 1)); 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)); 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); 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)); characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(21));

Loading…
Cancel
Save