Browse Source

Merge pull request #10047 from MrJul/textlayout-microopts

More text layout optimizations
pull/10050/head
Benedikt Stebner 3 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.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Direct2D1, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml, 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);
var glyphTypeface = typeface.GlyphTypeface;
var glyphTypeface = GetOrAddGlyphTypeface(typeface);
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
return true;

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

@ -1,13 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
public sealed class FamilyNameCollection : IReadOnlyList<string>
{
private readonly string[] _names;
/// <summary>
/// Initializes a new instance of the <see cref="FamilyNameCollection"/> class.
/// </summary>
@ -20,13 +21,20 @@ namespace Avalonia.Media.Fonts
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>
/// Gets the primary family name.
/// </summary>
@ -43,14 +51,6 @@ namespace Avalonia.Media.Fonts
/// </value>
public bool HasFallbacks { get; }
/// <summary>
/// Gets the internal collection of names.
/// </summary>
/// <value>
/// The names.
/// </value>
internal IReadOnlyList<string> Names { get; }
/// <summary>
/// Returns an enumerator for the name collection.
/// </summary>
@ -76,23 +76,7 @@ namespace Avalonia.Media.Fonts
/// A <see cref="string" /> that represents this instance.
/// </returns>
public override string ToString()
{
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);
}
=> String.Join(", ", _names);
/// <summary>
/// Returns a hash code for this instance.
@ -102,7 +86,7 @@ namespace Avalonia.Media.Fonts
/// </returns>
public override int GetHashCode()
{
if (Count == 0)
if (_names.Length == 0)
{
return 0;
}
@ -111,9 +95,9 @@ namespace Avalonia.Media.Fonts
{
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();
}
@ -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>.
/// </returns>
public override bool Equals(object? obj)
{
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;
}
=> obj is FamilyNameCollection other && _names.AsSpan().SequenceEqual(other._names);
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);
while (graphemeEnumerator.MoveNext())
while (graphemeEnumerator.MoveNext(out var grapheme))
{
var grapheme = graphemeEnumerator.Current;
finalLength += grapheme.Text.Length;
finalLength += grapheme.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);
while (lineBreakEnumerator.MoveNext())
while (lineBreakEnumerator.MoveNext(out var currentBreak))
{
var currentBreak = lineBreakEnumerator.Current;
if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
{
breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);

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

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

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

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

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

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

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

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

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

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

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

@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting
/// </remarks>
public abstract class TextRunProperties : IEquatable<TextRunProperties>
{
private IGlyphTypeface? _cachedGlyphTypeFace;
/// <summary>
/// Run typeface
/// </summary>
@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
internal IGlyphTypeface CachedGlyphTypeface
=> _cachedGlyphTypeFace ??= Typeface.GlyphTypeface;
public bool Equals(TextRunProperties? 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;
}
[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>
/// Build a list of matching isolates for a directionality slice
/// Implements BD9
@ -701,28 +712,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
var lastType = _workingClasses[lastCharIndex];
int nextLevel;
switch (lastType)
if (IsIsolateStart(lastType))
{
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
nextLevel = _paragraphEmbeddingLevel;
}
else
{
i = lastCharIndex + 1;
while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
{
nextLevel = _paragraphEmbeddingLevel;
break;
i++;
}
default:
{
i = lastCharIndex + 1;
while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
{
i++;
}
nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i];
break;
}
nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i];
}
var eos = DirectionFromLevel(Math.Max(nextLevel, level));
@ -831,8 +833,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// PDI and concatenate that run to this one
var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1];
var lastType = _originalClasses[lastCharacterIndex];
if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) &&
_isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
if (IsIsolateStart(lastType) && _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
{
// Find the continuing run index
runIndex = FindRunForIndex(nextRunIndex);
@ -869,74 +870,59 @@ namespace Avalonia.Media.TextFormatting.Unicode
_runDirection = DirectionFromLevel(runLevel);
_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
// Also, set hasXX flags
int i;
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++)
{
var resolvedClass = _runResolvedClasses[i];
switch (resolvedClass)
{
case BidiClass.NonspacingMark:
_runResolvedClasses[i] = previousClass;
break;
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
if (resolvedClass == BidiClass.NonspacingMark)
{
_runResolvedClasses[i] = previousClass;
}
else
{
var classBit = 1U << (int)resolvedClass;
if ((classBit & isolateMask) != 0U)
{
previousClass = BidiClass.OtherNeutral;
break;
case BidiClass.EuropeanNumber:
hasEN = true;
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:
}
else
{
wRules |= classBit & wRulesMask;
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
if (hasEN)
{
@ -1548,23 +1534,20 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsWhitespace(BidiClass biDiClass)
{
switch (biDiClass)
{
case BidiClass.LeftToRightEmbedding:
case BidiClass.RightToLeftEmbedding:
case BidiClass.LeftToRightOverride:
case BidiClass.RightToLeftOverride:
case BidiClass.PopDirectionalFormat:
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
case BidiClass.BoundaryNeutral:
case BidiClass.WhiteSpace:
return true;
default:
return false;
}
const uint mask =
(1U << (int)BidiClass.LeftToRightEmbedding) |
(1U << (int)BidiClass.RightToLeftEmbedding) |
(1U << (int)BidiClass.LeftToRightOverride) |
(1U << (int)BidiClass.RightToLeftOverride) |
(1U << (int)BidiClass.PopDirectionalFormat) |
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate) |
(1U << (int)BidiClass.PopDirectionalIsolate) |
(1U << (int)BidiClass.BoundaryNeutral) |
(1U << (int)BidiClass.WhiteSpace);
return ((1U << (int)biDiClass) & mask) != 0U;
}
/// <summary>
@ -1585,18 +1568,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsRemovedByX9(BidiClass biDiClass)
{
switch (biDiClass)
{
case BidiClass.LeftToRightEmbedding:
case BidiClass.RightToLeftEmbedding:
case BidiClass.LeftToRightOverride:
case BidiClass.RightToLeftOverride:
case BidiClass.PopDirectionalFormat:
case BidiClass.BoundaryNeutral:
return true;
default:
return false;
}
const uint mask =
(1U << (int)BidiClass.LeftToRightEmbedding) |
(1U << (int)BidiClass.RightToLeftEmbedding) |
(1U << (int)BidiClass.LeftToRightOverride) |
(1U << (int)BidiClass.RightToLeftOverride) |
(1U << (int)BidiClass.PopDirectionalFormat) |
(1U << (int)BidiClass.BoundaryNeutral);
return ((1U << (int)biDiClass) & mask) != 0U;
}
/// <summary>
@ -1605,20 +1585,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsNeutralClass(BidiClass direction)
{
switch (direction)
{
case BidiClass.ParagraphSeparator:
case BidiClass.SegmentSeparator:
case BidiClass.WhiteSpace:
case BidiClass.OtherNeutral:
case BidiClass.RightToLeftIsolate:
case BidiClass.LeftToRightIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
return true;
default:
return false;
}
const uint mask =
(1U << (int)BidiClass.ParagraphSeparator) |
(1U << (int)BidiClass.SegmentSeparator) |
(1U << (int)BidiClass.WhiteSpace) |
(1U << (int)BidiClass.OtherNeutral) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate) |
(1U << (int)BidiClass.PopDirectionalIsolate);
return ((1U << (int)direction) & mask) != 0U;
}
/// <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
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);
while (codePointEnumerator.MoveNext())
while (codePointEnumerator.MoveNext(out var codepoint))
{
var codepoint = codePointEnumerator.Current;
// Look up BiDiClass
var dir = codepoint.BiDiClass;
_classes[i] = dir;
switch (dir)
{
case BidiClass.LeftToRightEmbedding:
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;
}
}
var dirBit = 1U << (int)dir;
HasEmbeddings = (dirBit & embeddingMask) != 0U;
HasIsolates = (dirBit & isolateMask) != 0U;
// Lookup paired bracket types
var pbt = codepoint.PairedBracketType;

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

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode
@ -11,13 +10,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// The replacement codepoint that is used for non supported values.
/// </summary>
public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD');
public Codepoint(uint value)
public static Codepoint ReplacementCodepoint
{
_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>
/// Get the codepoint's value.
/// </summary>
@ -87,19 +92,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </returns>
public bool IsWhiteSpace
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
switch (GeneralCategory)
{
case GeneralCategory.Control:
case GeneralCategory.NonspacingMark:
case GeneralCategory.Format:
case GeneralCategory.SpaceSeparator:
case GeneralCategory.SpacingMark:
return true;
}
return false;
const ulong whiteSpaceMask =
(1UL << (int)GeneralCategory.Control) |
(1UL << (int)GeneralCategory.NonspacingMark) |
(1UL << (int)GeneralCategory.Format) |
(1UL << (int)GeneralCategory.SpaceSeparator) |
(1UL << (int)GeneralCategory.SpacingMark);
return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0UL;
}
}
@ -166,56 +169,62 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <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)
{
// Perf note: this method is performance critical for text layout, modify with care!
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;
}
var code = text[index];
ushort hi, low;
uint code = text[index];
//# High surrogate
if (0xD800 <= code && code <= 0xDBFF)
//# Surrogate
if (IsInRangeInclusive(code, 0xD800U, 0xDFFFU))
{
hi = code;
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;
}
uint hi, low;
//# Low surrogate
if (0xDC00 <= code && code <= 0xDFFF)
{
if (index == 0)
//# High surrogate
if (code <= 0xDBFF)
{
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 = code;
if (0xD800 <= hi && hi <= 0xDBFF)
//# Low surrogate
else
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
if (index > 0)
{
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;
@ -224,12 +233,16 @@ namespace Avalonia.Media.TextFormatting.Unicode
return new Codepoint(code);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound)
=> value - lowerBound <= upperBound - lowerBound;
/// <summary>
/// Returns <see langword="true"/> if <paramref name="cp"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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
{
private ReadOnlySpan<char> _text;
private readonly ReadOnlySpan<char> _text;
private int _offset;
public CodepointEnumerator(ReadOnlySpan<char> text)
{
_text = text;
Current = Codepoint.ReplacementCodepoint;
}
/// <summary>
/// Gets the current <see cref="Codepoint"/>.
/// </summary>
public Codepoint Current { get; private set; }
=> _text = text;
/// <summary>
/// Moves to the next <see cref="Codepoint"/>.
/// </summary>
/// <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;
}
Current = Codepoint.ReadAt(_text, 0, out var count);
codepoint = Codepoint.ReadAt(_text, _offset, out var count);
_text = _text.Slice(count);
_offset += count;
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>
/// Represents the smallest unit of a writing system of any given language.
/// </summary>
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
public Grapheme(Codepoint firstCodepoint, int offset, int length)
{
FirstCodepoint = firstCodepoint;
Text = text;
Offset = offset;
Length = length;
}
/// <summary>
@ -19,8 +18,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; }
/// <summary>
/// The text of the grapheme cluster
/// Gets the starting code unit offset of this grapheme inside its containing text.
/// </summary>
public int Offset { get; }
/// <summary>
/// Gets the length of this grapheme, in code units.
/// </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.
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Media.TextFormatting.Unicode
{
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)
{
_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>
/// Moves to the next <see cref="Grapheme"/>.
/// </summary>
/// <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;
}
// Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules.
var processor = new Processor(_text);
processor.MoveNext();
if (startOffset == 0)
{
ReadNextCodepoint();
}
var firstCodepoint = processor.CurrentCodepoint;
var firstCodepoint = _currentCodepoint;
// 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.
// 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
|| processor.CurrentType == GraphemeBreakClass.CR
|| processor.CurrentType == GraphemeBreakClass.LF)
const uint controlCrLfMask =
(1U << (int)GraphemeBreakClass.Control) |
(1U << (int)GraphemeBreakClass.CR) |
(1U << (int)GraphemeBreakClass.LF);
if (((1U << (int)_currentType) & controlCrLfMask) != 0U)
{
goto Return;
}
@ -62,19 +84,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Now begin the main state machine.
var previousClusterBreakType = processor.CurrentType;
var previousClusterBreakType = _currentType;
processor.MoveNext();
ReadNextCodepoint();
switch (previousClusterBreakType)
{
case GraphemeBreakClass.CR:
if (processor.CurrentType != GraphemeBreakClass.LF)
if (_currentType != GraphemeBreakClass.LF)
{
goto Return; // rules GB3 & GB4 (only <LF> can follow <CR>)
}
processor.MoveNext();
ReadNextCodepoint();
goto case GraphemeBreakClass.LF;
case GraphemeBreakClass.Control:
@ -82,53 +104,57 @@ namespace Avalonia.Media.TextFormatting.Unicode
goto Return; // rule GB4 (no data after Control | LF)
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;
}
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;
}
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;
}
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;
}
else
{
break;
}
}
case GraphemeBreakClass.LV:
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;
}
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;
}
else
{
break;
}
}
case GraphemeBreakClass.LVT:
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;
}
else
@ -139,123 +165,79 @@ namespace Avalonia.Media.TextFormatting.Unicode
case GraphemeBreakClass.ExtendedPictographic:
// Attempt processing extended pictographic (rules GB11, GB9).
// 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.
if (processor.CurrentType != GraphemeBreakClass.ZWJ)
if (_currentType != GraphemeBreakClass.ZWJ)
{
break;
}
processor.MoveNext();
if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic)
ReadNextCodepoint();
if (_currentType != GraphemeBreakClass.ExtendedPictographic)
{
break;
}
processor.MoveNext();
ReadNextCodepoint();
goto case GraphemeBreakClass.ExtendedPictographic;
case GraphemeBreakClass.RegionalIndicator:
// 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.
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
while (processor.CurrentType == GraphemeBreakClass.Extend
|| processor.CurrentType == GraphemeBreakClass.ZWJ
|| processor.CurrentType == GraphemeBreakClass.SpacingMark)
while (((1U << (int)_currentType) & gb9Mask) != 0U)
{
processor.MoveNext();
ReadNextCodepoint();
}
Return:
Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset));
_text = _text.Slice(processor.CurrentCodeUnitOffset);
var graphemeLength = _currentCodeUnitOffset - startOffset;
grapheme = new Grapheme(firstCodepoint, startOffset, graphemeLength);
return true; // rules GB2, GB999
}
[StructLayout(LayoutKind.Auto)]
private ref struct Processor
private void ReadNextCodepoint()
{
private readonly ReadOnlySpan<char> _buffer;
private int _codeUnitLengthOfCurrentScalar;
internal Processor(ReadOnlySpan<char> buffer)
{
_buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0;
CurrentCodepoint = Codepoint.ReplacementCodepoint;
CurrentType = GraphemeBreakClass.Other;
CurrentCodeUnitOffset = 0;
}
public int CurrentCodeUnitOffset { get; private set; }
/// <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>
public GraphemeBreakClass CurrentType { get; private set; }
/// <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;
}
// 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.
_currentCodeUnitOffset += _codeUnitLengthOfCurrentCodepoint;
_currentCodepoint = Codepoint.ReadAt(_text, _currentCodeUnitOffset,
out _codeUnitLengthOfCurrentCodepoint);
_currentType = _currentCodepoint.GraphemeBreakClass;
}
}
}

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

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

6
src/Avalonia.Controls/TextBox.cs

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

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

@ -23,6 +23,7 @@
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
</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 targetInfos = shapedBuffer.GlyphInfos;
var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
@ -77,9 +79,7 @@ namespace Avalonia.Skia
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
}
var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
}
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 targetInfos = shapedBuffer.GlyphInfos;
var glyphInfos = buffer.GetGlyphInfoSpan();
var glyphPositions = buffer.GetGlyphPositionSpan();
@ -77,9 +79,7 @@ namespace Avalonia.Direct2D1.Media
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
}
var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
targetInfos[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
}
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);
enumerator.MoveNext();
enumerator.MoveNext(out var g);
var actual = enumerator.Current.Text;
var actual = text.AsSpan(g.Offset, g.Length);
bool pass = actual.Length == grapheme.Length;
@ -86,9 +86,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var count = 0;
while (enumerator.MoveNext())
while (enumerator.MoveNext(out var grapheme))
{
Assert.Equal(1, enumerator.Current.Text.Length);
Assert.Equal(1, grapheme.Length);
count++;
}

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

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

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

@ -10,6 +10,7 @@
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.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" />
</ItemGroup>
<ItemGroup>

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

@ -3,6 +3,7 @@ using System.Linq;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Skia;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
@ -13,24 +14,35 @@ namespace Avalonia.Benchmarks.Text;
[MaxWarmupCount(15)]
public class HugeTextLayout : IDisposable
{
private static readonly Random s_rand = new();
private static readonly bool s_useSkia = true;
private readonly IDisposable _app;
private string[] _manySmallStrings;
private static Random _rand = new Random();
private readonly string[] _manySmallStrings;
private static string RandomString(int length)
{
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()
{
_manySmallStrings = Enumerable.Range(0, 1000).Select(x => RandomString(_rand.Next(2, 15))).ToArray();
_app = UnitTestApplication.Start(
TestServices.StyledWindow.With(
renderInterface: new NullRenderingPlatform(),
threadingInterface: new NullThreadingPlatform(),
standardCursorFactory: new NullCursorFactory()));
_manySmallStrings = Enumerable.Range(0, 1000).Select(_ => RandomString(s_rand.Next(2, 15))).ToArray();
var testServices = TestServices.StyledWindow.With(
renderInterface: new NullRenderingPlatform(),
threadingInterface: new NullThreadingPlatform(),
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)
@ -77,7 +89,17 @@ In respect that the structure of the sufficient amount poses problems and challe
public TextLayout BuildEmojisTextLayout() => MakeLayout(Emojis);
[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]
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>();
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#" +

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

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

6
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

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

10
tests/Avalonia.UnitTests/MockGlyphRun.cs

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
@ -9,7 +8,14 @@ namespace Avalonia.UnitTests
{
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; }

3
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -13,6 +13,7 @@ namespace Avalonia.UnitTests
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
var targetInfos = shapedBuffer.GlyphInfos;
var textSpan = text.Span;
var textStartIndex = TextTestHelper.GetStartCharIndex(text);
@ -26,7 +27,7 @@ namespace Avalonia.UnitTests
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;

Loading…
Cancel
Save