Browse Source

Perf: improved GraphemeEnumerator by avoiding double codepoint iteration

pull/10047/head
Julien Lebosquain 3 years ago
parent
commit
2f429062a1
  1. 6
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  2. 12
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  3. 20
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  4. 9
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  5. 6
      src/Avalonia.Controls/TextBox.cs
  6. 8
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
  7. 17
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

6
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -128,11 +128,9 @@ namespace Avalonia.Media.TextFormatting
var graphemeEnumerator = new GraphemeEnumerator(text);
while (graphemeEnumerator.MoveNext())
while (graphemeEnumerator.MoveNext(out var grapheme))
{
var grapheme = graphemeEnumerator.Current;
finalLength += grapheme.Text.Length;
finalLength += grapheme.Length;
if (finalLength >= length)
{

12
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -140,16 +140,14 @@ namespace Avalonia.Media.TextFormatting
var enumerator = new GraphemeEnumerator(textSpan);
while (enumerator.MoveNext())
while (enumerator.MoveNext(out var grapheme))
{
var grapheme = enumerator.Current;
if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
{
break;
}
count += grapheme.Text.Length;
count += grapheme.Length;
}
return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel);
@ -184,10 +182,8 @@ namespace Avalonia.Media.TextFormatting
var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext())
while (enumerator.MoveNext(out var currentGrapheme))
{
var currentGrapheme = enumerator.Current;
var currentScript = currentGrapheme.FirstCodepoint.Script;
if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
@ -217,7 +213,7 @@ namespace Avalonia.Media.TextFormatting
}
}
length += currentGrapheme.Text.Length;
length += currentGrapheme.Length;
}
return length > 0;

20
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@ -1,16 +1,15 @@
using System;
namespace Avalonia.Media.TextFormatting.Unicode
namespace Avalonia.Media.TextFormatting.Unicode
{
/// <summary>
/// Represents the smallest unit of a writing system of any given language.
/// </summary>
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
public Grapheme(Codepoint firstCodepoint, int offset, int length)
{
FirstCodepoint = firstCodepoint;
Text = text;
Offset = offset;
Length = length;
}
/// <summary>
@ -19,12 +18,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; }
/// <summary>
/// The text of the grapheme cluster
/// Gets the starting code unit offset of this grapheme inside its containing text.
/// </summary>
public ReadOnlySpan<char> Text { get; }
public int Offset { get; }
/// <inheritdoc />
public override string ToString()
=> Text.ToString();
/// <summary>
/// Gets the length of this grapheme, in code units.
/// </summary>
public int Length { get; }
}
}

9
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@ -198,10 +198,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
break; // nothing but trailers after the final RI
}
const uint gb9Mask =
(1U << (int)GraphemeBreakClass.Extend) |
(1U << (int)GraphemeBreakClass.ZWJ) |
(1U << (int)GraphemeBreakClass.SpacingMark);
// rules GB9, GB9a
while (_currentType is GraphemeBreakClass.Extend
or GraphemeBreakClass.ZWJ
or GraphemeBreakClass.SpacingMark)
while (((1U << (int)_currentType) & gb9Mask) != 0U)
{
ReadNextCodepoint();
}

6
src/Avalonia.Controls/TextBox.cs

@ -963,10 +963,8 @@ namespace Avalonia.Controls
var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan());
while (graphemeEnumerator.MoveNext())
while (graphemeEnumerator.MoveNext(out var grapheme))
{
var grapheme = graphemeEnumerator.Current;
if (grapheme.FirstCodepoint.IsBreakChar)
{
if (lineCount + 1 > MaxLines)
@ -979,7 +977,7 @@ namespace Avalonia.Controls
}
}
length += grapheme.Text.Length;
length += grapheme.Length;
}
if (length < input.Length)

8
tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs

@ -40,9 +40,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var enumerator = new GraphemeEnumerator(text);
enumerator.MoveNext();
enumerator.MoveNext(out var g);
var actual = enumerator.Current.Text;
var actual = text.AsSpan(g.Offset, g.Length);
bool pass = actual.Length == grapheme.Length;
@ -86,9 +86,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var count = 0;
while (enumerator.MoveNext())
while (enumerator.MoveNext(out var grapheme))
{
Assert.Equal(1, enumerator.Current.Text.Length);
Assert.Equal(1, grapheme.Length);
count++;
}

17
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@ -151,9 +151,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
while (true)
{
while (inner.MoveNext())
Grapheme grapheme;
while (inner.MoveNext(out grapheme))
{
j += inner.Current.Text.Length;
j += grapheme.Length;
if (j + i > text.Length)
{
@ -184,14 +185,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
if (!outer.MoveNext())
if (!outer.MoveNext(out grapheme))
{
break;
}
inner = new GraphemeEnumerator(text);
i += outer.Current.Text.Length;
i += grapheme.Length;
}
}
@ -979,13 +980,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var graphemeEnumerator = new GraphemeEnumerator(text);
while (graphemeEnumerator.MoveNext())
while (graphemeEnumerator.MoveNext(out var grapheme))
{
var grapheme = graphemeEnumerator.Current;
var textStyleOverrides = new[] { new ValueSpan<TextRunProperties>(i, grapheme.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
var textStyleOverrides = new[] { new ValueSpan<TextRunProperties>(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
i += grapheme.Text.Length;
i += grapheme.Length;
var layout = new TextLayout(
text,

Loading…
Cancel
Save