Browse Source

Adjust GetBackspaceCaretCharacterHit (#19586)

* Adjust GetBackspaceCaretCharacterHit implementation so it works with codepoint boundaries

* Add a unit test
pull/19934/head
Benedikt Stebner 4 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)
{
@ -833,8 +833,6 @@ namespace Avalonia.Media.TextFormatting
return new CharacterHit(FirstTextSourceIndex);
}
var currentCharacterHit = characterHit;
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
var previousCharacterHit = characterHit;
@ -843,46 +841,38 @@ namespace Avalonia.Media.TextFormatting
{
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)
{
currentCharacterHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
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 clusterLength = Math.Max(0, currentCharacterHit.FirstCharacterIndex + currentCharacterHit.TrailingLength -
previousCharacterHit.FirstCharacterIndex - previousCharacterHit.TrailingLength);
while (graphemeEnumerator.MoveNext(out var grapheme))
while (Codepoint.ReadAt(shapedRun.GlyphRun.Characters.Span, length, out var count) != Codepoint.ReplacementCodepoint)
{
if (length + grapheme.Length < clusterLength)
if (length + count >= runOffset)
{
length += grapheme.Length;
continue;
break;
}
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + length);
break;
length += count;
}
}
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;

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

@ -9,7 +9,6 @@ using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
using Xunit;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
@ -905,7 +904,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.NotNull(textLine);
Assert.Throws<ArgumentOutOfRangeException>(() => textLine.GetTextBounds(0, 0));
}
}
}
[Fact]
@ -983,12 +982,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(
new TextHidden(1),
new TextCharacters("Authenti", defaultProperties),
new TextHidden(1),
new TextHidden(1),
new TextCharacters("Authenti", defaultProperties),
new TextHidden(1),
new TextHidden(1),
new TextCharacters("ff", defaultProperties),
new TextHidden(1),
new TextHidden(1),
new TextHidden(1));
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 defaultProperties = new GenericTextRunProperties(typeface);
var textSource = new CustomTextBufferTextSource(
new TextCharacters("He", defaultProperties),
new TextCharacters("He", defaultProperties),
new TextCharacters("Wo", 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
{
public TextHidden(int length)
@ -1272,11 +1294,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var pos = 0;
for(var i = 0; i < _textRuns.Count; i++)
for (var i = 0; i < _textRuns.Count; i++)
{
var currentRun = _textRuns[i];
if(pos + currentRun.Length > textSourceIndex)
if (pos + currentRun.Length > textSourceIndex)
{
return currentRun;
}
@ -1638,7 +1660,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.True(firstBounds.TextRunBounds.Count > 0);
}
}
[Fact]
public void Should_GetTextBounds_NotInfiniteLoop()
{
@ -1809,7 +1831,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
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();
@ -1925,7 +1947,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textPosition = 0;
while(textPosition < text.Length)
while (textPosition < text.Length)
{
var bounds = textLine.GetTextBounds(textPosition, 1);
@ -2081,7 +2103,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var glyphInfo in firstRun.ShapedBuffer)
{
if(lastCluster != glyphInfo.GlyphCluster)
if (lastCluster != glyphInfo.GlyphCluster)
{
clusterWidth.Add(currentAdvance);
distances.Add(currentDistance);

Loading…
Cancel
Save