Browse Source

Merge pull request #10047 from MrJul/textlayout-microopts

More text layout optimizations
pull/10050/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
5992abcbe5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  2. 2
      src/Avalonia.Base/Media/FontManager.cs
  3. 74
      src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
  4. 6
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  5. 4
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  6. 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  7. 81
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  8. 4
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  9. 93
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  10. 7
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  11. 9
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  12. 5
      src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
  13. 211
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  14. 41
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  15. 117
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  16. 24
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  17. 18
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  18. 220
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  19. 244
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  20. 6
      src/Avalonia.Controls/TextBox.cs
  21. 1
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  22. 6
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  23. 6
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  24. 8
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
  25. 47
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs
  26. 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  27. 44
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  28. 4
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  29. 17
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  30. 6
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  31. 10
      tests/Avalonia.UnitTests/MockGlyphRun.cs
  32. 3
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -30,6 +30,7 @@
<InternalsVisibleTo Include="Avalonia.Desktop, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Desktop, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Direct2D1, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.OpenGL, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.OpenGL, PublicKey=$(AvaloniaPublicKey)" />

2
src/Avalonia.Base/Media/FontManager.cs

@ -132,7 +132,7 @@ namespace Avalonia.Media
{ {
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
var glyphTypeface = typeface.GlyphTypeface; var glyphTypeface = GetOrAddGlyphTypeface(typeface);
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
return true; return true;

74
src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs

@ -1,13 +1,14 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
{ {
public sealed class FamilyNameCollection : IReadOnlyList<string> public sealed class FamilyNameCollection : IReadOnlyList<string>
{ {
private readonly string[] _names;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FamilyNameCollection"/> class. /// Initializes a new instance of the <see cref="FamilyNameCollection"/> class.
/// </summary> /// </summary>
@ -20,13 +21,20 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames)); throw new ArgumentNullException(nameof(familyNames));
} }
Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim()); _names = SplitNames(familyNames);
PrimaryFamilyName = Names[0]; PrimaryFamilyName = _names[0];
HasFallbacks = Names.Count > 1; HasFallbacks = _names.Length > 1;
} }
private static string[] SplitNames(string names)
#if NET6_0_OR_GREATER
=> names.Split(',', StringSplitOptions.TrimEntries);
#else
=> Array.ConvertAll(names.Split(','), p => p.Trim());
#endif
/// <summary> /// <summary>
/// Gets the primary family name. /// Gets the primary family name.
/// </summary> /// </summary>
@ -43,14 +51,6 @@ namespace Avalonia.Media.Fonts
/// </value> /// </value>
public bool HasFallbacks { get; } public bool HasFallbacks { get; }
/// <summary>
/// Gets the internal collection of names.
/// </summary>
/// <value>
/// The names.
/// </value>
internal IReadOnlyList<string> Names { get; }
/// <summary> /// <summary>
/// Returns an enumerator for the name collection. /// Returns an enumerator for the name collection.
/// </summary> /// </summary>
@ -76,23 +76,7 @@ namespace Avalonia.Media.Fonts
/// A <see cref="string" /> that represents this instance. /// A <see cref="string" /> that represents this instance.
/// </returns> /// </returns>
public override string ToString() public override string ToString()
{ => String.Join(", ", _names);
var builder = StringBuilderCache.Acquire();
for (var index = 0; index < Names.Count; index++)
{
builder.Append(Names[index]);
if (index == Names.Count - 1)
{
break;
}
builder.Append(", ");
}
return StringBuilderCache.GetStringAndRelease(builder);
}
/// <summary> /// <summary>
/// Returns a hash code for this instance. /// Returns a hash code for this instance.
@ -102,7 +86,7 @@ namespace Avalonia.Media.Fonts
/// </returns> /// </returns>
public override int GetHashCode() public override int GetHashCode()
{ {
if (Count == 0) if (_names.Length == 0)
{ {
return 0; return 0;
} }
@ -111,9 +95,9 @@ namespace Avalonia.Media.Fonts
{ {
int hash = 17; int hash = 17;
for (var i = 0; i < Names.Count; i++) for (var i = 0; i < _names.Length; i++)
{ {
string name = Names[i]; string name = _names[i];
hash = hash * 23 + name.GetHashCode(); hash = hash * 23 + name.GetHashCode();
} }
@ -145,30 +129,10 @@ namespace Avalonia.Media.Fonts
/// <c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>. /// <c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.
/// </returns> /// </returns>
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ => obj is FamilyNameCollection other && _names.AsSpan().SequenceEqual(other._names);
if (!(obj is FamilyNameCollection other))
{
return false;
}
if (other.Count != Count)
{
return false;
}
for (int i = 0; i < Count; i++)
{
if (Names[i] != other.Names[i])
{
return false;
}
}
return true;
}
public int Count => Names.Count; public int Count => _names.Length;
public string this[int index] => Names[index]; public string this[int index] => _names[index];
} }
} }

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

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

4
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -60,10 +60,8 @@ namespace Avalonia.Media.TextFormatting
var lineBreakEnumerator = new LineBreakEnumerator(text.Span); var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext()) while (lineBreakEnumerator.MoveNext(out var currentBreak))
{ {
var currentBreak = lineBreakEnumerator.Current;
if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length) if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
{ {
breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure); breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);

2
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
{ {
ShapedBuffer = shapedBuffer; ShapedBuffer = shapedBuffer;
Properties = properties; Properties = properties;
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); TextMetrics = new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
} }
public bool IsReversed { get; private set; } public bool IsReversed { get; private set; }

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

@ -47,13 +47,13 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <returns>The shapeable text characters.</returns> /// <returns>The shapeable text characters.</returns>
internal void GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel, internal void GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
ref TextRunProperties? previousProperties, RentedList<TextRun> results) FontManager fontManager, ref TextRunProperties? previousProperties, RentedList<TextRun> results)
{ {
var properties = Properties; var properties = Properties;
while (!text.IsEmpty) while (!text.IsEmpty)
{ {
var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, fontManager, ref previousProperties);
results.Add(shapeableRun); results.Add(shapeableRun);
@ -69,37 +69,40 @@ namespace Avalonia.Media.TextFormatting
/// <param name="text">The characters to create text runs from.</param> /// <param name="text">The characters to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param> /// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param> /// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <param name="previousProperties"></param> /// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns> /// <returns>A list of shapeable text runs.</returns>
private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> text, private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> text,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) TextRunProperties defaultProperties, sbyte biDiLevel, FontManager fontManager,
ref TextRunProperties? previousProperties)
{ {
var defaultTypeface = defaultProperties.Typeface; var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface; var defaultGlyphTypeface = defaultProperties.CachedGlyphTypeface;
var previousTypeface = previousProperties?.Typeface; var previousTypeface = previousProperties?.Typeface;
var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface;
var textSpan = text.Span; var textSpan = text.Span;
if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script)) if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script))
{ {
if (script == Script.Common && previousTypeface is not null) if (script == Script.Common && previousGlyphTypeface is not null)
{ {
if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _)) if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _))
{ {
return new UnshapedTextRun(text.Slice(0, fallbackCount), return new UnshapedTextRun(text.Slice(0, fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
} }
} }
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface),
biDiLevel); biDiLevel);
} }
if (previousTypeface is not null) if (previousGlyphTypeface is not null)
{ {
if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _)) if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _))
{ {
return new UnshapedTextRun(text.Slice(0, count), return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
} }
} }
@ -107,48 +110,44 @@ namespace Avalonia.Media.TextFormatting
var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span); var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span);
while (codepointEnumerator.MoveNext()) while (codepointEnumerator.MoveNext(out var cp))
{ {
if (codepointEnumerator.Current.IsWhiteSpace) if (cp.IsWhiteSpace)
{ {
continue; continue;
} }
codepoint = codepointEnumerator.Current; codepoint = cp;
break; break;
} }
//ToDo: Fix FontFamily fallback //ToDo: Fix FontFamily fallback
var matchFound = var matchFound =
FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface); out var fallbackTypeface);
if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _)) var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _))
{ {
//Fallback found //Fallback found
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel); biDiLevel);
} }
// no fallback found // no fallback found
currentTypeface = defaultTypeface;
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(textSpan); var enumerator = new GraphemeEnumerator(textSpan);
while (enumerator.MoveNext()) while (enumerator.MoveNext(out var grapheme))
{ {
var grapheme = enumerator.Current; if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
{ {
break; break;
} }
count += grapheme.Text.Length; count += grapheme.Length;
} }
return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel); return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel);
@ -158,15 +157,15 @@ namespace Avalonia.Media.TextFormatting
/// Tries to get a shapeable length that is supported by the specified typeface. /// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary> /// </summary>
/// <param name="text">The characters to shape.</param> /// <param name="text">The characters to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param> /// <param name="glyphTypeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param> /// <param name="defaultGlyphTypeface">The default typeface.</param>
/// <param name="length">The shapeable length.</param> /// <param name="length">The shapeable length.</param>
/// <param name="script"></param> /// <param name="script"></param>
/// <returns></returns> /// <returns></returns>
internal static bool TryGetShapeableLength( internal static bool TryGetShapeableLength(
ReadOnlySpan<char> text, ReadOnlySpan<char> text,
Typeface typeface, IGlyphTypeface glyphTypeface,
Typeface? defaultTypeface, IGlyphTypeface? defaultGlyphTypeface,
out int length, out int length,
out Script script) out Script script)
{ {
@ -178,24 +177,22 @@ namespace Avalonia.Media.TextFormatting
return false; return false;
} }
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text); var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext()) while (enumerator.MoveNext(out var currentGrapheme))
{ {
var currentGrapheme = enumerator.Current; var currentCodepoint = currentGrapheme.FirstCodepoint;
var currentScript = currentCodepoint.Script;
var currentScript = currentGrapheme.FirstCodepoint.Script;
if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) if (!currentCodepoint.IsWhiteSpace
&& defaultGlyphTypeface != null
&& defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _))
{ {
break; break;
} }
//Stop at the first missing glyph //Stop at the first missing glyph
if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _))
{ {
break; break;
} }
@ -216,7 +213,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
length += currentGrapheme.Text.Length; length += currentGrapheme.Length;
} }
return length > 0; return length > 0;

4
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -48,9 +48,9 @@ namespace Avalonia.Media.TextFormatting
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{ {
var nextBreakPosition = lineBreaker.Current.PositionMeasure; var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0) if (nextBreakPosition == 0)
{ {

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

@ -27,6 +27,7 @@ namespace Avalonia.Media.TextFormatting
TextLineBreak? nextLineBreak = null; TextLineBreak? nextLineBreak = null;
IReadOnlyList<TextRun>? textRuns; IReadOnlyList<TextRun>? textRuns;
var objectPool = FormattingObjectPool.Instance; var objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current;
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
out var textEndOfLine, out var textSourceLength); out var textEndOfLine, out var textSourceLength);
@ -42,7 +43,7 @@ namespace Avalonia.Media.TextFormatting
} }
else else
{ {
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection);
textRuns = shapedTextRuns; textRuns = shapedTextRuns;
if (nextLineBreak == null && textEndOfLine != null) if (nextLineBreak == null && textEndOfLine != null)
@ -72,7 +73,7 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.Wrap: case TextWrapping.Wrap:
{ {
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
break; break;
} }
default: default:
@ -178,12 +179,13 @@ namespace Avalonia.Media.TextFormatting
/// <param name="paragraphProperties">The default paragraph properties.</param> /// <param name="paragraphProperties">The default paragraph properties.</param>
/// <param name="resolvedFlowDirection">The resolved flow direction.</param> /// <param name="resolvedFlowDirection">The resolved flow direction.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param> /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns> /// <returns>
/// A list of shaped text characters. /// A list of shaped text characters.
/// </returns> /// </returns>
private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns, private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns,
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool,
out FlowDirection resolvedFlowDirection) FontManager fontManager, out FlowDirection resolvedFlowDirection)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var shapedRuns = objectPool.TextRunLists.Rent(); var shapedRuns = objectPool.TextRunLists.Rent();
@ -223,12 +225,13 @@ namespace Avalonia.Media.TextFormatting
var processedRuns = objectPool.TextRunLists.Rent(); var processedRuns = objectPool.TextRunLists.Rent();
CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
bidiData.Reset(); bidiData.Reset();
bidiAlgorithm.Reset(); bidiAlgorithm.Reset();
var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
var textShaper = TextShaper.Current;
for (var index = 0; index < processedRuns.Count; index++) for (var index = 0; index < processedRuns.Count; index++)
{ {
@ -240,7 +243,9 @@ namespace Avalonia.Media.TextFormatting
{ {
groupedRuns.Clear(); groupedRuns.Clear();
groupedRuns.Add(shapeableRun); groupedRuns.Add(shapeableRun);
var text = shapeableRun.Text; var text = shapeableRun.Text;
var properties = shapeableRun.Properties;
while (index + 1 < processedRuns.Count) while (index + 1 < processedRuns.Count)
{ {
@ -251,7 +256,7 @@ namespace Avalonia.Media.TextFormatting
if (shapeableRun.BidiLevel == nextRun.BidiLevel if (shapeableRun.BidiLevel == nextRun.BidiLevel
&& TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
&& CanShapeTogether(shapeableRun.Properties, nextRun.Properties)) && CanShapeTogether(properties, nextRun.Properties))
{ {
groupedRuns.Add(nextRun); groupedRuns.Add(nextRun);
index++; index++;
@ -263,12 +268,12 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, var shaperOptions = new TextShaperOptions(
currentRun.Properties.FontRenderingEmSize, properties.CachedGlyphTypeface,
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns);
break; break;
} }
@ -356,9 +361,9 @@ namespace Avalonia.Media.TextFormatting
&& x.BaselineAlignment == y.BaselineAlignment; && x.BaselineAlignment == y.BaselineAlignment;
private static void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, private static void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text,
TextShaperOptions options, RentedList<TextRun> results) TextShaperOptions options, TextShaper textShaper, RentedList<TextRun> results)
{ {
var shapedBuffer = TextShaper.Current.ShapeText(text, options); var shapedBuffer = textShaper.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++) for (var i = 0; i < textRuns.Count; i++)
{ {
@ -377,10 +382,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param> /// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
/// <param name="levels">The bidi levels.</param> /// <param name="levels">The bidi levels.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <param name="processedRuns">A list that will be filled with the processed runs.</param> /// <param name="processedRuns">A list that will be filled with the processed runs.</param>
/// <returns></returns> /// <returns></returns>
private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels, private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels,
RentedList<TextRun> processedRuns) FontManager fontManager, RentedList<TextRun> processedRuns)
{ {
if (levels.Length == 0) if (levels.Length == 0)
{ {
@ -427,8 +433,8 @@ namespace Avalonia.Media.TextFormatting
if (j == runTextSpan.Length) if (j == runTextSpan.Length)
{ {
currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager,
processedRuns); ref previousProperties, processedRuns);
runLevel = levels[levelIndex]; runLevel = levels[levelIndex];
@ -441,8 +447,8 @@ namespace Avalonia.Media.TextFormatting
} }
// End of this run // End of this run
currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager,
processedRuns); ref previousProperties, processedRuns);
runText = runText.Slice(j); runText = runText.Slice(j);
runTextSpan = runText.Span; runTextSpan = runText.Span;
@ -459,7 +465,7 @@ namespace Avalonia.Media.TextFormatting
return; return;
} }
currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns); currentRun.GetShapeableCharacters(runText, runLevel, fontManager, ref previousProperties, processedRuns);
} }
/// <summary> /// <summary>
@ -554,15 +560,13 @@ namespace Avalonia.Media.TextFormatting
var lineBreakEnumerator = new LineBreakEnumerator(text.Span); var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext()) while (lineBreakEnumerator.MoveNext(out lineBreak))
{ {
if (!lineBreakEnumerator.Current.Required) if (!lineBreak.Required)
{ {
continue; continue;
} }
lineBreak = lineBreakEnumerator.Current;
return lineBreak.PositionWrap >= textRun.Length || true; return lineBreak.PositionWrap >= textRun.Length || true;
} }
@ -637,11 +641,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <returns>The empty text line.</returns> /// <returns>The empty text line.</returns>
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) TextParagraphProperties paragraphProperties, FontManager fontManager)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties; var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface; var glyphTypeface = properties.CachedGlyphTypeface;
var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) };
@ -665,14 +669,15 @@ namespace Avalonia.Media.TextFormatting
/// <param name="resolvedFlowDirection"></param> /// <param name="resolvedFlowDirection"></param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param> /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param> /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns>The wrapped text line.</returns> /// <returns>The wrapped text line.</returns>
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager)
{ {
if (textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager);
} }
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@ -698,20 +703,20 @@ namespace Avalonia.Media.TextFormatting
{ {
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (lineBreaker.MoveNext()) while (lineBreaker.MoveNext(out var lineBreak))
{ {
if (lineBreaker.Current.Required && if (lineBreak.Required &&
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) currentLength + lineBreak.PositionMeasure <= measuredLength)
{ {
//Explicit break found //Explicit break found
breakFound = true; breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap; currentPosition = currentLength + lineBreak.PositionWrap;
break; break;
} }
if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) if (currentLength + lineBreak.PositionMeasure > measuredLength)
{ {
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{ {
@ -727,21 +732,21 @@ namespace Avalonia.Media.TextFormatting
//Find next possible wrap position (overflow) //Find next possible wrap position (overflow)
if (index < textRuns.Count - 1) if (index < textRuns.Count - 1)
{ {
if (lineBreaker.Current.PositionWrap != currentRun.Length) if (lineBreak.PositionWrap != currentRun.Length)
{ {
//We already found the next possible wrap position. //We already found the next possible wrap position.
breakFound = true; breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap; currentPosition = currentLength + lineBreak.PositionWrap;
break; break;
} }
while (lineBreaker.MoveNext() && index < textRuns.Count) while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count)
{ {
currentPosition += lineBreaker.Current.PositionWrap; currentPosition += lineBreak.PositionWrap;
if (lineBreaker.Current.PositionWrap != currentRun.Length) if (lineBreak.PositionWrap != currentRun.Length)
{ {
break; break;
} }
@ -760,7 +765,7 @@ namespace Avalonia.Media.TextFormatting
} }
else else
{ {
currentPosition = currentLength + lineBreaker.Current.PositionWrap; currentPosition = currentLength + lineBreak.PositionWrap;
} }
breakFound = true; breakFound = true;
@ -776,9 +781,9 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) if (lineBreak.PositionMeasure != lineBreak.PositionWrap)
{ {
lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; lastWrapPosition = currentLength + lineBreak.PositionWrap;
} }
} }
@ -800,18 +805,18 @@ namespace Avalonia.Media.TextFormatting
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
var lineBreak = postSplitRuns?.Count > 0 ? var textLineBreak = postSplitRuns?.Count > 0 ?
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
null; null;
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
{ {
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
} }
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, paragraphWidth, paragraphProperties, resolvedFlowDirection,
lineBreak); textLineBreak);
textLine.FinalizeLine(); textLine.FinalizeLine();
@ -868,7 +873,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var textShaper = TextShaper.Current; var textShaper = TextShaper.Current;
var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface; var glyphTypeface = textRun.Properties!.CachedGlyphTypeface;
var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;

7
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -427,11 +427,12 @@ namespace Avalonia.Media.TextFormatting
private TextLine[] CreateTextLines() private TextLine[] CreateTextLines()
{ {
var objectPool = FormattingObjectPool.Instance; var objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties,
FormattingObjectPool.Instance); fontManager);
Bounds = new Rect(0, 0, 0, textLine.Height); Bounds = new Rect(0, 0, 0, textLine.Height);
@ -458,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
if (previousLine != null && previousLine.NewLineLength > 0) if (previousLine != null && previousLine.NewLineLength > 0)
{ {
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
_paragraphProperties, objectPool); _paragraphProperties, fontManager);
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
@ -517,7 +518,7 @@ namespace Avalonia.Media.TextFormatting
//Make sure the TextLayout always contains at least on empty line //Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0) if (textLines.Count == 0)
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
textLines.Add(textLine); textLines.Add(textLine);

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

@ -1256,7 +1256,7 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics() private TextLineMetrics CreateLineMetrics()
{ {
var fontMetrics = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface.Metrics; var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics;
var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;
@ -1285,12 +1285,13 @@ namespace Avalonia.Media.TextFormatting
{ {
case ShapedTextRun textRun: case ShapedTextRun textRun:
{ {
var properties = textRun.Properties;
var textMetrics = var textMetrics =
new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) if (fontRenderingEmSize < properties.FontRenderingEmSize)
{ {
fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; fontRenderingEmSize = properties.FontRenderingEmSize;
if (ascent > textMetrics.Ascent) if (ascent > textMetrics.Ascent)
{ {

5
src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs

@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting
/// </remarks> /// </remarks>
public abstract class TextRunProperties : IEquatable<TextRunProperties> public abstract class TextRunProperties : IEquatable<TextRunProperties>
{ {
private IGlyphTypeface? _cachedGlyphTypeFace;
/// <summary> /// <summary>
/// Run typeface /// Run typeface
/// </summary> /// </summary>
@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline; public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
internal IGlyphTypeface CachedGlyphTypeface
=> _cachedGlyphTypeFace ??= Typeface.GlyphTypeface;
public bool Equals(TextRunProperties? other) public bool Equals(TextRunProperties? other)
{ {
if (ReferenceEquals(null, other)) if (ReferenceEquals(null, other))

211
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@ -343,6 +343,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
return 0; return 0;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsIsolateStart(BidiClass type)
{
const uint mask =
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate);
return ((1U << (int)type) & mask) != 0U;
}
/// <summary> /// <summary>
/// Build a list of matching isolates for a directionality slice /// Build a list of matching isolates for a directionality slice
/// Implements BD9 /// Implements BD9
@ -701,28 +712,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
var lastType = _workingClasses[lastCharIndex]; var lastType = _workingClasses[lastCharIndex];
int nextLevel; int nextLevel;
switch (lastType) if (IsIsolateStart(lastType))
{ {
case BidiClass.LeftToRightIsolate: nextLevel = _paragraphEmbeddingLevel;
case BidiClass.RightToLeftIsolate: }
case BidiClass.FirstStrongIsolate: else
{
i = lastCharIndex + 1;
while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
{ {
nextLevel = _paragraphEmbeddingLevel; i++;
break;
} }
default:
{
i = lastCharIndex + 1;
while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
{
i++;
}
nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i]; nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i];
break;
}
} }
var eos = DirectionFromLevel(Math.Max(nextLevel, level)); var eos = DirectionFromLevel(Math.Max(nextLevel, level));
@ -831,8 +833,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// PDI and concatenate that run to this one // PDI and concatenate that run to this one
var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1];
var lastType = _originalClasses[lastCharacterIndex]; var lastType = _originalClasses[lastCharacterIndex];
if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && if (IsIsolateStart(lastType) && _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
_isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
{ {
// Find the continuing run index // Find the continuing run index
runIndex = FindRunForIndex(nextRunIndex); runIndex = FindRunForIndex(nextRunIndex);
@ -869,74 +870,59 @@ namespace Avalonia.Media.TextFormatting.Unicode
_runDirection = DirectionFromLevel(runLevel); _runDirection = DirectionFromLevel(runLevel);
_runLength = _runResolvedClasses.Length; _runLength = _runResolvedClasses.Length;
// By tracking the types of characters known to be in the current run, we can
// skip some of the rules that we know won't apply. The flags will be
// initialized while we're processing rule W1 below.
var hasEN = false;
var hasAL = false;
var hasES = false;
var hasCS = false;
var hasAN = false;
var hasET = false;
// Rule W1 // Rule W1
// Also, set hasXX flags // Also, set hasXX flags
int i; int i;
var previousClass = sos; var previousClass = sos;
const uint isolateMask =
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate) |
(1U << (int)BidiClass.PopDirectionalIsolate);
const uint wRulesMask =
(1U << (int)BidiClass.EuropeanNumber) |
(1U << (int)BidiClass.ArabicLetter) |
(1U << (int)BidiClass.EuropeanSeparator) |
(1U << (int)BidiClass.CommonSeparator) |
(1U << (int)BidiClass.ArabicNumber) |
(1U << (int)BidiClass.EuropeanTerminator);
uint wRules = 0;
for (i = 0; i < _runLength; i++) for (i = 0; i < _runLength; i++)
{ {
var resolvedClass = _runResolvedClasses[i]; var resolvedClass = _runResolvedClasses[i];
switch (resolvedClass)
{
case BidiClass.NonspacingMark:
_runResolvedClasses[i] = previousClass;
break;
case BidiClass.LeftToRightIsolate: if (resolvedClass == BidiClass.NonspacingMark)
case BidiClass.RightToLeftIsolate: {
case BidiClass.FirstStrongIsolate: _runResolvedClasses[i] = previousClass;
case BidiClass.PopDirectionalIsolate: }
else
{
var classBit = 1U << (int)resolvedClass;
if ((classBit & isolateMask) != 0U)
{
previousClass = BidiClass.OtherNeutral; previousClass = BidiClass.OtherNeutral;
break; }
else
case BidiClass.EuropeanNumber: {
hasEN = true; wRules |= classBit & wRulesMask;
previousClass = resolvedClass;
break;
case BidiClass.ArabicLetter:
hasAL = true;
previousClass = resolvedClass;
break;
case BidiClass.EuropeanSeparator:
hasES = true;
previousClass = resolvedClass;
break;
case BidiClass.CommonSeparator:
hasCS = true;
previousClass = resolvedClass;
break;
case BidiClass.ArabicNumber:
hasAN = true;
previousClass = resolvedClass;
break;
case BidiClass.EuropeanTerminator:
hasET = true;
previousClass = resolvedClass;
break;
default:
previousClass = resolvedClass; previousClass = resolvedClass;
break; }
} }
} }
// By tracking the types of characters known to be in the current run, we can
// skip some of the rules that we know won't apply.
var hasEN = (wRules & (1U << (int)BidiClass.EuropeanNumber)) != 0U;
var hasAL = (wRules & (1U << (int)BidiClass.ArabicLetter)) != 0U;
var hasES = (wRules & (1U << (int)BidiClass.EuropeanSeparator)) != 0U;
var hasCS = (wRules & (1U << (int)BidiClass.CommonSeparator)) != 0U;
var hasAN = (wRules & (1U << (int)BidiClass.ArabicNumber)) != 0U;
var hasET = (wRules & (1U << (int)BidiClass.EuropeanTerminator)) != 0U;
// Rule W2 // Rule W2
if (hasEN) if (hasEN)
{ {
@ -1548,23 +1534,20 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsWhitespace(BidiClass biDiClass) private static bool IsWhitespace(BidiClass biDiClass)
{ {
switch (biDiClass) const uint mask =
{ (1U << (int)BidiClass.LeftToRightEmbedding) |
case BidiClass.LeftToRightEmbedding: (1U << (int)BidiClass.RightToLeftEmbedding) |
case BidiClass.RightToLeftEmbedding: (1U << (int)BidiClass.LeftToRightOverride) |
case BidiClass.LeftToRightOverride: (1U << (int)BidiClass.RightToLeftOverride) |
case BidiClass.RightToLeftOverride: (1U << (int)BidiClass.PopDirectionalFormat) |
case BidiClass.PopDirectionalFormat: (1U << (int)BidiClass.LeftToRightIsolate) |
case BidiClass.LeftToRightIsolate: (1U << (int)BidiClass.RightToLeftIsolate) |
case BidiClass.RightToLeftIsolate: (1U << (int)BidiClass.FirstStrongIsolate) |
case BidiClass.FirstStrongIsolate: (1U << (int)BidiClass.PopDirectionalIsolate) |
case BidiClass.PopDirectionalIsolate: (1U << (int)BidiClass.BoundaryNeutral) |
case BidiClass.BoundaryNeutral: (1U << (int)BidiClass.WhiteSpace);
case BidiClass.WhiteSpace:
return true; return ((1U << (int)biDiClass) & mask) != 0U;
default:
return false;
}
} }
/// <summary> /// <summary>
@ -1585,18 +1568,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsRemovedByX9(BidiClass biDiClass) private static bool IsRemovedByX9(BidiClass biDiClass)
{ {
switch (biDiClass) const uint mask =
{ (1U << (int)BidiClass.LeftToRightEmbedding) |
case BidiClass.LeftToRightEmbedding: (1U << (int)BidiClass.RightToLeftEmbedding) |
case BidiClass.RightToLeftEmbedding: (1U << (int)BidiClass.LeftToRightOverride) |
case BidiClass.LeftToRightOverride: (1U << (int)BidiClass.RightToLeftOverride) |
case BidiClass.RightToLeftOverride: (1U << (int)BidiClass.PopDirectionalFormat) |
case BidiClass.PopDirectionalFormat: (1U << (int)BidiClass.BoundaryNeutral);
case BidiClass.BoundaryNeutral:
return true; return ((1U << (int)biDiClass) & mask) != 0U;
default:
return false;
}
} }
/// <summary> /// <summary>
@ -1605,20 +1585,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsNeutralClass(BidiClass direction) private static bool IsNeutralClass(BidiClass direction)
{ {
switch (direction) const uint mask =
{ (1U << (int)BidiClass.ParagraphSeparator) |
case BidiClass.ParagraphSeparator: (1U << (int)BidiClass.SegmentSeparator) |
case BidiClass.SegmentSeparator: (1U << (int)BidiClass.WhiteSpace) |
case BidiClass.WhiteSpace: (1U << (int)BidiClass.OtherNeutral) |
case BidiClass.OtherNeutral: (1U << (int)BidiClass.RightToLeftIsolate) |
case BidiClass.RightToLeftIsolate: (1U << (int)BidiClass.LeftToRightIsolate) |
case BidiClass.LeftToRightIsolate: (1U << (int)BidiClass.FirstStrongIsolate) |
case BidiClass.FirstStrongIsolate: (1U << (int)BidiClass.PopDirectionalIsolate);
case BidiClass.PopDirectionalIsolate:
return true; return ((1U << (int)direction) & mask) != 0U;
default:
return false;
}
} }
/// <summary> /// <summary>

41
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -73,39 +73,32 @@ namespace Avalonia.Media.TextFormatting.Unicode
// bracket values for all code points // bracket values for all code points
int i = Length; int i = Length;
const uint embeddingMask =
(1U << (int)BidiClass.LeftToRightEmbedding) |
(1U << (int)BidiClass.LeftToRightOverride) |
(1U << (int)BidiClass.RightToLeftEmbedding) |
(1U << (int)BidiClass.RightToLeftOverride) |
(1U << (int)BidiClass.PopDirectionalFormat);
const uint isolateMask =
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate) |
(1U << (int)BidiClass.PopDirectionalIsolate);
var codePointEnumerator = new CodepointEnumerator(text); var codePointEnumerator = new CodepointEnumerator(text);
while (codePointEnumerator.MoveNext()) while (codePointEnumerator.MoveNext(out var codepoint))
{ {
var codepoint = codePointEnumerator.Current;
// Look up BiDiClass // Look up BiDiClass
var dir = codepoint.BiDiClass; var dir = codepoint.BiDiClass;
_classes[i] = dir; _classes[i] = dir;
switch (dir) var dirBit = 1U << (int)dir;
{ HasEmbeddings = (dirBit & embeddingMask) != 0U;
case BidiClass.LeftToRightEmbedding: HasIsolates = (dirBit & isolateMask) != 0U;
case BidiClass.LeftToRightOverride:
case BidiClass.RightToLeftEmbedding:
case BidiClass.RightToLeftOverride:
case BidiClass.PopDirectionalFormat:
{
HasEmbeddings = true;
break;
}
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
{
HasIsolates = true;
break;
}
}
// Lookup paired bracket types // Lookup paired bracket types
var pbt = codepoint.PairedBracketType; var pbt = codepoint.PairedBracketType;

117
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
@ -11,13 +10,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// The replacement codepoint that is used for non supported values. /// The replacement codepoint that is used for non supported values.
/// </summary> /// </summary>
public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD'); public static Codepoint ReplacementCodepoint
public Codepoint(uint value)
{ {
_value = value; [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new('\uFFFD');
} }
/// <summary>
/// Creates a new instance of <see cref="Codepoint"/> with the specified value.
/// </summary>
/// <param name="value">The codepoint value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Codepoint(uint value) => _value = value;
/// <summary> /// <summary>
/// Get the codepoint's value. /// Get the codepoint's value.
/// </summary> /// </summary>
@ -87,19 +92,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </returns> /// </returns>
public bool IsWhiteSpace public bool IsWhiteSpace
{ {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get get
{ {
switch (GeneralCategory) const ulong whiteSpaceMask =
{ (1UL << (int)GeneralCategory.Control) |
case GeneralCategory.Control: (1UL << (int)GeneralCategory.NonspacingMark) |
case GeneralCategory.NonspacingMark: (1UL << (int)GeneralCategory.Format) |
case GeneralCategory.Format: (1UL << (int)GeneralCategory.SpaceSeparator) |
case GeneralCategory.SpaceSeparator: (1UL << (int)GeneralCategory.SpacingMark);
case GeneralCategory.SpacingMark:
return true; return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0UL;
}
return false;
} }
} }
@ -166,56 +169,62 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param> /// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param> /// <param name="count">The count of character that were read.</param>
/// <returns></returns> /// <returns></returns>
#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count) public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
{ {
// Perf note: this method is performance critical for text layout, modify with care!
count = 1; count = 1;
if (index >= text.Length) // Perf note: uint check allows the JIT to ellide the next bound check
if ((uint)index >= (uint)text.Length)
{ {
return ReplacementCodepoint; return ReplacementCodepoint;
} }
var code = text[index]; uint code = text[index];
ushort hi, low;
//# High surrogate //# Surrogate
if (0xD800 <= code && code <= 0xDBFF) if (IsInRangeInclusive(code, 0xD800U, 0xDFFFU))
{ {
hi = code; uint hi, low;
if (index + 1 == text.Length)
{
return ReplacementCodepoint;
}
low = text[index + 1];
if (0xDC00 <= low && low <= 0xDFFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
//# Low surrogate //# High surrogate
if (0xDC00 <= code && code <= 0xDFFF) if (code <= 0xDBFF)
{
if (index == 0)
{ {
return ReplacementCodepoint; if ((uint)(index + 1) < (uint)text.Length)
{
hi = code;
low = text[index + 1];
if (IsInRangeInclusive(low, 0xDC00U, 0xDFFFU))
{
count = 2;
// Perf note: the code is written as below to become just two instructions: shl, lea.
// See https://github.com/dotnet/runtime/blob/7ec3634ee579d89b6024f72b595bfd7118093fc5/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs#L38
return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16)));
}
}
} }
hi = text[index - 1]; //# Low surrogate
else
low = code;
if (0xD800 <= hi && hi <= 0xDBFF)
{ {
count = 2; if (index > 0)
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); {
low = code;
hi = text[index - 1];
if (IsInRangeInclusive(hi, 0xD800U, 0xDBFFU))
{
count = 2;
return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16)));
}
}
} }
return ReplacementCodepoint; return ReplacementCodepoint;
@ -224,12 +233,16 @@ namespace Avalonia.Media.TextFormatting.Unicode
return new Codepoint(code); return new Codepoint(code);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound)
=> value - lowerBound <= upperBound - lowerBound;
/// <summary> /// <summary>
/// Returns <see langword="true"/> if <paramref name="cp"/> is between /// Returns <see langword="true"/> if <paramref name="cp"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive. /// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInRangeInclusive(Codepoint cp, uint lowerBound, uint upperBound) public static bool IsInRangeInclusive(Codepoint cp, uint lowerBound, uint upperBound)
=> (cp._value - lowerBound) <= (upperBound - lowerBound); => IsInRangeInclusive(cp._value, lowerBound, upperBound);
} }
} }

24
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -4,35 +4,27 @@ namespace Avalonia.Media.TextFormatting.Unicode
{ {
public ref struct CodepointEnumerator public ref struct CodepointEnumerator
{ {
private ReadOnlySpan<char> _text; private readonly ReadOnlySpan<char> _text;
private int _offset;
public CodepointEnumerator(ReadOnlySpan<char> text) public CodepointEnumerator(ReadOnlySpan<char> text)
{ => _text = text;
_text = text;
Current = Codepoint.ReplacementCodepoint;
}
/// <summary>
/// Gets the current <see cref="Codepoint"/>.
/// </summary>
public Codepoint Current { get; private set; }
/// <summary> /// <summary>
/// Moves to the next <see cref="Codepoint"/>. /// Moves to the next <see cref="Codepoint"/>.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public bool MoveNext() public bool MoveNext(out Codepoint codepoint)
{ {
if (_text.IsEmpty) if ((uint)_offset >= (uint)_text.Length)
{ {
Current = Codepoint.ReplacementCodepoint; codepoint = Codepoint.ReplacementCodepoint;
return false; return false;
} }
Current = Codepoint.ReadAt(_text, 0, out var count); codepoint = Codepoint.ReadAt(_text, _offset, out var count);
_text = _text.Slice(count); _offset += count;
return true; return true;
} }

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

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

@ -4,57 +4,79 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System; using System;
using System.Runtime.InteropServices;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
public ref struct GraphemeEnumerator public ref struct GraphemeEnumerator
{ {
private ReadOnlySpan<char> _text; private readonly ReadOnlySpan<char> _text;
private int _currentCodeUnitOffset;
private int _codeUnitLengthOfCurrentCodepoint;
private Codepoint _currentCodepoint;
/// <summary>
/// Will be <see cref="GraphemeBreakClass.Other"/> if invalid data or EOF reached.
/// Caller shouldn't need to special-case this since the normal rules will halt on this condition.
/// </summary>
private GraphemeBreakClass _currentType;
public GraphemeEnumerator(ReadOnlySpan<char> text) public GraphemeEnumerator(ReadOnlySpan<char> text)
{ {
_text = text; _text = text;
Current = default; _currentCodeUnitOffset = 0;
_codeUnitLengthOfCurrentCodepoint = 0;
_currentCodepoint = Codepoint.ReplacementCodepoint;
_currentType = GraphemeBreakClass.Other;
} }
/// <summary>
/// Gets the current <see cref="Grapheme"/>.
/// </summary>
public Grapheme Current { get; private set; }
/// <summary> /// <summary>
/// Moves to the next <see cref="Grapheme"/>. /// Moves to the next <see cref="Grapheme"/>.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public bool MoveNext() public bool MoveNext(out Grapheme grapheme)
{ {
if (_text.IsEmpty) var startOffset = _currentCodeUnitOffset;
if ((uint)startOffset >= (uint)_text.Length)
{ {
grapheme = default;
return false; return false;
} }
// Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules. // Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules.
var processor = new Processor(_text); if (startOffset == 0)
{
processor.MoveNext(); ReadNextCodepoint();
}
var firstCodepoint = processor.CurrentCodepoint; var firstCodepoint = _currentCodepoint;
// First, consume as many Prepend scalars as we can (rule GB9b). // First, consume as many Prepend scalars as we can (rule GB9b).
while (processor.CurrentType == GraphemeBreakClass.Prepend) if (_currentType == GraphemeBreakClass.Prepend)
{ {
processor.MoveNext(); do
{
ReadNextCodepoint();
} while (_currentType == GraphemeBreakClass.Prepend);
// There were only Prepend scalars in the text
if ((uint)_currentCodeUnitOffset >= (uint)_text.Length)
{
goto Return;
}
} }
// Next, make sure we're not about to violate control character restrictions. // Next, make sure we're not about to violate control character restrictions.
// Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5). // Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5).
if (processor.CurrentCodeUnitOffset > 0) if (_currentCodeUnitOffset > startOffset)
{ {
if (processor.CurrentType == GraphemeBreakClass.Control const uint controlCrLfMask =
|| processor.CurrentType == GraphemeBreakClass.CR (1U << (int)GraphemeBreakClass.Control) |
|| processor.CurrentType == GraphemeBreakClass.LF) (1U << (int)GraphemeBreakClass.CR) |
(1U << (int)GraphemeBreakClass.LF);
if (((1U << (int)_currentType) & controlCrLfMask) != 0U)
{ {
goto Return; goto Return;
} }
@ -62,19 +84,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Now begin the main state machine. // Now begin the main state machine.
var previousClusterBreakType = processor.CurrentType; var previousClusterBreakType = _currentType;
processor.MoveNext(); ReadNextCodepoint();
switch (previousClusterBreakType) switch (previousClusterBreakType)
{ {
case GraphemeBreakClass.CR: case GraphemeBreakClass.CR:
if (processor.CurrentType != GraphemeBreakClass.LF) if (_currentType != GraphemeBreakClass.LF)
{ {
goto Return; // rules GB3 & GB4 (only <LF> can follow <CR>) goto Return; // rules GB3 & GB4 (only <LF> can follow <CR>)
} }
processor.MoveNext(); ReadNextCodepoint();
goto case GraphemeBreakClass.LF; goto case GraphemeBreakClass.LF;
case GraphemeBreakClass.Control: case GraphemeBreakClass.Control:
@ -82,53 +104,57 @@ namespace Avalonia.Media.TextFormatting.Unicode
goto Return; // rule GB4 (no data after Control | LF) goto Return; // rule GB4 (no data after Control | LF)
case GraphemeBreakClass.L: case GraphemeBreakClass.L:
if (processor.CurrentType == GraphemeBreakClass.L) {
if (_currentType == GraphemeBreakClass.L)
{ {
processor.MoveNext(); // rule GB6 (L x L) ReadNextCodepoint(); // rule GB6 (L x L)
goto case GraphemeBreakClass.L; goto case GraphemeBreakClass.L;
} }
else if (processor.CurrentType == GraphemeBreakClass.V) else if (_currentType == GraphemeBreakClass.V)
{ {
processor.MoveNext(); // rule GB6 (L x V) ReadNextCodepoint(); // rule GB6 (L x V)
goto case GraphemeBreakClass.V; goto case GraphemeBreakClass.V;
} }
else if (processor.CurrentType == GraphemeBreakClass.LV) else if (_currentType == GraphemeBreakClass.LV)
{ {
processor.MoveNext(); // rule GB6 (L x LV) ReadNextCodepoint(); // rule GB6 (L x LV)
goto case GraphemeBreakClass.LV; goto case GraphemeBreakClass.LV;
} }
else if (processor.CurrentType == GraphemeBreakClass.LVT) else if (_currentType == GraphemeBreakClass.LVT)
{ {
processor.MoveNext(); // rule GB6 (L x LVT) ReadNextCodepoint(); // rule GB6 (L x LVT)
goto case GraphemeBreakClass.LVT; goto case GraphemeBreakClass.LVT;
} }
else else
{ {
break; break;
} }
}
case GraphemeBreakClass.LV: case GraphemeBreakClass.LV:
case GraphemeBreakClass.V: case GraphemeBreakClass.V:
if (processor.CurrentType == GraphemeBreakClass.V) {
if (_currentType == GraphemeBreakClass.V)
{ {
processor.MoveNext(); // rule GB7 (LV | V x V) ReadNextCodepoint(); // rule GB7 (LV | V x V)
goto case GraphemeBreakClass.V; goto case GraphemeBreakClass.V;
} }
else if (processor.CurrentType == GraphemeBreakClass.T) else if (_currentType == GraphemeBreakClass.T)
{ {
processor.MoveNext(); // rule GB7 (LV | V x T) ReadNextCodepoint(); // rule GB7 (LV | V x T)
goto case GraphemeBreakClass.T; goto case GraphemeBreakClass.T;
} }
else else
{ {
break; break;
} }
}
case GraphemeBreakClass.LVT: case GraphemeBreakClass.LVT:
case GraphemeBreakClass.T: case GraphemeBreakClass.T:
if (processor.CurrentType == GraphemeBreakClass.T) if (_currentType == GraphemeBreakClass.T)
{ {
processor.MoveNext(); // rule GB8 (LVT | T x T) ReadNextCodepoint(); // rule GB8 (LVT | T x T)
goto case GraphemeBreakClass.T; goto case GraphemeBreakClass.T;
} }
else else
@ -139,123 +165,79 @@ namespace Avalonia.Media.TextFormatting.Unicode
case GraphemeBreakClass.ExtendedPictographic: case GraphemeBreakClass.ExtendedPictographic:
// Attempt processing extended pictographic (rules GB11, GB9). // Attempt processing extended pictographic (rules GB11, GB9).
// First, drain any Extend scalars that might exist // First, drain any Extend scalars that might exist
while (processor.CurrentType == GraphemeBreakClass.Extend) while (_currentType == GraphemeBreakClass.Extend)
{ {
processor.MoveNext(); ReadNextCodepoint();
} }
// Now see if there's a ZWJ + extended pictograph again. // Now see if there's a ZWJ + extended pictograph again.
if (processor.CurrentType != GraphemeBreakClass.ZWJ) if (_currentType != GraphemeBreakClass.ZWJ)
{ {
break; break;
} }
processor.MoveNext(); ReadNextCodepoint();
if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic) if (_currentType != GraphemeBreakClass.ExtendedPictographic)
{ {
break; break;
} }
processor.MoveNext(); ReadNextCodepoint();
goto case GraphemeBreakClass.ExtendedPictographic; goto case GraphemeBreakClass.ExtendedPictographic;
case GraphemeBreakClass.RegionalIndicator: case GraphemeBreakClass.RegionalIndicator:
// We've consumed a single RI scalar. Try to consume another (to make it a pair). // We've consumed a single RI scalar. Try to consume another (to make it a pair).
if (processor.CurrentType == GraphemeBreakClass.RegionalIndicator) if (_currentType == GraphemeBreakClass.RegionalIndicator)
{ {
processor.MoveNext(); ReadNextCodepoint();
} }
// Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers. // Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers.
break; // nothing but trailers after the final RI break; // nothing but trailers after the final RI
default:
break;
} }
const uint gb9Mask =
(1U << (int)GraphemeBreakClass.Extend) |
(1U << (int)GraphemeBreakClass.ZWJ) |
(1U << (int)GraphemeBreakClass.SpacingMark);
// rules GB9, GB9a // rules GB9, GB9a
while (processor.CurrentType == GraphemeBreakClass.Extend while (((1U << (int)_currentType) & gb9Mask) != 0U)
|| processor.CurrentType == GraphemeBreakClass.ZWJ
|| processor.CurrentType == GraphemeBreakClass.SpacingMark)
{ {
processor.MoveNext(); ReadNextCodepoint();
} }
Return: Return:
Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset)); var graphemeLength = _currentCodeUnitOffset - startOffset;
grapheme = new Grapheme(firstCodepoint, startOffset, graphemeLength);
_text = _text.Slice(processor.CurrentCodeUnitOffset);
return true; // rules GB2, GB999 return true; // rules GB2, GB999
} }
[StructLayout(LayoutKind.Auto)] private void ReadNextCodepoint()
private ref struct Processor
{ {
private readonly ReadOnlySpan<char> _buffer; // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on
private int _codeUnitLengthOfCurrentScalar; // the decoder's default behavior of interpreting these ill-formed subsequences as
// equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property
internal Processor(ReadOnlySpan<char> buffer) // of Other (XX), which matches the modifications made to UAX#29, Rev. 35.
{ // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications
_buffer = buffer; // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file
_codeUnitLengthOfCurrentScalar = 0; // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
CurrentCodepoint = Codepoint.ReplacementCodepoint; // has the line "D800..DFFF ; Control # Cs [2048] <surrogate-D800>..<surrogate-DFFF>",
CurrentType = GraphemeBreakClass.Other; // but starting with Unicode 12.0 that line has been removed.
CurrentCodeUnitOffset = 0; //
} // If a later version of the Unicode Standard further modifies this guidance we should reflect
// that here.
public int CurrentCodeUnitOffset { get; private set; }
_currentCodeUnitOffset += _codeUnitLengthOfCurrentCodepoint;
/// <summary>
/// Will be <see cref="GraphemeBreakClass.Other"/> if invalid data or EOF reached. _currentCodepoint = Codepoint.ReadAt(_text, _currentCodeUnitOffset,
/// Caller shouldn't need to special-case this since the normal rules will halt on this condition. out _codeUnitLengthOfCurrentCodepoint);
/// </summary>
public GraphemeBreakClass CurrentType { get; private set; } _currentType = _currentCodepoint.GraphemeBreakClass;
/// <summary>
/// Get the currently processed <see cref="Codepoint"/>.
/// </summary>
public Codepoint CurrentCodepoint { get; private set; }
public void MoveNext()
{
// For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on
// the decoder's default behavior of interpreting these ill-formed subsequences as
// equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property
// of Other (XX), which matches the modifications made to UAX#29, Rev. 35.
// See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications
// This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file
// https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
// has the line "D800..DFFF ; Control # Cs [2048] <surrogate-D800>..<surrogate-DFFF>",
// but starting with Unicode 12.0 that line has been removed.
//
// If a later version of the Unicode Standard further modifies this guidance we should reflect
// that here.
if (CurrentCodeUnitOffset == _buffer.Length)
{
CurrentCodepoint = Codepoint.ReplacementCodepoint;
}
else
{
CurrentCodeUnitOffset += _codeUnitLengthOfCurrentScalar;
if (CurrentCodeUnitOffset < _buffer.Length)
{
CurrentCodepoint = Codepoint.ReadAt(_buffer, CurrentCodeUnitOffset,
out _codeUnitLengthOfCurrentScalar);
}
else
{
CurrentCodepoint = Codepoint.ReplacementCodepoint;
}
}
CurrentType = CurrentCodepoint.GraphemeBreakClass;
}
} }
} }
} }

244
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -3,6 +3,7 @@
// Ported from: https://github.com/SixLabors/Fonts/ // Ported from: https://github.com/SixLabors/Fonts/
using System; using System;
using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
@ -46,10 +47,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30 = false; _lb30 = false;
_lb30a = 0; _lb30a = 0;
} }
public LineBreak Current { get; private set; } public bool MoveNext(out LineBreak lineBreak)
public bool MoveNext()
{ {
// Get the first char if we're at the beginning of the string. // Get the first char if we're at the beginning of the string.
if (_first) if (_first)
@ -75,7 +74,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed: case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed:
{ {
_currentClass = MapFirst(_nextClass); _currentClass = MapFirst(_nextClass);
Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true); lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true);
return true; return true;
} }
} }
@ -87,7 +86,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (shouldBreak) if (shouldBreak)
{ {
Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition);
return true; return true;
} }
} }
@ -108,23 +107,23 @@ namespace Avalonia.Media.TextFormatting.Unicode
break; break;
} }
Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required); lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required);
return true; return true;
} }
} }
Current = default; lineBreak = default;
return false; return false;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LineBreakClass MapClass(Codepoint cp) private static LineBreakClass MapClass(Codepoint cp)
{ {
if (cp.Value == 327685) if (cp.Value == 327685)
{ {
return LineBreakClass.Alphabetic; return LineBreakClass.Alphabetic;
} }
// LB 1 // LB 1
// ========================================== // ==========================================
// Resolved Original General_Category // Resolved Original General_Category
@ -133,26 +132,38 @@ namespace Avalonia.Media.TextFormatting.Unicode
// CM SA Only Mn or Mc // CM SA Only Mn or Mc
// AL SA Any except Mn and Mc // AL SA Any except Mn and Mc
// NS CJ Any // NS CJ Any
switch (cp.LineBreakClass) var cls = cp.LineBreakClass;
{
case LineBreakClass.Ambiguous:
case LineBreakClass.Surrogate:
case LineBreakClass.Unknown:
return LineBreakClass.Alphabetic;
case LineBreakClass.ComplexContext:
return cp.GeneralCategory == GeneralCategory.NonspacingMark || cp.GeneralCategory == GeneralCategory.SpacingMark
? LineBreakClass.CombiningMark
: LineBreakClass.Alphabetic;
case LineBreakClass.ConditionalJapaneseStarter: const ulong specialMask =
return LineBreakClass.Nonstarter; (1UL << (int)LineBreakClass.Ambiguous) |
(1UL << (int)LineBreakClass.Surrogate) |
(1UL << (int)LineBreakClass.Unknown) |
(1UL << (int)LineBreakClass.ComplexContext) |
(1UL << (int)LineBreakClass.ConditionalJapaneseStarter);
default: if (((1UL << (int)cls) & specialMask) != 0UL)
return cp.LineBreakClass; {
switch (cls)
{
case LineBreakClass.Ambiguous:
case LineBreakClass.Surrogate:
case LineBreakClass.Unknown:
return LineBreakClass.Alphabetic;
case LineBreakClass.ComplexContext:
return cp.GeneralCategory is GeneralCategory.NonspacingMark or GeneralCategory.SpacingMark
? LineBreakClass.CombiningMark
: LineBreakClass.Alphabetic;
case LineBreakClass.ConditionalJapaneseStarter:
return LineBreakClass.Nonstarter;
}
} }
return cls;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LineBreakClass MapFirst(LineBreakClass c) private static LineBreakClass MapFirst(LineBreakClass c)
{ {
switch (c) switch (c)
@ -169,10 +180,80 @@ namespace Avalonia.Media.TextFormatting.Unicode
} }
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAlphaNumeric(LineBreakClass cls) private static bool IsAlphaNumeric(LineBreakClass cls)
=> cls == LineBreakClass.Alphabetic {
|| cls == LineBreakClass.HebrewLetter const ulong mask =
|| cls == LineBreakClass.Numeric; (1UL << (int)LineBreakClass.Alphabetic) |
(1UL << (int)LineBreakClass.HebrewLetter) |
(1UL << (int)LineBreakClass.Numeric);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPrefixPostfixNumericOrSpace(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.PostfixNumeric) |
(1UL << (int)LineBreakClass.PrefixNumeric) |
(1UL << (int)LineBreakClass.Space);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPrefixPostfixNumeric(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.PostfixNumeric) |
(1UL << (int)LineBreakClass.PrefixNumeric);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsClosePunctuationOrParenthesis(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.ClosePunctuation) |
(1UL << (int)LineBreakClass.CloseParenthesis);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsClosePunctuationOrInfixNumericOrBreakSymbols(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.ClosePunctuation) |
(1UL << (int)LineBreakClass.InfixNumeric) |
(1UL << (int)LineBreakClass.BreakSymbols);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsSpaceOrWordJoinerOrAlphabetic(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.Space) |
(1UL << (int)LineBreakClass.WordJoiner) |
(1UL << (int)LineBreakClass.Alphabetic);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsMandatoryBreakOrLineFeedOrCarriageReturn(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.MandatoryBreak) |
(1UL << (int)LineBreakClass.LineFeed) |
(1UL << (int)LineBreakClass.CarriageReturn);
return ((1UL << (int)cls) & mask) != 0UL;
}
private LineBreakClass PeekNextCharClass() private LineBreakClass PeekNextCharClass()
{ {
@ -198,83 +279,77 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Track combining mark exceptions. LB22 // Track combining mark exceptions. LB22
if (cls == LineBreakClass.CombiningMark) if (cls == LineBreakClass.CombiningMark)
{ {
switch (_currentClass) const ulong lb22ExMask =
(1UL << (int)LineBreakClass.MandatoryBreak) |
(1UL << (int)LineBreakClass.ContingentBreak) |
(1UL << (int)LineBreakClass.Exclamation) |
(1UL << (int)LineBreakClass.LineFeed) |
(1UL << (int)LineBreakClass.NextLine) |
(1UL << (int)LineBreakClass.Space) |
(1UL << (int)LineBreakClass.ZWSpace) |
(1UL << (int)LineBreakClass.CarriageReturn);
if (((1UL << (int)_currentClass) & lb22ExMask) != 0UL)
{ {
case LineBreakClass.MandatoryBreak: _lb22ex = true;
case LineBreakClass.ContingentBreak:
case LineBreakClass.Exclamation:
case LineBreakClass.LineFeed:
case LineBreakClass.NextLine:
case LineBreakClass.Space:
case LineBreakClass.ZWSpace:
case LineBreakClass.CarriageReturn:
_lb22ex = true;
break;
} }
}
// Track combining mark exceptions. LB31 const ulong lb31Mask =
if (_first && cls == LineBreakClass.CombiningMark) (1UL << (int)LineBreakClass.MandatoryBreak) |
{ (1UL << (int)LineBreakClass.ContingentBreak) |
_lb31 = true; (1UL << (int)LineBreakClass.Exclamation) |
(1UL << (int)LineBreakClass.LineFeed) |
(1UL << (int)LineBreakClass.NextLine) |
(1UL << (int)LineBreakClass.Space) |
(1UL << (int)LineBreakClass.ZWSpace) |
(1UL << (int)LineBreakClass.CarriageReturn) |
(1UL << (int)LineBreakClass.ZWJ);
// Track combining mark exceptions. LB31
if (_first || ((1UL << (int)_currentClass) & lb31Mask) != 0UL)
{
_lb31 = true;
}
} }
if (cls == LineBreakClass.CombiningMark) if (_first)
{ {
switch (_currentClass) // Rule LB24
if (IsClosePunctuationOrParenthesis(cls))
{ {
case LineBreakClass.MandatoryBreak: _lb24ex = true;
case LineBreakClass.ContingentBreak:
case LineBreakClass.Exclamation:
case LineBreakClass.LineFeed:
case LineBreakClass.NextLine:
case LineBreakClass.Space:
case LineBreakClass.ZWSpace:
case LineBreakClass.CarriageReturn:
case LineBreakClass.ZWJ:
_lb31 = true;
break;
} }
}
if (_first // Rule LB25
&& (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) if (IsClosePunctuationOrInfixNumericOrBreakSymbols(cls))
{ {
_lb31 = true; _lb25ex = true;
}
if (IsPrefixPostfixNumericOrSpace(cls))
{
_lb31 = true;
}
} }
if (_currentClass == LineBreakClass.Alphabetic && if (_currentClass == LineBreakClass.Alphabetic && IsPrefixPostfixNumericOrSpace(cls))
(cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space))
{ {
_lb31 = true; _lb31 = true;
} }
// Reset LB31 if next is U+0028 (Left Opening Parenthesis) // Reset LB31 if next is U+0028 (Left Opening Parenthesis)
if (_lb31 if (_lb31
&& _currentClass != LineBreakClass.PostfixNumeric && !IsPrefixPostfixNumeric(_currentClass)
&& _currentClass != LineBreakClass.PrefixNumeric && cls == LineBreakClass.OpenPunctuation
&& cls == LineBreakClass.OpenPunctuation && cp.Value == 0x0028) && cp.Value == 0x0028)
{ {
_lb31 = false; _lb31 = false;
} }
// Rule LB24 if (IsSpaceOrWordJoinerOrAlphabetic(cls))
if (_first && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.CloseParenthesis))
{
_lb24ex = true;
}
// Rule LB25
if (_first
&& (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.InfixNumeric || cls == LineBreakClass.BreakSymbols))
{
_lb25ex = true;
}
if (cls == LineBreakClass.Space || cls == LineBreakClass.WordJoiner || cls == LineBreakClass.Alphabetic)
{ {
var next = PeekNextCharClass(); var next = PeekNextCharClass();
if (next == LineBreakClass.ClosePunctuation || next == LineBreakClass.InfixNumeric || next == LineBreakClass.BreakSymbols) if (IsClosePunctuationOrInfixNumericOrBreakSymbols(next))
{ {
_lb25ex = true; _lb25ex = true;
} }
@ -295,6 +370,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
return cls; return cls;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool? GetSimpleBreak() private bool? GetSimpleBreak()
{ {
// handle classes not handled by the pair table // handle classes not handled by the pair table
@ -317,6 +393,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
return null; return null;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] // quite long but only one usage
private bool GetPairTableBreak(LineBreakClass lastClass) private bool GetPairTableBreak(LineBreakClass lastClass)
{ {
// If not handled already, use the pair table // If not handled already, use the pair table
@ -477,8 +554,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var cls = cp.LineBreakClass; var cls = cp.LineBreakClass;
if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || if (IsMandatoryBreakOrLineFeedOrCarriageReturn(cls))
cls == LineBreakClass.CarriageReturn)
{ {
from -= count; from -= count;
} }

6
src/Avalonia.Controls/TextBox.cs

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

1
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@ -23,6 +23,7 @@
<ItemGroup Label="InternalsVisibleTo"> <ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

6
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -52,6 +52,8 @@ namespace Avalonia.Skia
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var targetInfos = shapedBuffer.GlyphInfos;
var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan(); var glyphPositions = buffer.GetGlyphPositionSpan();
@ -77,9 +79,7 @@ namespace Avalonia.Skia
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
} }
var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
} }
return shapedBuffer; return shapedBuffer;

6
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -52,6 +52,8 @@ namespace Avalonia.Direct2D1.Media
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var targetInfos = shapedBuffer.GlyphInfos;
var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan(); var glyphPositions = buffer.GetGlyphPositionSpan();
@ -77,9 +79,7 @@ namespace Avalonia.Direct2D1.Media
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
} }
var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); targetInfos[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
} }
return shapedBuffer; return shapedBuffer;

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

@ -40,9 +40,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var enumerator = new GraphemeEnumerator(text); 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; bool pass = actual.Length == grapheme.Length;
@ -86,9 +86,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var count = 0; 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++; count++;
} }

47
tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs

@ -24,32 +24,33 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
public void BasicLatinTest() public void BasicLatinTest()
{ {
var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test."); var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.");
LineBreak lineBreak;
Assert.True(lineBreaker.MoveNext()); Assert.True(lineBreaker.MoveNext(out lineBreak));
Assert.Equal(6, lineBreaker.Current.PositionWrap); Assert.Equal(6, lineBreak.PositionWrap);
Assert.False(lineBreaker.Current.Required); Assert.False(lineBreak.Required);
Assert.True(lineBreaker.MoveNext()); Assert.True(lineBreaker.MoveNext(out lineBreak));
Assert.Equal(13, lineBreaker.Current.PositionWrap); Assert.Equal(13, lineBreak.PositionWrap);
Assert.True(lineBreaker.Current.Required); Assert.True(lineBreak.Required);
Assert.True(lineBreaker.MoveNext()); Assert.True(lineBreaker.MoveNext(out lineBreak));
Assert.Equal(18, lineBreaker.Current.PositionWrap); Assert.Equal(18, lineBreak.PositionWrap);
Assert.False(lineBreaker.Current.Required); Assert.False(lineBreak.Required);
Assert.True(lineBreaker.MoveNext()); Assert.True(lineBreaker.MoveNext(out lineBreak));
Assert.Equal(21, lineBreaker.Current.PositionWrap); Assert.Equal(21, lineBreak.PositionWrap);
Assert.False(lineBreaker.Current.Required); Assert.False(lineBreak.Required);
Assert.True(lineBreaker.MoveNext()); Assert.True(lineBreaker.MoveNext(out lineBreak));
Assert.Equal(23, lineBreaker.Current.PositionWrap); Assert.Equal(23, lineBreak.PositionWrap);
Assert.False(lineBreaker.Current.Required); Assert.False(lineBreak.Required);
Assert.True(lineBreaker.MoveNext()); Assert.True(lineBreaker.MoveNext(out lineBreak));
Assert.Equal(28, lineBreaker.Current.PositionWrap); Assert.Equal(28, lineBreak.PositionWrap);
Assert.False(lineBreaker.Current.Required); Assert.False(lineBreak.Required);
Assert.False(lineBreaker.MoveNext()); Assert.False(lineBreaker.MoveNext(out lineBreak));
} }
@ -72,9 +73,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
{ {
var breaks = new List<LineBreak>(); var breaks = new List<LineBreak>();
while (lineBreaker.MoveNext()) while (lineBreaker.MoveNext(out var lineBreak))
{ {
breaks.Add(lineBreaker.Current); breaks.Add(lineBreak);
} }
return breaks; return breaks;
@ -104,9 +105,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var foundBreaks = new List<int>(); var foundBreaks = new List<int>();
while (lineBreaker.MoveNext()) while (lineBreaker.MoveNext(out var lineBreak))
{ {
foundBreaks.Add(lineBreaker.Current.PositionWrap); foundBreaks.Add(lineBreak.PositionWrap);
} }
// Check the same // Check the same

1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -10,6 +10,7 @@
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" /> <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" /> <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

44
tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs

@ -3,6 +3,7 @@ using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Skia;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
@ -13,24 +14,35 @@ namespace Avalonia.Benchmarks.Text;
[MaxWarmupCount(15)] [MaxWarmupCount(15)]
public class HugeTextLayout : IDisposable public class HugeTextLayout : IDisposable
{ {
private static readonly Random s_rand = new();
private static readonly bool s_useSkia = true;
private readonly IDisposable _app; private readonly IDisposable _app;
private string[] _manySmallStrings; private readonly string[] _manySmallStrings;
private static Random _rand = new Random();
private static string RandomString(int length) private static string RandomString(int length)
{ {
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&?%$@"; const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&?%$@";
return new string(Enumerable.Repeat(chars, length).Select(s => s[_rand.Next(s.Length)]).ToArray()); return new string(Enumerable.Repeat(chars, length).Select(s => s[s_rand.Next(s.Length)]).ToArray());
} }
public HugeTextLayout() public HugeTextLayout()
{ {
_manySmallStrings = Enumerable.Range(0, 1000).Select(x => RandomString(_rand.Next(2, 15))).ToArray(); _manySmallStrings = Enumerable.Range(0, 1000).Select(_ => RandomString(s_rand.Next(2, 15))).ToArray();
_app = UnitTestApplication.Start(
TestServices.StyledWindow.With( var testServices = TestServices.StyledWindow.With(
renderInterface: new NullRenderingPlatform(), renderInterface: new NullRenderingPlatform(),
threadingInterface: new NullThreadingPlatform(), threadingInterface: new NullThreadingPlatform(),
standardCursorFactory: new NullCursorFactory())); standardCursorFactory: new NullCursorFactory());
if (s_useSkia)
{
testServices = testServices.With(
textShaperImpl: new TextShaperImpl(),
fontManagerImpl: new FontManagerImpl());
}
_app = UnitTestApplication.Start(testServices);
} }
private const string Text = @"Though, the objectives of the development of the prominent landmarks can be neglected in most cases, it should be realized that after the completion of the strategic decision gives rise to The Expertise of Regular Program (Carlton Cartwright in The Book of the Key Factor) private const string Text = @"Though, the objectives of the development of the prominent landmarks can be neglected in most cases, it should be realized that after the completion of the strategic decision gives rise to The Expertise of Regular Program (Carlton Cartwright in The Book of the Key Factor)
@ -77,7 +89,17 @@ In respect that the structure of the sufficient amount poses problems and challe
public TextLayout BuildEmojisTextLayout() => MakeLayout(Emojis); public TextLayout BuildEmojisTextLayout() => MakeLayout(Emojis);
[Benchmark] [Benchmark]
public TextLayout[] BuildManySmallTexts() => _manySmallStrings.Select(MakeLayout).ToArray(); public TextLayout[] BuildManySmallTexts()
{
var results = new TextLayout[_manySmallStrings.Length];
for (var i = 0; i < _manySmallStrings.Length; i++)
{
results[i] = MakeLayout(_manySmallStrings[i]);
}
return results;
}
[Benchmark] [Benchmark]
public void VirtualizeTextBlocks() public void VirtualizeTextBlocks()

4
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -283,9 +283,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var expected = new List<int>(); var expected = new List<int>();
while (lineBreaker.MoveNext()) while (lineBreaker.MoveNext(out var lineBreak))
{ {
expected.Add(lineBreaker.Current.PositionWrap - 1); expected.Add(lineBreak.PositionWrap - 1);
} }
var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" +

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

@ -151,9 +151,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
while (true) 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) if (j + i > text.Length)
{ {
@ -184,14 +185,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
if (!outer.MoveNext()) if (!outer.MoveNext(out grapheme))
{ {
break; break;
} }
inner = new GraphemeEnumerator(text); 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); 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.Length;
i += grapheme.Text.Length;
var layout = new TextLayout( var layout = new TextLayout(
text, text,

6
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@ -52,6 +52,8 @@ namespace Avalonia.UnitTests
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var targetInfos = shapedBuffer.GlyphInfos;
var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan(); var glyphPositions = buffer.GetGlyphPositionSpan();
@ -77,9 +79,7 @@ namespace Avalonia.UnitTests
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
} }
var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
} }
return shapedBuffer; return shapedBuffer;

10
tests/Avalonia.UnitTests/MockGlyphRun.cs

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Platform; using Avalonia.Platform;
@ -9,7 +8,14 @@ namespace Avalonia.UnitTests
{ {
public MockGlyphRun(IReadOnlyList<GlyphInfo> glyphInfos) public MockGlyphRun(IReadOnlyList<GlyphInfo> glyphInfos)
{ {
Size = new Size(glyphInfos.Sum(x=> x.GlyphAdvance), 10); var width = 0.0;
for (var i = 0; i < glyphInfos.Count; ++i)
{
width += glyphInfos[i].GlyphAdvance;
}
Size = new Size(width, 10);
} }
public Size Size { get; } public Size Size { get; }

3
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -13,6 +13,7 @@ namespace Avalonia.UnitTests
var fontRenderingEmSize = options.FontRenderingEmSize; var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel; var bidiLevel = options.BidiLevel;
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
var targetInfos = shapedBuffer.GlyphInfos;
var textSpan = text.Span; var textSpan = text.Span;
var textStartIndex = TextTestHelper.GetStartCharIndex(text); var textStartIndex = TextTestHelper.GetStartCharIndex(text);
@ -26,7 +27,7 @@ namespace Avalonia.UnitTests
for (var j = 0; j < count; ++j) for (var j = 0; j < count; ++j)
{ {
shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); targetInfos[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
} }
i += count; i += count;

Loading…
Cancel
Save