Browse Source

Merge pull request #12239 from Gillibald/reworkTextLineHitTesting

Rework TextLine.GetNext/PreviousCharacterHit
pull/12245/head
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
8328ac0f6e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 321
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  3. 2
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  4. 116
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

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

@ -424,13 +424,13 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{ {
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
if (characterHit.TrailingLength != 0) if (characterHit.TrailingLength != 0)
{ {
return new CharacterHit(characterHit.FirstCharacterIndex); return previousCharacterHit;
} }
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
return new CharacterHit(previousCharacterHit.FirstCharacterIndex); return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
} }

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

@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting
internal static Comparer<TextBounds> TextBoundsComparer { get; } = internal static Comparer<TextBounds> TextBoundsComparer { get; } =
Comparer<TextBounds>.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left)); Comparer<TextBounds>.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left));
private IReadOnlyList<IndexedTextRun>? _indexedTextRuns; internal IReadOnlyList<IndexedTextRun>? _indexedTextRuns;
private readonly TextRun[] _textRuns; private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth; private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
@ -512,38 +512,45 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{ {
if (_textRuns.Length == 0) if (_textRuns.Length == 0 || _indexedTextRuns is null)
{ {
return new CharacterHit(); return new CharacterHit();
} }
if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) var currentCharacterrHit = characterHit;
{ var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
return nextCharacterHit;
}
var lastTextPosition = FirstTextSourceIndex + Length;
// Can't move, we're after the last character var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Forward, out var currentPosition);
var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition);
var currentRun = _textRuns[runIndex]; var nextCharacterHit = characterHit;
switch (currentRun) switch (currentRun)
{ {
case ShapedTextRun shapedRun: case ShapedTextRun shapedRun:
{ {
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster - characterHit.TrailingLength);
if (offset > 0)
{
currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(currentCharacterrHit);
if (offset > 0)
{
nextCharacterHit = new CharacterHit(nextCharacterHit.FirstCharacterIndex + offset, nextCharacterHit.TrailingLength);
}
break; break;
} }
default: case TextRun:
{ {
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length); nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
break; break;
} }
} }
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) if (characterIndex == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
{ {
return characterHit; return characterHit;
} }
@ -554,17 +561,75 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{ {
if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit)) 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 previousCharacterHit; 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 (characterHit.FirstCharacterIndex <= FirstTextSourceIndex) if (characterIndex == previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength)
{ {
characterHit = new CharacterHit(FirstTextSourceIndex); return characterHit;
} }
return characterHit; // Can't move, we're before the first character return previousCharacterHit;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -1009,161 +1074,7 @@ namespace Avalonia.Media.TextFormatting
if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
{ {
_textLineBreak = new TextLineBreak(textEndOfLine); _textLineBreak = new TextLineBreak(textEndOfLine);
}
}
/// <summary>
/// Tries to find the next character hit.
/// </summary>
/// <param name="characterHit">The current character hit.</param>
/// <param name="nextCharacterHit">The next character hit.</param>
/// <returns></returns>
private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit)
{
nextCharacterHit = characterHit;
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lastCodepointIndex = FirstTextSourceIndex + Length;
if (codepointIndex >= lastCodepointIndex)
{
return false; // Cannot go forward anymore
}
if (codepointIndex < FirstTextSourceIndex)
{
codepointIndex = FirstTextSourceIndex;
}
var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Length)
{
var currentRun = _textRuns[runIndex];
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length;
if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight)
{
nextCharacterHit = foundCharacterHit;
return true;
}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
{
return true;
}
break;
}
default:
{
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (textPosition == currentPosition)
{
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
return true;
}
break;
}
}
currentPosition += currentRun.Length;
runIndex++;
}
return false;
}
/// <summary>
/// Tries to find the previous character hit.
/// </summary>
/// <param name="characterHit">The current character hit.</param>
/// <param name="previousCharacterHit">The previous character hit.</param>
/// <returns></returns>
private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (characterIndex == FirstTextSourceIndex)
{
previousCharacterHit = new CharacterHit(FirstTextSourceIndex);
return true;
}
previousCharacterHit = characterHit;
if (characterIndex < FirstTextSourceIndex)
{
return false; // Cannot go backward anymore.
}
var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
while (runIndex >= 0)
{
var currentRun = _textRuns[runIndex];
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
{
previousCharacterHit = foundCharacterHit;
return true;
}
var previousPosition = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength;
if (foundCharacterHit.TrailingLength > 0 && previousPosition == characterIndex)
{
previousCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
if (previousCharacterHit != characterHit)
{
return true;
}
break;
}
default:
{
if (characterIndex == currentPosition + currentRun.Length)
{
previousCharacterHit = new CharacterHit(currentPosition);
return true;
}
break;
}
}
currentPosition -= currentRun.Length;
runIndex--;
} }
return false;
} }
/// <summary> /// <summary>
@ -1173,15 +1084,23 @@ namespace Avalonia.Media.TextFormatting
/// <param name="direction">The logical direction.</param> /// <param name="direction">The logical direction.</param>
/// <param name="textPosition">The text position of the found run index.</param> /// <param name="textPosition">The text position of the found run index.</param>
/// <returns>The text run index.</returns> /// <returns>The text run index.</returns>
private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition) private TextRun? GetRunAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
{ {
var runIndex = 0; var runIndex = 0;
textPosition = FirstTextSourceIndex; textPosition = FirstTextSourceIndex;
if (_indexedTextRuns is null)
{
return null;
}
TextRun? currentRun = null;
TextRun? previousRun = null; TextRun? previousRun = null;
while (runIndex < _textRuns.Length) while (runIndex < _indexedTextRuns.Count)
{ {
var currentRun = _textRuns[runIndex]; var indexedRun = _indexedTextRuns[runIndex];
currentRun = indexedRun.TextRun;
switch (currentRun) switch (currentRun)
{ {
@ -1189,64 +1108,49 @@ namespace Avalonia.Media.TextFormatting
{ {
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
if (firstCluster > codepointIndex) firstCluster += Math.Max(0, indexedRun.TextSourceCharacterIndex - firstCluster);
{
break;
}
if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
{
if (shapedRun.ShapedBuffer.IsLeftToRight)
{
if (firstCluster >= codepointIndex)
{
return --runIndex;
}
}
else
{
if (codepointIndex > firstCluster + currentRun.Length)
{
return --runIndex;
}
}
}
if (direction == LogicalDirection.Forward) if (direction == LogicalDirection.Forward)
{ {
if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length) if (codepointIndex >= firstCluster && codepointIndex < firstCluster + currentRun.Length)
{ {
return runIndex; return currentRun;
} }
} }
else else
{ {
if (codepointIndex > firstCluster && if (previousRun is not null && previousRun is not ShapedTextRun && codepointIndex == textPosition + firstCluster)
codepointIndex <= firstCluster + currentRun.Length) {
textPosition -= previousRun.Length;
return previousRun;
}
if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length)
{ {
return runIndex; return currentRun;
} }
} }
if (runIndex + 1 >= _textRuns.Length) if (runIndex + 1 >= _textRuns.Length)
{ {
return runIndex; return currentRun;
} }
textPosition += currentRun.Length; textPosition += currentRun.Length;
break; break;
} }
default: case TextRun:
{ {
if (codepointIndex == textPosition) if (codepointIndex == textPosition)
{ {
return runIndex; return currentRun;
} }
if (runIndex + 1 >= _textRuns.Length) if (runIndex + 1 >= _textRuns.Length)
{ {
return runIndex; return currentRun;
} }
textPosition += currentRun.Length; textPosition += currentRun.Length;
@ -1257,10 +1161,11 @@ namespace Avalonia.Media.TextFormatting
} }
runIndex++; runIndex++;
previousRun = currentRun; previousRun = currentRun;
} }
return runIndex; return currentRun;
} }
private TextLineMetrics CreateLineMetrics() private TextLineMetrics CreateLineMetrics()

2
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@ -111,7 +111,7 @@ namespace Avalonia.Base.UnitTests.Media
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel)) using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{ {
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength)); var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex + currentLength));
Assert.Equal(previousIndex, characterHit.FirstCharacterIndex); Assert.Equal(previousIndex, characterHit.FirstCharacterIndex);

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

@ -194,7 +194,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
for (var i = 0; i < clusters.Count; i++) for (var i = 0; i < clusters.Count; i++)
{ {
var expectedCluster = clusters[i]; var expectedCluster = clusters[i];
var actualCluster = nextCharacterHit.FirstCharacterIndex; var actualCluster = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
Assert.Equal(expectedCluster, actualCluster); Assert.Equal(expectedCluster, actualCluster);
@ -278,16 +278,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(clusters[i], Assert.Equal(clusters[i],
previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength); previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength);
} }
firstCharacterHit = previousCharacterHit;
firstCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex);
Assert.Equal(0, previousCharacterHit.TrailingLength);
} }
} }
@ -728,6 +718,110 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[Fact]
public void Should_GetNextCaretCharacterHit_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(9, 1));
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
Assert.Equal(11, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(19, 1));
Assert.Equal(20, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(10));
Assert.Equal(11, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
Assert.Equal(12, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(20));
Assert.Equal(21, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
}
}
[Fact]
public void Should_GetPreviousCaretCharacterHit_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(20, 1));
Assert.Equal(19, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(10, 1));
Assert.Equal(9, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
Assert.Equal(8, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(21));
Assert.Equal(20, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(11));
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
Assert.Equal(9, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
}
}
private class MixedTextBufferTextSource : ITextSource private class MixedTextBufferTextSource : ITextSource
{ {
public TextRun? GetTextRun(int textSourceIndex) public TextRun? GetTextRun(int textSourceIndex)

Loading…
Cancel
Save