Browse Source

Adjust GetBackspaceCaretCharacterHit (#19586)

* Adjust GetBackspaceCaretCharacterHit implementation so it works with codepoint boundaries

* Add a unit test
pull/19934/head
Benedikt Stebner 12 months ago
committed by GitHub
parent
commit
c5efedb81a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 50
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  2. 48
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

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

@ -814,7 +814,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool useGraphemeBoundaries) private CharacterHit GetPreviousCharacterHit(CharacterHit characterHit, bool isBackspaceDelete)
{ {
if (_textRuns.Length == 0 || _indexedTextRuns is null) if (_textRuns.Length == 0 || _indexedTextRuns is null)
{ {
@ -833,8 +833,6 @@ namespace Avalonia.Media.TextFormatting
return new CharacterHit(FirstTextSourceIndex); return new CharacterHit(FirstTextSourceIndex);
} }
var currentCharacterHit = characterHit;
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition); var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
var previousCharacterHit = characterHit; var previousCharacterHit = characterHit;
@ -843,46 +841,38 @@ namespace Avalonia.Media.TextFormatting
{ {
case ShapedTextRun shapedRun: case ShapedTextRun shapedRun:
{ {
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster); //Determine the start of the first hit in local positions.
var runOffset = Math.Max(0, characterIndex - currentPosition);
if (offset > 0) var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
{
currentCharacterHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterHit); //Current position is a text source index and first cluster is relative to the GlyphRun's buffer.
var textSourceOffset = currentPosition - firstCluster;
if (useGraphemeBoundaries) if (isBackspaceDelete)
{ {
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 length = 0;
var clusterLength = Math.Max(0, currentCharacterHit.FirstCharacterIndex + currentCharacterHit.TrailingLength - while (Codepoint.ReadAt(shapedRun.GlyphRun.Characters.Span, length, out var count) != Codepoint.ReplacementCodepoint)
previousCharacterHit.FirstCharacterIndex - previousCharacterHit.TrailingLength);
while (graphemeEnumerator.MoveNext(out var grapheme))
{ {
if (length + grapheme.Length < clusterLength) if (length + count >= runOffset)
{ {
length += grapheme.Length; break;
continue;
} }
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + length); length += count;
break;
} }
}
if (offset > 0) previousCharacterHit = new CharacterHit(characterIndex - runOffset + length);
}
else
{ {
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength); previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(new CharacterHit(firstCluster + runOffset));
if(textSourceOffset > 0)
{
previousCharacterHit = new CharacterHit(textSourceOffset + previousCharacterHit.FirstCharacterIndex, previousCharacterHit.TrailingLength);
}
} }
break; break;

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

@ -9,7 +9,6 @@ using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
@ -905,7 +904,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.NotNull(textLine); Assert.NotNull(textLine);
Assert.Throws<ArgumentOutOfRangeException>(() => textLine.GetTextBounds(0, 0)); Assert.Throws<ArgumentOutOfRangeException>(() => textLine.GetTextBounds(0, 0));
} }
} }
[Fact] [Fact]
@ -983,12 +982,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(typeface); var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource( var textSource = new CustomTextBufferTextSource(
new TextHidden(1), new TextHidden(1),
new TextCharacters("Authenti", defaultProperties), new TextCharacters("Authenti", defaultProperties),
new TextHidden(1), new TextHidden(1),
new TextHidden(1), new TextHidden(1),
new TextCharacters("ff", defaultProperties), new TextCharacters("ff", defaultProperties),
new TextHidden(1), new TextHidden(1),
new TextHidden(1)); new TextHidden(1));
var formatter = new TextFormatterImpl(); var formatter = new TextFormatterImpl();
@ -1138,7 +1137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope")); var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
var defaultProperties = new GenericTextRunProperties(typeface); var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource( var textSource = new CustomTextBufferTextSource(
new TextCharacters("He", defaultProperties), new TextCharacters("He", defaultProperties),
new TextCharacters("Wo", defaultProperties), new TextCharacters("Wo", defaultProperties),
new TextCharacters("ff", defaultProperties)); new TextCharacters("ff", defaultProperties));
@ -1249,6 +1248,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[Fact]
public void Should_Get_In_Cluster_Backspace_Hit()
{
using (Start())
{
var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope"));
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new SingleBufferTextSource("ff", defaultProperties);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
Assert.NotNull(textLine);
var backspaceHit = textLine.GetBackspaceCaretCharacterHit(new CharacterHit(1, 1));
Assert.Equal(1, backspaceHit.FirstCharacterIndex);
}
}
private class TextHidden : TextRun private class TextHidden : TextRun
{ {
public TextHidden(int length) public TextHidden(int length)
@ -1272,11 +1294,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
var pos = 0; var pos = 0;
for(var i = 0; i < _textRuns.Count; i++) for (var i = 0; i < _textRuns.Count; i++)
{ {
var currentRun = _textRuns[i]; var currentRun = _textRuns[i];
if(pos + currentRun.Length > textSourceIndex) if (pos + currentRun.Length > textSourceIndex)
{ {
return currentRun; return currentRun;
} }
@ -1638,7 +1660,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.True(firstBounds.TextRunBounds.Count > 0); Assert.True(firstBounds.TextRunBounds.Count > 0);
} }
} }
[Fact] [Fact]
public void Should_GetTextBounds_NotInfiniteLoop() public void Should_GetTextBounds_NotInfiniteLoop()
{ {
@ -1809,7 +1831,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
var defaultProperties = new GenericTextRunProperties(Typeface.Default); var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new TextFormatterTests.ListTextSource(new TextHidden(1) ,new TextCharacters(text, defaultProperties)); var textSource = new TextFormatterTests.ListTextSource(new TextHidden(1), new TextCharacters(text, defaultProperties));
var formatter = new TextFormatterImpl(); var formatter = new TextFormatterImpl();
@ -1925,7 +1947,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textPosition = 0; var textPosition = 0;
while(textPosition < text.Length) while (textPosition < text.Length)
{ {
var bounds = textLine.GetTextBounds(textPosition, 1); var bounds = textLine.GetTextBounds(textPosition, 1);
@ -2081,7 +2103,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var glyphInfo in firstRun.ShapedBuffer) foreach (var glyphInfo in firstRun.ShapedBuffer)
{ {
if(lastCluster != glyphInfo.GlyphCluster) if (lastCluster != glyphInfo.GlyphCluster)
{ {
clusterWidth.Add(currentAdvance); clusterWidth.Add(currentAdvance);
distances.Add(currentDistance); distances.Add(currentDistance);

Loading…
Cancel
Save