Browse Source

Merge pull request #10013 from MrJul/textlayout-allocs

Reduced text layout allocations
pull/10015/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
53bc1ff2d6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 518
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 268
      src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
  3. 84
      src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs
  4. 135
      src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs
  5. 36
      src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs
  6. 5
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  7. 230
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  8. 24
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  9. 11
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  10. 11
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  11. 6
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  12. 57
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  13. 222
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  14. 72
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  15. 104
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  16. 294
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  17. 3
      src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
  18. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  19. 8
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  20. 100
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  21. 53
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  22. 9
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  23. 2
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  24. 66
      src/Avalonia.Base/Utilities/ArrayBuilder.cs
  25. 11
      src/Avalonia.Base/Utilities/ArraySlice.cs
  26. 29
      src/Avalonia.Base/Utilities/BidiDictionary.cs
  27. 3
      src/Avalonia.Base/Utilities/BinarySearchExtension.cs
  28. 2
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  29. 3
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  30. 83
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  31. 2
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  32. 42
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  33. 2
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  34. 14
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  35. 6
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs
  36. 151
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs
  37. 3
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  38. 3
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  39. 17
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  40. 10
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  41. 21
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  42. 10
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  43. 55
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  44. 30
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  45. 10
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
  46. 2
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  47. 3
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  48. 5
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs
  49. BIN
      tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

518
src/Avalonia.Base/Media/GlyphRun.cs

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -11,64 +13,112 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public sealed class GlyphRun : IDisposable public sealed class GlyphRun : IDisposable
{ {
private static readonly IComparer<int> s_ascendingComparer = Comparer<int>.Default;
private static readonly IComparer<int> s_descendingComparer = new ReverseComparer<int>();
private IGlyphRunImpl? _glyphRunImpl; private IGlyphRunImpl? _glyphRunImpl;
private IGlyphTypeface _glyphTypeface;
private double _fontRenderingEmSize; private double _fontRenderingEmSize;
private int _biDiLevel; private int _biDiLevel;
private Point? _baselineOrigin; private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics; private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlyMemory<char> _characters; private ReadOnlyMemory<char> _characters;
private IReadOnlyList<ushort> _glyphIndices; private IReadOnlyList<GlyphInfo> _glyphInfos;
private IReadOnlyList<double>? _glyphAdvances; private bool _hasOneCharPerCluster; // if true, character index and cluster are similar
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class. /// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary> /// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param> /// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The rendering em size.</param> /// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="glyphAdvances">The glyph advances.</param>
/// <param name="glyphOffsets">The glyph offsets.</param>
/// <param name="characters">The characters.</param> /// <param name="characters">The characters.</param>
/// <param name="glyphClusters">The glyph clusters.</param> /// <param name="glyphIndices">The glyph indices.</param>
/// <param name="biDiLevel">The bidi level.</param> /// <param name="biDiLevel">The bidi level.</param>
public GlyphRun( public GlyphRun(
IGlyphTypeface glyphTypeface, IGlyphTypeface glyphTypeface,
double fontRenderingEmSize, double fontRenderingEmSize,
ReadOnlyMemory<char> characters, ReadOnlyMemory<char> characters,
IReadOnlyList<ushort> glyphIndices, IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null, int biDiLevel = 0)
IReadOnlyList<Vector>? glyphOffsets = null, : this(glyphTypeface, fontRenderingEmSize, characters,
IReadOnlyList<int>? glyphClusters = null, CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel)
{
_hasOneCharPerCluster = true;
}
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="characters">The characters.</param>
/// <param name="glyphInfos">The list of glyphs used.</param>
/// <param name="biDiLevel">The bidi level.</param>
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlyMemory<char> characters,
IReadOnlyList<GlyphInfo> glyphInfos,
int biDiLevel = 0) int biDiLevel = 0)
{ {
_glyphTypeface = glyphTypeface; GlyphTypeface = glyphTypeface;
_fontRenderingEmSize = fontRenderingEmSize; _fontRenderingEmSize = fontRenderingEmSize;
_characters = characters; _characters = characters;
_glyphIndices = glyphIndices; _glyphInfos = glyphInfos;
_glyphAdvances = glyphAdvances; _biDiLevel = biDiLevel;
}
_glyphOffsets = glyphOffsets; private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices,
double fontRenderingEmSize, IGlyphTypeface glyphTypeface)
{
var glyphIndexSpan = ListToSpan(glyphIndices);
var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan);
_glyphClusters = glyphClusters; var glyphInfos = new GlyphInfo[glyphIndexSpan.Length];
var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
_biDiLevel = biDiLevel; for (var i = 0; i < glyphIndexSpan.Length; ++i)
{
glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale);
}
return glyphInfos;
}
private static ReadOnlySpan<ushort> ListToSpan(IReadOnlyList<ushort> list)
{
var count = list.Count;
if (count == 0)
{
return default;
}
if (list is ushort[] array)
{
return array.AsSpan();
}
#if NET6_0_OR_GREATER
if (list is List<ushort> concreteList)
{
return CollectionsMarshal.AsSpan(concreteList);
}
#endif
array = new ushort[count];
for (var i = 0; i < count; ++i)
{
array[i] = list[i];
}
return array.AsSpan();
} }
/// <summary> /// <summary>
/// Gets the <see cref="IGlyphTypeface"/> for the <see cref="GlyphRun"/>. /// Gets the <see cref="IGlyphTypeface"/> for the <see cref="GlyphRun"/>.
/// </summary> /// </summary>
public IGlyphTypeface GlyphTypeface => _glyphTypeface; public IGlyphTypeface GlyphTypeface { get; }
/// <summary> /// <summary>
/// Gets or sets the em size used for rendering the <see cref="GlyphRun"/>. /// Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
@ -88,56 +138,17 @@ namespace Avalonia.Media
/// ///
/// </summary> /// </summary>
public GlyphRunMetrics Metrics public GlyphRunMetrics Metrics
{ => _glyphRunMetrics ??= CreateGlyphRunMetrics();
get
{
_glyphRunMetrics ??= CreateGlyphRunMetrics();
return _glyphRunMetrics.Value;
}
}
/// <summary> /// <summary>
/// Gets or sets the baseline origin of the<see cref="GlyphRun"/>. /// Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
/// </summary> /// </summary>
public Point BaselineOrigin public Point BaselineOrigin
{ {
get get => _baselineOrigin ??= CalculateBaselineOrigin();
{
_baselineOrigin ??= CalculateBaselineOrigin();
return _baselineOrigin.Value;
}
set => Set(ref _baselineOrigin, value); set => Set(ref _baselineOrigin, value);
} }
/// <summary>
/// Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
/// </summary>
public IReadOnlyList<ushort> GlyphIndices
{
get => _glyphIndices;
set => Set(ref _glyphIndices, value);
}
/// <summary>
/// Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices.
/// </summary>
public IReadOnlyList<double>? GlyphAdvances
{
get => _glyphAdvances;
set => Set(ref _glyphAdvances, value);
}
/// <summary>
/// Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>.
/// </summary>
public IReadOnlyList<Vector>? GlyphOffsets
{
get => _glyphOffsets;
set => Set(ref _glyphOffsets, value);
}
/// <summary> /// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>. /// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary> /// </summary>
@ -148,12 +159,16 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index. /// Gets or sets the list of glyphs to use to render this run.
/// </summary> /// </summary>
public IReadOnlyList<int>? GlyphClusters public IReadOnlyList<GlyphInfo> GlyphInfos
{ {
get => _glyphClusters; get => _glyphInfos;
set => Set(ref _glyphClusters, value); set
{
Set(ref _glyphInfos, value);
_hasOneCharPerCluster = false;
}
} }
/// <summary> /// <summary>
@ -179,17 +194,7 @@ namespace Avalonia.Media
/// The platform implementation of the <see cref="GlyphRun"/>. /// The platform implementation of the <see cref="GlyphRun"/>.
/// </summary> /// </summary>
public IGlyphRunImpl GlyphRunImpl public IGlyphRunImpl GlyphRunImpl
{ => _glyphRunImpl ??= CreateGlyphRunImpl();
get
{
if (_glyphRunImpl == null)
{
Initialize();
}
return _glyphRunImpl!;
}
}
/// <summary> /// <summary>
/// Obtains geometry for the glyph run. /// Obtains geometry for the glyph run.
@ -221,38 +226,32 @@ namespace Avalonia.Media
if (IsLeftToRight) if (IsLeftToRight)
{ {
if (GlyphClusters != null) if (characterIndex < Metrics.FirstCluster)
{ {
if (characterIndex < Metrics.FirstCluster) return 0;
{ }
return 0;
}
if (characterIndex > Metrics.LastCluster) if (characterIndex > Metrics.LastCluster)
{ {
return Metrics.WidthIncludingTrailingWhitespace; return Metrics.WidthIncludingTrailingWhitespace;
}
} }
var glyphIndex = FindGlyphIndex(characterIndex); var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null) var currentCluster = _glyphInfos[glyphIndex].GlyphCluster;
{
var currentCluster = GlyphClusters[glyphIndex];
//Move to the end of the glyph cluster //Move to the end of the glyph cluster
if (characterHit.TrailingLength > 0) if (characterHit.TrailingLength > 0)
{
while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster)
{ {
while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster) glyphIndex++;
{
glyphIndex++;
}
} }
} }
for (var i = 0; i < glyphIndex; i++) for (var i = 0; i < glyphIndex; i++)
{ {
distance += GetGlyphAdvance(i, out _); distance += _glyphInfos[i].GlyphAdvance;
} }
return distance; return distance;
@ -262,22 +261,19 @@ namespace Avalonia.Media
//RightToLeft //RightToLeft
var glyphIndex = FindGlyphIndex(characterIndex); var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null && GlyphClusters.Count > 0) if (characterIndex > Metrics.LastCluster)
{ {
if (characterIndex > Metrics.LastCluster) return 0;
{ }
return 0;
}
if (characterIndex <= Metrics.FirstCluster) if (characterIndex <= Metrics.FirstCluster)
{ {
return Size.Width; return Size.Width;
}
} }
for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++) for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++)
{ {
distance += GetGlyphAdvance(i, out _); distance += _glyphInfos[i].GlyphAdvance;
} }
return Size.Width - distance; return Size.Width - distance;
@ -322,11 +318,12 @@ namespace Avalonia.Media
if (IsLeftToRight) if (IsLeftToRight)
{ {
for (var index = 0; index < GlyphIndices.Count; index++) for (var index = 0; index < _glyphInfos.Count; index++)
{ {
var advance = GetGlyphAdvance(index, out var cluster); var glyphInfo = _glyphInfos[index];
var advance = glyphInfo.GlyphAdvance;
characterIndex = cluster; characterIndex = glyphInfo.GlyphCluster;
if (distance > currentX && distance <= currentX + advance) if (distance > currentX && distance <= currentX + advance)
{ {
@ -340,11 +337,12 @@ namespace Avalonia.Media
{ {
currentX = Size.Width; currentX = Size.Width;
for (var index = GlyphIndices.Count - 1; index >= 0; index--) for (var index = _glyphInfos.Count - 1; index >= 0; index--)
{ {
var advance = GetGlyphAdvance(index, out var cluster); var glyphInfo = _glyphInfos[index];
var advance = glyphInfo.GlyphAdvance;
characterIndex = cluster; characterIndex = glyphInfo.GlyphCluster;
var offsetX = currentX - advance; var offsetX = currentX - advance;
@ -424,7 +422,7 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public int FindGlyphIndex(int characterIndex) public int FindGlyphIndex(int characterIndex)
{ {
if (GlyphClusters == null || GlyphClusters.Count == 0) if (_hasOneCharPerCluster)
{ {
return characterIndex; return characterIndex;
} }
@ -433,7 +431,7 @@ namespace Avalonia.Media
{ {
if (IsLeftToRight) if (IsLeftToRight)
{ {
return GlyphIndices.Count - 1; return _glyphInfos.Count - 1;
} }
return 0; return 0;
@ -446,15 +444,13 @@ namespace Avalonia.Media
return 0; return 0;
} }
return GlyphIndices.Count - 1; return _glyphInfos.Count - 1;
} }
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer;
var clusters = GlyphClusters;
// Find the start of the cluster at the character index. // Find the start of the cluster at the character index.
var start = clusters.BinarySearch(characterIndex, comparer); var start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer);
// No cluster found. // No cluster found.
if (start < 0) if (start < 0)
@ -463,40 +459,38 @@ namespace Avalonia.Media
{ {
characterIndex--; characterIndex--;
start = clusters.BinarySearch(characterIndex, comparer); start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer);
} }
if (start < 0) if (start < 0)
{ {
goto result; return 0;
} }
} }
if (IsLeftToRight) if (IsLeftToRight)
{ {
while (start > 0 && clusters[start - 1] == clusters[start]) while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster)
{ {
start--; start--;
} }
} }
else else
{ {
while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start]) while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster)
{ {
start++; start++;
} }
} }
result:
if (start < 0) if (start < 0)
{ {
return 0; return 0;
} }
if (start > GlyphIndices.Count - 1) if (start > _glyphInfos.Count - 1)
{ {
return GlyphIndices.Count - 1; return _glyphInfos.Count - 1;
} }
return start; return start;
@ -516,14 +510,14 @@ namespace Avalonia.Media
var glyphIndex = FindGlyphIndex(index); var glyphIndex = FindGlyphIndex(index);
if (GlyphClusters == null) if (_hasOneCharPerCluster)
{ {
width = GetGlyphAdvance(index, out _); width = _glyphInfos[index].GlyphAdvance;
return new CharacterHit(glyphIndex, 1); return new CharacterHit(glyphIndex, 1);
} }
var cluster = GlyphClusters[glyphIndex]; var cluster = _glyphInfos[glyphIndex].GlyphCluster;
var nextCluster = cluster; var nextCluster = cluster;
@ -531,13 +525,13 @@ namespace Avalonia.Media
while (nextCluster == cluster) while (nextCluster == cluster)
{ {
width += GetGlyphAdvance(currentIndex, out _); width += _glyphInfos[currentIndex].GlyphAdvance;
if (IsLeftToRight) if (IsLeftToRight)
{ {
currentIndex++; currentIndex++;
if (currentIndex == GlyphClusters.Count) if (currentIndex == _glyphInfos.Count)
{ {
break; break;
} }
@ -552,7 +546,7 @@ namespace Avalonia.Media
} }
} }
nextCluster = GlyphClusters[currentIndex]; nextCluster = _glyphInfos[currentIndex].GlyphCluster;
} }
var clusterLength = Math.Max(0, nextCluster - cluster); var clusterLength = Math.Max(0, nextCluster - cluster);
@ -565,9 +559,9 @@ namespace Avalonia.Media
if (IsLeftToRight) if (IsLeftToRight)
{ {
for (int i = 1; i < GlyphClusters.Count; i++) for (int i = 1; i < _glyphInfos.Count; i++)
{ {
nextCluster = GlyphClusters[i]; nextCluster = _glyphInfos[i].GlyphCluster;
if (currentCluster > cluster) if (currentCluster > cluster)
{ {
@ -583,9 +577,9 @@ namespace Avalonia.Media
} }
else else
{ {
for (int i = GlyphClusters.Count - 1; i >= 0; i--) for (int i = _glyphInfos.Count - 1; i >= 0; i--)
{ {
nextCluster = GlyphClusters[i]; nextCluster = _glyphInfos[i].GlyphCluster;
if (currentCluster > cluster) if (currentCluster > cluster)
{ {
@ -613,26 +607,6 @@ namespace Avalonia.Media
return new CharacterHit(cluster, clusterLength); return new CharacterHit(cluster, clusterLength);
} }
/// <summary>
/// Gets a glyph's width.
/// </summary>
/// <param name="index">The glyph index.</param>
/// <param name="cluster">The current cluster.</param>
/// <returns>The glyph's width.</returns>
private double GetGlyphAdvance(int index, out int cluster)
{
cluster = GlyphClusters != null ? GlyphClusters[index] : index;
if (GlyphAdvances != null)
{
return GlyphAdvances[index];
}
var glyph = GlyphIndices[index];
return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
/// <summary> /// <summary>
/// Calculates the default baseline origin of the <see cref="GlyphRun"/>. /// Calculates the default baseline origin of the <see cref="GlyphRun"/>.
/// </summary> /// </summary>
@ -644,20 +618,17 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics() private GlyphRunMetrics CreateGlyphRunMetrics()
{ {
int firstCluster = 0, lastCluster = 0; int firstCluster, lastCluster;
if (_glyphClusters != null && _glyphClusters.Count > 0) if (Characters.IsEmpty)
{ {
firstCluster = _glyphClusters[0]; firstCluster = 0;
lastCluster = _glyphClusters[_glyphClusters.Count - 1]; lastCluster = 0;
} }
else else
{ {
if (!Characters.IsEmpty) firstCluster = _glyphInfos[0].GlyphCluster;
{ lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster;
firstCluster = 0;
lastCluster = Characters.Length - 1;
}
} }
if (!IsLeftToRight) if (!IsLeftToRight)
@ -671,9 +642,9 @@ namespace Avalonia.Media
var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount);
for (var index = 0; index < GlyphIndices.Count; index++) for (var index = 0; index < _glyphInfos.Count; index++)
{ {
var advance = GetGlyphAdvance(index, out _); var advance = _glyphInfos[index].GlyphAdvance;
widthIncludingTrailingWhitespace += advance; widthIncludingTrailingWhitespace += advance;
} }
@ -684,14 +655,14 @@ namespace Avalonia.Media
{ {
for (var index = 0; index < glyphCount; index++) for (var index = 0; index < glyphCount; index++)
{ {
width -= GetGlyphAdvance(index, out _); width -= _glyphInfos[index].GlyphAdvance;
} }
} }
else else
{ {
for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) for (var index = _glyphInfos.Count - glyphCount; index < _glyphInfos.Count; index++)
{ {
width -= GetGlyphAdvance(index, out _); width -= _glyphInfos[index].GlyphAdvance;
} }
} }
@ -710,7 +681,7 @@ namespace Avalonia.Media
{ {
if (isReversed) if (isReversed)
{ {
return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
} }
glyphCount = 0; glyphCount = 0;
@ -720,84 +691,59 @@ namespace Avalonia.Media
if (!charactersSpan.IsEmpty) if (!charactersSpan.IsEmpty)
{ {
if (GlyphClusters == null) var characterIndex = charactersSpan.Length - 1;
{
for (var i = charactersSpan.Length - 1; i >= 0;)
{
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i -= count; for (var i = _glyphInfos.Count - 1; i >= 0; i--)
glyphCount++;
}
}
else
{ {
var characterIndex = charactersSpan.Length - 1; var currentCluster = _glyphInfos[i].GlyphCluster;
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
for (var i = GlyphClusters.Count - 1; i >= 0; i--) characterIndex -= characterLength;
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex -= characterLength; if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace) var clusterLength = 1;
{
break;
}
var clusterLength = 1; while (i - 1 >= 0)
{
var nextCluster = _glyphInfos[i - 1].GlyphCluster;
while (i - 1 >= 0) if (currentCluster == nextCluster)
{ {
var nextCluster = GlyphClusters[i - 1]; clusterLength++;
i--;
if (currentCluster == nextCluster) if(characterIndex >= 0)
{ {
clusterLength++; codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
characterIndex -= characterLength; characterIndex -= characterLength;
}
continue;
} }
break; continue;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
} }
trailingWhitespaceLength += clusterLength; break;
}
glyphCount++; if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
} }
trailingWhitespaceLength += clusterLength;
glyphCount++;
} }
} }
return trailingWhitespaceLength; return trailingWhitespaceLength;
} }
private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
{ {
glyphCount = 0; glyphCount = 0;
newLineLength = 0; newLineLength = 0;
@ -806,71 +752,46 @@ namespace Avalonia.Media
if (!charactersSpan.IsEmpty) if (!charactersSpan.IsEmpty)
{ {
if (GlyphClusters == null) var characterIndex = 0;
{
for (var i = 0; i < charactersSpan.Length;)
{
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace) for (var i = 0; i < _glyphInfos.Count; i++)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i += count;
glyphCount++;
}
}
else
{ {
var characterIndex = 0; var currentCluster = _glyphInfos[i].GlyphCluster;
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
for (var i = 0; i < GlyphClusters.Count; i++) characterIndex += characterLength;
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex += characterLength; if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace) var clusterLength = 1;
{
break;
}
var clusterLength = 1; var j = i;
var j = i; while (j - 1 >= 0)
{
var nextCluster = _glyphInfos[--j].GlyphCluster;
while (j - 1 >= 0) if (currentCluster == nextCluster)
{ {
var nextCluster = GlyphClusters[--j]; clusterLength++;
if (currentCluster == nextCluster)
{
clusterLength++;
continue; continue;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
} }
trailingWhitespaceLength += clusterLength; break;
}
glyphCount += clusterLength; if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
} }
trailingWhitespaceLength += clusterLength;
glyphCount += clusterLength;
} }
} }
@ -890,44 +811,17 @@ namespace Avalonia.Media
field = value; field = value;
} }
/// <summary> private IGlyphRunImpl CreateGlyphRunImpl()
/// Initializes the <see cref="GlyphRun"/>.
/// </summary>
private void Initialize()
{ {
if (GlyphIndices == null)
{
throw new InvalidOperationException();
}
var glyphCount = GlyphIndices.Count;
if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount)
{
throw new InvalidOperationException();
}
if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount)
{
throw new InvalidOperationException();
}
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos);
} }
public void Dispose() public void Dispose()
{ {
_glyphRunImpl?.Dispose(); _glyphRunImpl?.Dispose();
} _glyphRunImpl = null;
private class ReverseComparer<T> : IComparer<T>
{
public int Compare(T? x, T? y)
{
return Comparer<T>.Default.Compare(y, x);
}
} }
} }
} }

268
src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs

@ -0,0 +1,268 @@
using System;
using System.Diagnostics;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Reorders text runs according to their bidi level.
/// </summary>
/// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
internal sealed class BidiReorderer
{
[ThreadStatic] private static BidiReorderer? t_instance;
private ArrayBuilder<OrderedBidiRun> _runs;
private ArrayBuilder<BidiRange> _ranges;
public static BidiReorderer Instance
=> t_instance ??= new();
public void BidiReorder(Span<TextRun> textRuns, FlowDirection flowDirection)
{
Debug.Assert(_runs.Length == 0);
Debug.Assert(_ranges.Length == 0);
if (textRuns.IsEmpty)
{
return;
}
try
{
_runs.Add(textRuns.Length);
// Build up the collection of ordered runs.
for (var i = 0; i < textRuns.Length; i++)
{
var textRun = textRuns[i];
_runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection));
if (i > 0)
{
_runs[i - 1].NextRunIndex = i;
}
}
// Reorder them into visual order.
var firstIndex = LinearReorder();
// Now perform a recursive reversal of each run.
// From the highest level found in the text to the lowest odd level on each line, including intermediate levels
// not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
// https://unicode.org/reports/tr9/#L2
sbyte max = 0;
var min = sbyte.MaxValue;
for (var i = 0; i < textRuns.Length; i++)
{
var level = GetRunBidiLevel(textRuns[i], flowDirection);
if (level > max)
{
max = level;
}
if ((level & 1) != 0 && level < min)
{
min = level;
}
}
if (min > max)
{
min = max;
}
if (max == 0 || (min == max && (max & 1) == 0))
{
// Nothing to reverse.
return;
}
// Now apply the reversal and replace the original contents.
var minLevelToReverse = max;
int currentIndex;
while (minLevelToReverse >= min)
{
currentIndex = firstIndex;
while (currentIndex >= 0)
{
ref var current = ref _runs[currentIndex];
if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{
if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
{
shapedTextCharacters.Reverse();
}
}
currentIndex = current.NextRunIndex;
}
minLevelToReverse--;
}
var index = 0;
currentIndex = firstIndex;
while (currentIndex >= 0)
{
ref var current = ref _runs[currentIndex];
textRuns[index++] = current.Run;
currentIndex = current.NextRunIndex;
}
}
finally
{
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges);
}
}
private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
{
if (run is ShapedTextRun shapedTextRun)
{
return shapedTextRun.BidiLevel;
}
var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
return (sbyte)defaultLevel;
}
/// <summary>
/// Reorders the runs from logical to visual order.
/// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
/// </summary>
/// <returns>The first run index in visual order.</returns>
private int LinearReorder()
{
var runIndex = 0;
var rangeIndex = -1;
while (runIndex >= 0)
{
ref var run = ref _runs[runIndex];
var nextRunIndex = run.NextRunIndex;
while (rangeIndex >= 0
&& _ranges[rangeIndex].Level > run.Level
&& _ranges[rangeIndex].PreviousRangeIndex >= 0
&& _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level)
{
rangeIndex = MergeRangeWithPrevious(rangeIndex);
}
if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level)
{
// Attach run to the range.
if ((run.Level & 1) != 0)
{
// Odd, range goes to the right of run.
run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex;
_ranges[rangeIndex].LeftRunIndex = runIndex;
}
else
{
// Even, range goes to the left of run.
_runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex;
_ranges[rangeIndex].RightRunIndex = runIndex;
}
_ranges[rangeIndex].Level = run.Level;
}
else
{
var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex);
_ranges.AddItem(r);
rangeIndex = _ranges.Length - 1;
}
runIndex = nextRunIndex;
}
while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0)
{
rangeIndex = MergeRangeWithPrevious(rangeIndex);
}
// Terminate.
_runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1;
return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex;
}
private int MergeRangeWithPrevious(int index)
{
var previousIndex = _ranges[index].PreviousRangeIndex;
ref var previous = ref _ranges[previousIndex];
int leftIndex;
int rightIndex;
if ((previous.Level & 1) != 0)
{
// Odd, previous goes to the right of range.
leftIndex = index;
rightIndex = previousIndex;
}
else
{
// Even, previous goes to the left of range.
leftIndex = previousIndex;
rightIndex = index;
}
// Stitch them
ref var left = ref _ranges[leftIndex];
ref var right = ref _ranges[rightIndex];
_runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex;
previous.LeftRunIndex = left.LeftRunIndex;
previous.RightRunIndex = right.RightRunIndex;
return previousIndex;
}
private struct OrderedBidiRun
{
public OrderedBidiRun(int runIndex, TextRun run, sbyte level)
{
RunIndex = runIndex;
Run = run;
Level = level;
NextRunIndex = -1;
}
public int RunIndex { get; }
public sbyte Level { get; }
public TextRun Run { get; }
public int NextRunIndex { get; set; } // -1 if none
}
private struct BidiRange
{
public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex)
{
Level = level;
LeftRunIndex = leftRunIndex;
RightRunIndex = rightRunIndex;
PreviousRangeIndex = previousRangeIndex;
}
public sbyte Level { get; set; }
public int LeftRunIndex { get; set; }
public int RightRunIndex { get; set; }
public int PreviousRangeIndex { get; } // -1 if none
}
}
}

84
src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs

@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal static class FormattingBufferHelper
{
// 1MB, arbitrary, that's 512K characters or 128K object references on x64
private const long MaxKeptBufferSizeInBytes = 1024 * 1024;
public static void ClearThenResetIfTooLarge<T>(ref ArrayBuilder<T> arrayBuilder)
{
arrayBuilder.Clear();
if (IsBufferTooLarge<T>((uint) arrayBuilder.Capacity))
{
arrayBuilder = default;
}
}
public static void ClearThenResetIfTooLarge<T>(List<T> list)
{
list.Clear();
if (IsBufferTooLarge<T>((uint) list.Capacity))
{
list.TrimExcess();
}
}
public static void ClearThenResetIfTooLarge<T>(Stack<T> stack)
{
var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count);
stack.Clear();
if (IsBufferTooLarge<T>(approximateCapacity))
{
stack.TrimExcess();
}
}
public static void ClearThenResetIfTooLarge<TKey, TValue>(ref Dictionary<TKey, TValue> dictionary)
where TKey : notnull
{
var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count);
dictionary.Clear();
// dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here
if (IsBufferTooLarge<KeyValuePair<TKey, TValue>>(approximateCapacity))
{
#if NET6_0_OR_GREATER
dictionary.TrimExcess();
#else
dictionary = new Dictionary<TKey, TValue>();
#endif
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsBufferTooLarge<T>(uint capacity)
=> (long) (uint) Unsafe.SizeOf<T>() * capacity > MaxKeptBufferSizeInBytes;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint RoundUpToPowerOf2(uint value)
{
#if NET6_0_OR_GREATER
return BitOperations.RoundUpToPowerOf2(value);
#else
// Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
--value;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return value + 1;
#endif
}
}
}

135
src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs

@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// <para>Contains various list pools that are commonly used during text layout.</para>
/// <para>
/// This class provides an instance per thread.
/// In most applications, there'll be only one instance: on the UI thread, which is responsible for layout.
/// </para>
/// </summary>
/// <seealso cref="RentedList{T}"/>
internal sealed class FormattingObjectPool
{
[ThreadStatic] private static FormattingObjectPool? t_instance;
/// <summary>
/// Gets an instance of this class for the current thread.
/// </summary>
/// <remarks>
/// Since this is backed by a thread static field which is slower than a normal static field,
/// prefer passing the instance around when possible instead of calling this property each time.
/// </remarks>
public static FormattingObjectPool Instance
=> t_instance ??= new();
public ListPool<TextRun> TextRunLists { get; } = new();
public ListPool<UnshapedTextRun> UnshapedTextRunLists { get; } = new();
public ListPool<TextLine> TextLines { get; } = new();
[Conditional("DEBUG")]
public void VerifyAllReturned()
{
TextRunLists.VerifyAllReturned();
UnshapedTextRunLists.VerifyAllReturned();
TextLines.VerifyAllReturned();
}
internal sealed class ListPool<T>
{
// we don't need a big number here, these are for temporary usages only which should quickly be returned
private const int MaxSize = 16;
private readonly RentedList<T>[] _lists = new RentedList<T>[MaxSize];
private int _size;
private int _pendingReturnCount;
/// <summary>
/// Rents a list.
/// See <see cref="RentedList{T}"/> for the intended usages.
/// </summary>
/// <returns>A rented list instance that must be returned to the pool.</returns>
/// <seealso cref="RentedList{T}"/>
public RentedList<T> Rent()
{
var list = _size > 0 ? _lists[--_size] : new RentedList<T>();
Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!");
++_pendingReturnCount;
return list;
}
/// <summary>
/// Returns a rented list to the pool.
/// </summary>
/// <param name="rentedList">
/// On input, the list to return.
/// On output, the reference is set to null to avoid misuse.
/// </param>
public void Return(ref RentedList<T>? rentedList)
{
if (rentedList is null)
{
return;
}
--_pendingReturnCount;
FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList);
if (_size < MaxSize)
{
_lists[_size++] = rentedList;
}
rentedList = null;
}
[Conditional("DEBUG")]
public void VerifyAllReturned()
{
if (_pendingReturnCount > 0)
{
throw new InvalidOperationException(
$"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!");
}
if (_pendingReturnCount < 0)
{
throw new InvalidOperationException(
$"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!");
}
}
}
/// <summary>
/// <para>Represents a list that has been rented through <see cref="FormattingObjectPool"/>.</para>
/// <para>
/// This class can be used when a temporary list is needed to store some items during text layout.
/// It can also be used as a reusable array builder by calling <see cref="List{T}.ToArray"/> when done.
/// </para>
/// <list type="bullet">
/// <item>NEVER use an instance of this type after it's been returned to the pool.</item>
/// <item>AVOID storing an instance of this type into a field or property.</item>
/// <item>AVOID casting an instance of this type to another type.</item>
/// <item>
/// AVOID passing an instance of this type as an argument to a method expecting a standard list,
/// unless you're absolutely sure it won't store it.
/// </item>
/// <item>
/// If you call a method returning an instance of this type,
/// you're now responsible for returning it to the pool.
/// </item>
/// </list>
/// </summary>
/// <typeparam name="T">The type of elements in the list.</typeparam>
internal sealed class RentedList<T> : List<T>
{
}
}
}

36
src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs

@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Represents a single glyph.
/// </summary>
public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default)
{
internal static Comparer<GlyphInfo> ClusterAscendingComparer { get; } =
Comparer<GlyphInfo>.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster));
internal static Comparer<GlyphInfo> ClusterDescendingComparer { get; } =
Comparer<GlyphInfo>.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster));
/// <summary>
/// Get the glyph index.
/// </summary>
public ushort GlyphIndex { get; } = GlyphIndex;
/// <summary>
/// Get the glyph cluster.
/// </summary>
public int GlyphCluster { get; } = GlyphCluster;
/// <summary>
/// Get the glyph advance.
/// </summary>
public double GlyphAdvance { get; } = GlyphAdvance;
/// <summary>
/// Get the glyph offset.
/// </summary>
public Vector GlyphOffset { get; } = GlyphOffset;
}
}

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

@ -48,8 +48,9 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = textLine.FirstTextSourceIndex; var currentPosition = textLine.FirstTextSourceIndex;
foreach (var textRun in lineImpl.TextRuns) for (var i = 0; i < lineImpl.TextRuns.Count; ++i)
{ {
var textRun = lineImpl.TextRuns[i];
var text = textRun.Text; var text = textRun.Text;
if (text.IsEmpty) if (text.IsEmpty)
@ -111,7 +112,7 @@ namespace Avalonia.Media.TextFormatting
shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing);
} }
glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; glyphRun.GlyphInfos = shapedBuffer.GlyphInfos;
} }
currentPosition += textRun.Length; currentPosition += textRun.Length;

230
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -1,24 +1,23 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
public sealed class ShapedBuffer : IList<GlyphInfo>, IDisposable public sealed class ShapedBuffer : IReadOnlyList<GlyphInfo>, IDisposable
{ {
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters(); private GlyphInfo[]? _rentedBuffer;
private bool _bufferRented;
public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(text,
new ArraySlice<GlyphInfo>(ArrayPool<GlyphInfo>.Shared.Rent(bufferLength), 0, bufferLength),
glyphTypeface,
fontRenderingEmSize,
bidiLevel)
{ {
_bufferRented = true; _rentedBuffer = ArrayPool<GlyphInfo>.Shared.Rent(bufferLength);
Length = bufferLength; Text = text;
GlyphInfos = new ArraySlice<GlyphInfo>(_rentedBuffer, 0, bufferLength);
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
} }
internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
@ -28,12 +27,12 @@ namespace Avalonia.Media.TextFormatting
GlyphTypeface = glyphTypeface; GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize; FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel; BidiLevel = bidiLevel;
Length = GlyphInfos.Length;
} }
internal ArraySlice<GlyphInfo> GlyphInfos { get; } internal ArraySlice<GlyphInfo> GlyphInfos { get; private set; }
public int Length { get; } public int Length
=> GlyphInfos.Length;
public IGlyphTypeface GlyphTypeface { get; } public IGlyphTypeface GlyphTypeface { get; }
@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting
public sbyte BidiLevel { get; } public sbyte BidiLevel { get; }
public bool IsLeftToRight => (BidiLevel & 1) == 0; public bool IsLeftToRight => (BidiLevel & 1) == 0;
public IReadOnlyList<ushort> GlyphIndices => new GlyphIndexList(GlyphInfos);
public IReadOnlyList<int> GlyphClusters => new GlyphClusterList(GlyphInfos);
public IReadOnlyList<double> GlyphAdvances => new GlyphAdvanceList(GlyphInfos);
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public ReadOnlyMemory<char> Text { get; } public ReadOnlyMemory<char> Text { get; }
@ -73,13 +64,13 @@ namespace Avalonia.Media.TextFormatting
} }
var comparer = s_clusterComparer; var comparer = GlyphInfo.ClusterAscendingComparer;
var clusters = GlyphInfos.Span; var glyphInfos = GlyphInfos.Span;
var searchValue = new GlyphInfo(0, characterIndex); var searchValue = new GlyphInfo(default, characterIndex, default);
var start = clusters.BinarySearch(searchValue, comparer); var start = glyphInfos.BinarySearch(searchValue, comparer);
if (start < 0) if (start < 0)
{ {
@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting
{ {
characterIndex--; characterIndex--;
searchValue = new GlyphInfo(0, characterIndex); searchValue = new GlyphInfo(default, characterIndex, default);
start = clusters.BinarySearch(searchValue, comparer); start = glyphInfos.BinarySearch(searchValue, comparer);
} }
if (start < 0) if (start < 0)
@ -98,7 +89,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster) while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster)
{ {
start--; start--;
} }
@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult<ShapedBuffer>(this, null); return new SplitResult<ShapedBuffer>(this, null);
} }
var firstCluster = GlyphClusters[0]; var firstCluster = GlyphInfos[0].GlyphCluster;
var lastCluster = GlyphClusters[GlyphClusters.Count - 1]; var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster;
var start = firstCluster < lastCluster ? firstCluster : lastCluster; var start = firstCluster < lastCluster ? firstCluster : lastCluster;
@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult<ShapedBuffer>(first, second); return new SplitResult<ShapedBuffer>(first, second);
} }
int ICollection<GlyphInfo>.Count => throw new NotImplementedException(); int IReadOnlyCollection<GlyphInfo>.Count => GlyphInfos.Length;
bool ICollection<GlyphInfo>.IsReadOnly => true;
public GlyphInfo this[int index] public GlyphInfo this[int index]
{ {
@ -144,177 +133,18 @@ namespace Avalonia.Media.TextFormatting
set => GlyphInfos[index] = value; set => GlyphInfos[index] = value;
} }
int IList<GlyphInfo>.IndexOf(GlyphInfo item)
{
throw new NotImplementedException();
}
void IList<GlyphInfo>.Insert(int index, GlyphInfo item)
{
throw new NotImplementedException();
}
void IList<GlyphInfo>.RemoveAt(int index)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.Add(GlyphInfo item)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.Clear()
{
throw new NotImplementedException();
}
bool ICollection<GlyphInfo>.Contains(GlyphInfo item)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.CopyTo(GlyphInfo[] array, int arrayIndex)
{
throw new NotImplementedException();
}
bool ICollection<GlyphInfo>.Remove(GlyphInfo item)
{
throw new NotImplementedException();
}
public IEnumerator<GlyphInfo> GetEnumerator() => GlyphInfos.GetEnumerator(); public IEnumerator<GlyphInfo> GetEnumerator() => GlyphInfos.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private class CompareClusters : IComparer<GlyphInfo>
{
private static readonly Comparer<int> s_intClusterComparer = Comparer<int>.Default;
public int Compare(GlyphInfo x, GlyphInfo y)
{
return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster);
}
}
private readonly struct GlyphAdvanceList : IReadOnlyList<double>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphAdvanceList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public double this[int index] => _glyphInfos[index].GlyphAdvance;
public int Count => _glyphInfos.Length;
public IEnumerator<double> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<double>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphIndexList : IReadOnlyList<ushort>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphIndexList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public ushort this[int index] => _glyphInfos[index].GlyphIndex;
public int Count => _glyphInfos.Length;
public IEnumerator<ushort> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<ushort>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphClusterList : IReadOnlyList<int>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphClusterList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public int this[int index] => _glyphInfos[index].GlyphCluster;
public int Count => _glyphInfos.Length;
public IEnumerator<int> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<int>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphOffsetList : IReadOnlyList<Vector>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphOffsetList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public Vector this[int index] => _glyphInfos[index].GlyphOffset;
public int Count => _glyphInfos.Length;
public IEnumerator<Vector> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<Vector>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
public void Dispose() public void Dispose()
{ {
GC.SuppressFinalize(this); if (_rentedBuffer is not null)
if (_bufferRented)
{ {
GlyphInfos.ReturnRent(); ArrayPool<GlyphInfo>.Shared.Return(_rentedBuffer);
_rentedBuffer = null;
GlyphInfos = ArraySlice<GlyphInfo>.Empty; // ensure we don't misuse the returned array
} }
} }
~ShapedBuffer()
{
if (_bufferRented)
{
GlyphInfos.ReturnRent();
}
}
}
public readonly record struct GlyphInfo
{
public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default)
{
GlyphIndex = glyphIndex;
GlyphAdvance = glyphAdvance;
GlyphCluster = glyphCluster;
GlyphOffset = glyphOffset;
}
/// <summary>
/// Get the glyph index.
/// </summary>
public ushort GlyphIndex { get; }
/// <summary>
/// Get the glyph cluster.
/// </summary>
public int GlyphCluster { get; }
/// <summary>
/// Get the glyph advance.
/// </summary>
public double GlyphAdvance { get; }
/// <summary>
/// Get the glyph offset.
/// </summary>
public Vector GlyphOffset { get; }
} }
} }

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

@ -40,25 +40,14 @@ namespace Avalonia.Media.TextFormatting
public override Size Size => GlyphRun.Size; public override Size Size => GlyphRun.Size;
public GlyphRun GlyphRun public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun();
{
get
{
if(_glyphRun is null)
{
_glyphRun = CreateGlyphRun();
}
return _glyphRun;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin) public override void Draw(DrawingContext drawingContext, Point origin)
{ {
using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin)))
{ {
if (GlyphRun.GlyphIndices.Count == 0) if (GlyphRun.GlyphInfos.Count == 0)
{ {
return; return;
} }
@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < ShapedBuffer.Length; i++) for (var i = 0; i < ShapedBuffer.Length; i++)
{ {
var advance = ShapedBuffer.GlyphAdvances[i]; var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
if (currentWidth + advance > availableWidth) if (currentWidth + advance > availableWidth)
{ {
@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting
for (var i = ShapedBuffer.Length - 1; i >= 0; i--) for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{ {
var advance = ShapedBuffer.GlyphAdvances[i]; var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
if (width + advance > availableWidth) if (width + advance > availableWidth)
{ {
@ -195,10 +184,7 @@ namespace Avalonia.Media.TextFormatting
ShapedBuffer.GlyphTypeface, ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize, ShapedBuffer.FontRenderingEmSize,
Text, Text,
ShapedBuffer.GlyphIndices, ShapedBuffer,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,
ShapedBuffer.GlyphClusters,
BidiLevel); BidiLevel);
} }

11
src/Avalonia.Base/Media/TextFormatting/SplitResult.cs

@ -26,5 +26,16 @@
/// The second part. /// The second part.
/// </value> /// </value>
public T? Second { get; } public T? Second { get; }
/// <summary>
/// Deconstructs the split results into its components.
/// </summary>
/// <param name="first">On return, contains the first part.</param>
/// <param name="second">On return, contains the second part.</param>
public void Deconstruct(out T first, out T? second)
{
first = First;
second = Second;
}
} }
} }

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

@ -1,6 +1,6 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using static Avalonia.Media.TextFormatting.FormattingObjectPool;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="UnshapedTextRun"/>. /// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary> /// </summary>
/// <returns>The shapeable text characters.</returns> /// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel, internal void GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
ref TextRunProperties? previousProperties) ref TextRunProperties? previousProperties, RentedList<TextRun> results)
{ {
var shapeableCharacters = new List<UnshapedTextRun>(2);
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, ref previousProperties);
shapeableCharacters.Add(shapeableRun); results.Add(shapeableRun);
text = text.Slice(shapeableRun.Length); text = text.Slice(shapeableRun.Length);
previousProperties = shapeableRun.Properties; previousProperties = shapeableRun.Properties;
} }
return shapeableCharacters;
} }
/// <summary> /// <summary>

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

@ -1,6 +1,4 @@
using System.Collections.Generic; namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{ {
/// <summary> /// <summary>
/// Properties of text collapsing. /// Properties of text collapsing.
@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line. /// Collapses given text line.
/// </summary> /// </summary>
/// <param name="textLine">Text line to collapse.</param> /// <param name="textLine">Text line to collapse.</param>
public abstract List<TextRun>? Collapse(TextLine textLine); public abstract TextRun[]? Collapse(TextLine textLine);
} }
} }

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

@ -1,15 +1,16 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
internal static class TextEllipsisHelper internal static class TextEllipsisHelper
{ {
public static List<TextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{ {
var textRuns = textLine.TextRuns; var textRuns = textLine.TextRuns;
if (textRuns == null || textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return null; return null;
} }
@ -22,7 +23,7 @@ namespace Avalonia.Media.TextFormatting
if (properties.Width < shapedSymbol.GlyphRun.Size.Width) if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{ {
//Not enough space to fit in the symbol //Not enough space to fit in the symbol
return new List<TextRun>(0); return Array.Empty<TextRun>();
} }
var availableWidth = properties.Width - shapedSymbol.Size.Width; var availableWidth = properties.Width - shapedSymbol.Size.Width;
@ -70,18 +71,7 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength; collapsedLength += measuredLength;
var collapsedRuns = new List<TextRun>(textRuns.Count); return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
} }
availableWidth -= shapedRun.Size.Width; availableWidth -= shapedRun.Size.Width;
@ -94,18 +84,7 @@ namespace Avalonia.Media.TextFormatting
//The whole run needs to fit into available space //The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth) if (currentWidth + drawableRun.Size.Width > availableWidth)
{ {
var collapsedRuns = new List<TextRun>(textRuns.Count); return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
} }
availableWidth -= drawableRun.Size.Width; availableWidth -= drawableRun.Size.Width;
@ -121,5 +100,27 @@ namespace Avalonia.Media.TextFormatting
return null; return null;
} }
private static TextRun[] CreateCollapsedRuns(IReadOnlyList<TextRun> textRuns, int collapsedLength,
TextRun shapedSymbol)
{
if (collapsedLength <= 0)
{
return new[] { shapedSymbol };
}
var objectPool = FormattingObjectPool.Instance;
var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
return collapsedRuns;
}
} }
} }

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

@ -1,15 +1,22 @@
using System; // ReSharper disable ForCanBeConvertedToForeach
using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
using static Avalonia.Media.TextFormatting.FormattingObjectPool;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
internal class TextFormatterImpl : TextFormatter internal sealed class TextFormatterImpl : TextFormatter
{ {
private static readonly char[] s_empty = { ' ' }; private static readonly char[] s_empty = { ' ' };
private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength];
[ThreadStatic] private static BidiData? t_bidiData;
[ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm;
/// <inheritdoc cref="TextFormatter.FormatLine"/> /// <inheritdoc cref="TextFormatter.FormatLine"/>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
@ -18,20 +25,25 @@ namespace Avalonia.Media.TextFormatting
var textWrapping = paragraphProperties.TextWrapping; var textWrapping = paragraphProperties.TextWrapping;
FlowDirection resolvedFlowDirection; FlowDirection resolvedFlowDirection;
TextLineBreak? nextLineBreak = null; TextLineBreak? nextLineBreak = null;
IReadOnlyList<TextRun> textRuns; IReadOnlyList<TextRun>? textRuns;
var objectPool = FormattingObjectPool.Instance;
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
out var textEndOfLine, out var textSourceLength); out var textEndOfLine, out var textSourceLength);
if (previousLineBreak?.RemainingRuns != null) RentedList<TextRun>? shapedTextRuns;
if (previousLineBreak?.RemainingRuns is { } remainingRuns)
{ {
resolvedFlowDirection = previousLineBreak.FlowDirection; resolvedFlowDirection = previousLineBreak.FlowDirection;
textRuns = previousLineBreak.RemainingRuns; textRuns = remainingRuns;
nextLineBreak = previousLineBreak; nextLineBreak = previousLineBreak;
shapedTextRuns = null;
} }
else else
{ {
textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection);
textRuns = shapedTextRuns;
if (nextLineBreak == null && textEndOfLine != null) if (nextLineBreak == null && textEndOfLine != null)
{ {
@ -44,25 +56,32 @@ namespace Avalonia.Media.TextFormatting
switch (textWrapping) switch (textWrapping)
{ {
case TextWrapping.NoWrap: case TextWrapping.NoWrap:
{ {
textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); // which already uses an array: ToArray() won't ever be called in this case
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
textLine.FinalizeLine(); textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
break; textLine.FinalizeLine();
}
break;
}
case TextWrapping.WrapWithOverflow: case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap: case TextWrapping.Wrap:
{ {
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
resolvedFlowDirection, nextLineBreak); paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
break; break;
} }
default: default:
throw new ArgumentOutOfRangeException(nameof(textWrapping)); throw new ArgumentOutOfRangeException(nameof(textWrapping));
} }
objectPool.TextRunLists.Return(ref shapedTextRuns);
objectPool.TextRunLists.Return(ref fetchedRuns);
return textLine; return textLine;
} }
@ -71,9 +90,12 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <param name="textRuns">The text run's.</param> /// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param> /// <param name="length">The length to split at.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <returns>The split text runs.</returns> /// <returns>The split text runs.</returns>
internal static SplitResult<IReadOnlyList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length) internal static SplitResult<RentedList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length,
FormattingObjectPool objectPool)
{ {
var first = objectPool.TextRunLists.Rent();
var currentLength = 0; var currentLength = 0;
for (var i = 0; i < textRuns.Count; i++) for (var i = 0; i < textRuns.Count; i++)
@ -89,8 +111,6 @@ namespace Avalonia.Media.TextFormatting
var firstCount = currentRun.Length >= 1 ? i + 1 : i; var firstCount = currentRun.Length >= 1 ? i + 1 : i;
var first = new List<TextRun>(firstCount);
if (firstCount > 1) if (firstCount > 1)
{ {
for (var j = 0; j < i; j++) for (var j = 0; j < i; j++)
@ -103,7 +123,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Length == length) if (currentLength + currentRun.Length == length)
{ {
var second = secondCount > 0 ? new List<TextRun>(secondCount) : null; var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null;
if (second != null) if (second != null)
{ {
@ -117,13 +137,13 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun); first.Add(currentRun);
return new SplitResult<IReadOnlyList<TextRun>>(first, second); return new SplitResult<RentedList<TextRun>>(first, second);
} }
else else
{ {
secondCount++; secondCount++;
var second = new List<TextRun>(secondCount); var second = objectPool.TextRunLists.Rent();
if (currentRun is ShapedTextRun shapedTextCharacters) if (currentRun is ShapedTextRun shapedTextCharacters)
{ {
@ -139,11 +159,16 @@ namespace Avalonia.Media.TextFormatting
second.Add(textRuns[i + j]); second.Add(textRuns[i + j]);
} }
return new SplitResult<IReadOnlyList<TextRun>>(first, second); return new SplitResult<RentedList<TextRun>>(first, second);
} }
} }
return new SplitResult<IReadOnlyList<TextRun>>(textRuns, null); for (var i = 0; i < textRuns.Count; i++)
{
first.Add(textRuns[i]);
}
return new SplitResult<RentedList<TextRun>>(first, null);
} }
/// <summary> /// <summary>
@ -152,40 +177,58 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRuns">The text runs to shape.</param> /// <param name="textRuns">The text runs to shape.</param>
/// <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>
/// <returns> /// <returns>
/// A list of shaped text characters. /// A list of shaped text characters.
/// </returns> /// </returns>
private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties, private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns,
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool,
out FlowDirection resolvedFlowDirection) out FlowDirection resolvedFlowDirection)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var shapedRuns = new List<TextRun>(); var shapedRuns = objectPool.TextRunLists.Rent();
using var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns) if (textRuns.Count == 0)
{ {
if (textRun.Text.IsEmpty) resolvedFlowDirection = flowDirection;
{ return shapedRuns;
biDiData.Append(new char[textRun.Length]);
}
else
{
biDiData.Append(textRun.Text.Span);
}
} }
using var biDi = new BidiAlgorithm(); var bidiData = t_bidiData ??= new();
bidiData.Reset();
bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
for (var i = 0; i < textRuns.Count; ++i)
{
var textRun = textRuns[i];
ReadOnlySpan<char> text;
if (!textRun.Text.IsEmpty)
text = textRun.Text.Span;
else if (textRun.Length == TextRun.DefaultTextSourceLength)
text = s_defaultText;
else
text = new char[textRun.Length];
bidiData.Append(text);
}
biDi.Process(biDiData); var bidiAlgorithm = t_bidiAlgorithm ??= new();
bidiAlgorithm.Process(bidiData);
var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes); var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes);
resolvedFlowDirection = resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
var processedRuns = new List<TextRun>(textRuns.Count); var processedRuns = objectPool.TextRunLists.Rent();
CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns);
CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns); bidiData.Reset();
bidiAlgorithm.Reset();
var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
for (var index = 0; index < processedRuns.Count; index++) for (var index = 0; index < processedRuns.Count; index++)
{ {
@ -194,8 +237,9 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case UnshapedTextRun shapeableRun: case UnshapedTextRun shapeableRun:
{ {
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun }; groupedRuns.Clear();
groupedRuns.Add(shapeableRun);
var text = shapeableRun.Text; var text = shapeableRun.Text;
while (index + 1 < processedRuns.Count) while (index + 1 < processedRuns.Count)
@ -224,7 +268,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns);
break; break;
} }
@ -237,6 +281,9 @@ namespace Avalonia.Media.TextFormatting
} }
} }
objectPool.TextRunLists.Return(ref processedRuns);
objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
return shapedRuns; return shapedRuns;
} }
@ -303,17 +350,14 @@ namespace Avalonia.Media.TextFormatting
} }
} }
private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y) private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y)
=> MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize) => MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize)
&& x.Typeface == y.Typeface && x.Typeface == y.Typeface
&& x.BaselineAlignment == y.BaselineAlignment; && x.BaselineAlignment == y.BaselineAlignment;
private static IReadOnlyList<ShapedTextRun> ShapeTogether( private static void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text,
IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, TextShaperOptions options) TextShaperOptions options, RentedList<TextRun> results)
{ {
var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, options); var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++) for (var i = 0; i < textRuns.Count; i++)
@ -322,12 +366,10 @@ namespace Avalonia.Media.TextFormatting
var splitResult = shapedBuffer.Split(currentRun.Length); var splitResult = shapedBuffer.Split(currentRun.Length);
shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
shapedBuffer = splitResult.Second!; shapedBuffer = splitResult.Second!;
} }
return shapedRuns;
} }
/// <summary> /// <summary>
@ -335,10 +377,10 @@ 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="processedRuns"></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, ArraySlice<sbyte> levels, private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels,
List<TextRun> processedRuns) RentedList<TextRun> processedRuns)
{ {
if (levels.Length == 0) if (levels.Length == 0)
{ {
@ -385,7 +427,8 @@ namespace Avalonia.Media.TextFormatting
if (j == runTextSpan.Length) if (j == runTextSpan.Length)
{ {
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties,
processedRuns);
runLevel = levels[levelIndex]; runLevel = levels[levelIndex];
@ -398,7 +441,8 @@ namespace Avalonia.Media.TextFormatting
} }
// End of this run // End of this run
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties,
processedRuns);
runText = runText.Slice(j); runText = runText.Slice(j);
runTextSpan = runText.Span; runTextSpan = runText.Span;
@ -415,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
return; return;
} }
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns);
} }
/// <summary> /// <summary>
@ -423,19 +467,20 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <param name="textSource">The text source.</param> /// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param> /// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="endOfLine"></param> /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="textSourceLength"></param> /// <param name="endOfLine">On return, the end of line, if any.</param>
/// <param name="textSourceLength">On return, the processed text source length.</param>
/// <returns> /// <returns>
/// The formatted text runs. /// The formatted text runs.
/// </returns> /// </returns>
private static List<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, private static RentedList<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
out TextEndOfLine? endOfLine, out int textSourceLength) FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength)
{ {
textSourceLength = 0; textSourceLength = 0;
endOfLine = null; endOfLine = null;
var textRuns = new List<TextRun>(); var textRuns = objectPool.TextRunLists.Rent();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
@ -529,8 +574,10 @@ namespace Avalonia.Media.TextFormatting
measuredLength = 0; measuredLength = 0;
var currentWidth = 0.0; var currentWidth = 0.0;
foreach (var currentRun in textRuns) for (var i = 0; i < textRuns.Count; ++i)
{ {
var currentRun = textRuns[i];
switch (currentRun) switch (currentRun)
{ {
case ShapedTextRun shapedTextCharacters: case ShapedTextRun shapedTextCharacters:
@ -540,15 +587,15 @@ namespace Avalonia.Media.TextFormatting
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var lastCluster = firstCluster; var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++)
{ {
var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; var glyphInfo = shapedTextCharacters.ShapedBuffer[j];
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{ {
measuredLength += Math.Max(0, lastCluster - firstCluster); measuredLength += Math.Max(0, lastCluster - firstCluster);
goto found; return measuredLength != 0;
} }
lastCluster = glyphInfo.GlyphCluster; lastCluster = glyphInfo.GlyphCluster;
@ -565,7 +612,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{ {
goto found; return measuredLength != 0;
} }
measuredLength += currentRun.Length; measuredLength += currentRun.Length;
@ -582,8 +629,6 @@ namespace Avalonia.Media.TextFormatting
} }
} }
found:
return measuredLength != 0; return measuredLength != 0;
} }
@ -591,20 +636,23 @@ namespace Avalonia.Media.TextFormatting
/// Creates an empty text line. /// Creates an empty text line.
/// </summary> /// </summary>
/// <returns>The empty text line.</returns> /// <returns>The empty text line.</returns>
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties; var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface; var glyphTypeface = properties.Typeface.GlyphTypeface;
var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) };
var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection); (sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) }; var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection);
line.FinalizeLine();
return line;
} }
/// <summary> /// <summary>
@ -616,14 +664,15 @@ namespace Avalonia.Media.TextFormatting
/// <param name="paragraphProperties">The text paragraph properties.</param> /// <param name="paragraphProperties">The text paragraph properties.</param>
/// <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>
/// <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) TextLineBreak? currentLineBreak, FormattingObjectPool objectPool)
{ {
if (textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool);
} }
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@ -749,12 +798,10 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
var splitResult = SplitTextRuns(textRuns, measuredLength); var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
var remainingCharacters = splitResult.Second;
var lineBreak = remainingCharacters?.Count > 0 ? var lineBreak = postSplitRuns?.Count > 0 ?
new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
null; null;
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
@ -762,11 +809,16 @@ namespace Avalonia.Media.TextFormatting
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
} }
var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, paragraphWidth, paragraphProperties, resolvedFlowDirection,
lineBreak); lineBreak);
return textLine.FinalizeLine(); textLine.FinalizeLine();
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
return textLine;
} }
private struct TextRunEnumerator private struct TextRunEnumerator

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

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting
private readonly ITextSource _textSource; private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
private readonly TextTrimming _textTrimming; private readonly TextTrimming _textTrimming;
private readonly TextLine[] _textLines;
private int _textSourceLength; private int _textSourceLength;
@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
MaxLines = maxLines; MaxLines = maxLines;
TextLines = CreateTextLines(); _textLines = CreateTextLines();
} }
/// <summary> /// <summary>
@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting
MaxLines = maxLines; MaxLines = maxLines;
TextLines = CreateTextLines(); _textLines = CreateTextLines();
} }
/// <summary> /// <summary>
@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting
/// <value> /// <value>
/// The text lines. /// The text lines.
/// </value> /// </value>
public IReadOnlyList<TextLine> TextLines { get; private set; } public IReadOnlyList<TextLine> TextLines
=> _textLines;
/// <summary> /// <summary>
/// Gets the bounds of the layout. /// Gets the bounds of the layout.
@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting
/// <param name="origin">The origin.</param> /// <param name="origin">The origin.</param>
public void Draw(DrawingContext context, Point origin) public void Draw(DrawingContext context, Point origin)
{ {
if (!TextLines.Any()) if (_textLines.Length == 0)
{ {
return; return;
} }
var (currentX, currentY) = origin; var (currentX, currentY) = origin;
foreach (var textLine in TextLines) foreach (var textLine in _textLines)
{ {
textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); textLine.Draw(context, new Point(currentX + textLine.Start, currentY));
@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns></returns> /// <returns></returns>
public Rect HitTestTextPosition(int textPosition) public Rect HitTestTextPosition(int textPosition)
{ {
if (TextLines.Count == 0) if (_textLines.Length == 0)
{ {
return new Rect(); return new Rect();
} }
@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting
var currentY = 0.0; var currentY = 0.0;
foreach (var textLine in TextLines) foreach (var textLine in _textLines)
{ {
var end = textLine.FirstTextSourceIndex + textLine.Length; var end = textLine.FirstTextSourceIndex + textLine.Length;
@ -230,11 +231,11 @@ namespace Avalonia.Media.TextFormatting
return Array.Empty<Rect>(); return Array.Empty<Rect>();
} }
var result = new List<Rect>(TextLines.Count); var result = new List<Rect>(_textLines.Length);
var currentY = 0d; var currentY = 0d;
foreach (var textLine in TextLines) foreach (var textLine in _textLines)
{ {
//Current line isn't covered. //Current line isn't covered.
if (textLine.FirstTextSourceIndex + textLine.Length < start) if (textLine.FirstTextSourceIndex + textLine.Length < start)
@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting
{ {
var currentY = 0d; var currentY = 0d;
var lineIndex = 0;
TextLine? currentLine = null; TextLine? currentLine = null;
CharacterHit characterHit; CharacterHit characterHit;
for (; lineIndex < TextLines.Count; lineIndex++) for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++)
{ {
currentLine = TextLines[lineIndex]; currentLine = _textLines[lineIndex];
if (currentY + currentLine.Height > point.Y) if (currentY + currentLine.Height > point.Y)
{ {
@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting
if (charIndex > _textSourceLength) if (charIndex > _textSourceLength)
{ {
return TextLines.Count - 1; return _textLines.Length - 1;
} }
for (var index = 0; index < TextLines.Count; index++) for (var index = 0; index < _textLines.Length; index++)
{ {
var textLine = TextLines[index]; var textLine = _textLines[index];
if (textLine.FirstTextSourceIndex + textLine.Length < charIndex) if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
{ {
@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
return TextLines.Count - 1; return _textLines.Length - 1;
} }
private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point) private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
@ -424,18 +424,21 @@ namespace Avalonia.Media.TextFormatting
height += textLine.Height; height += textLine.Height;
} }
private IReadOnlyList<TextLine> CreateTextLines() private TextLine[] CreateTextLines()
{ {
var objectPool = FormattingObjectPool.Instance;
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);
Bounds = new Rect(0, 0, 0, textLine.Height); Bounds = new Rect(0, 0, 0, textLine.Height);
return new List<TextLine> { textLine }; return new TextLine[] { textLine };
} }
var textLines = new List<TextLine>(); var textLines = objectPool.TextLines.Rent();
double left = double.PositiveInfinity, width = 0.0, height = 0.0; double left = double.PositiveInfinity, width = 0.0, height = 0.0;
@ -443,16 +446,19 @@ namespace Avalonia.Media.TextFormatting
TextLine? previousLine = null; TextLine? previousLine = null;
var textFormatter = TextFormatter.Current;
while (true) while (true)
{ {
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties,
_paragraphProperties, previousLine?.TextLineBreak); previousLine?.TextLineBreak);
if(textLine == null || textLine.Length == 0) if (textLine.Length == 0)
{ {
if (previousLine != null && previousLine.NewLineLength > 0) if (previousLine != null && previousLine.NewLineLength > 0)
{ {
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties); var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
_paragraphProperties, objectPool);
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
@ -494,7 +500,7 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max lines constraint //Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines) if (MaxLines > 0 && textLines.Count >= MaxLines)
{ {
if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null) if(textLine.TextLineBreak?.RemainingRuns is not null)
{ {
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
} }
@ -511,7 +517,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); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool);
textLines.Add(textLine); textLines.Add(textLine);
@ -524,8 +530,9 @@ namespace Avalonia.Media.TextFormatting
{ {
var whitespaceWidth = 0d; var whitespaceWidth = 0d;
foreach (var line in textLines) for (var i = 0; i < textLines.Count; i++)
{ {
var line = textLines[i];
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if (lineWhitespaceWidth > whitespaceWidth) if (lineWhitespaceWidth > whitespaceWidth)
@ -549,7 +556,12 @@ namespace Avalonia.Media.TextFormatting
} }
} }
return textLines; var result = textLines.ToArray();
objectPool.TextLines.Return(ref textLines);
objectPool.VerifyAllReturned();
return result;
} }
/// <summary> /// <summary>
@ -569,7 +581,7 @@ namespace Avalonia.Media.TextFormatting
public void Dispose() public void Dispose()
{ {
foreach (var line in TextLines) foreach (var line in _textLines)
{ {
line.Dispose(); line.Dispose();
} }

104
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -1,5 +1,7 @@
using System; // ReSharper disable ForCanBeConvertedToForeach
using System;
using System.Collections.Generic; using System.Collections.Generic;
using static Avalonia.Media.TextFormatting.FormattingObjectPool;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextRun Symbol { get; } public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine) /// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{ {
var textRuns = textLine.TextRuns; var textRuns = textLine.TextRuns;
if (textRuns == null || textRuns.Count == 0) if (textRuns.Count == 0)
{ {
return null; return null;
} }
@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width) if (Width < shapedSymbol.GlyphRun.Size.Width)
{ {
return new List<TextRun>(0); return Array.Empty<TextRun>();
} }
// Overview of ellipsis structure // Overview of ellipsis structure
@ -75,55 +78,66 @@ namespace Avalonia.Media.TextFormatting
{ {
shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
var collapsedRuns = new List<TextRun>(textRuns.Count);
if (measuredLength > 0) if (measuredLength > 0)
{ {
IReadOnlyList<TextRun>? preSplitRuns = null; var objectPool = FormattingObjectPool.Instance;
IReadOnlyList<TextRun>? postSplitRuns;
if (_prefixLength > 0) var collapsedRuns = objectPool.TextRunLists.Rent();
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns,
Math.Min(_prefixLength, measuredLength));
collapsedRuns.AddRange(splitResult.First); RentedList<TextRun>? rentedPreSplitRuns = null;
RentedList<TextRun>? rentedPostSplitRuns = null;
TextRun[]? results;
preSplitRuns = splitResult.First; try
postSplitRuns = splitResult.Second;
}
else
{ {
postSplitRuns = textRuns; IReadOnlyList<TextRun>? effectivePostSplitRuns;
}
collapsedRuns.Add(shapedSymbol); if (_prefixLength > 0)
{
(rentedPreSplitRuns, rentedPostSplitRuns) = TextFormatterImpl.SplitTextRuns(
textRuns, Math.Min(_prefixLength, measuredLength), objectPool);
if (measuredLength <= _prefixLength || postSplitRuns is null) effectivePostSplitRuns = rentedPostSplitRuns;
{
return collapsedRuns; foreach (var preSplitRun in rentedPreSplitRuns)
} {
collapsedRuns.Add(preSplitRun);
}
}
else
{
effectivePostSplitRuns = textRuns;
}
var availableSuffixWidth = availableWidth; collapsedRuns.Add(shapedSymbol);
if (preSplitRuns is not null) if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
{
foreach (var run in preSplitRuns)
{ {
if (run is DrawableTextRun drawableTextRun) results = collapsedRuns.ToArray();
objectPool.TextRunLists.Return(ref collapsedRuns);
return results;
}
var availableSuffixWidth = availableWidth;
if (rentedPreSplitRuns is not null)
{
foreach (var run in rentedPreSplitRuns)
{ {
availableSuffixWidth -= drawableTextRun.Size.Width; if (run is DrawableTextRun drawableTextRun)
{
availableSuffixWidth -= drawableTextRun.Size.Width;
}
} }
} }
}
for (var i = postSplitRuns.Count - 1; i >= 0; i--)
{
var run = postSplitRuns[i];
switch (run) for (var i = effectivePostSplitRuns.Count - 1; i >= 0; i--)
{ {
case ShapedTextRun endShapedRun: var run = effectivePostSplitRuns[i];
switch (run)
{
case ShapedTextRun endShapedRun:
{ {
if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
out var suffixCount, out var suffixWidth)) out var suffixCount, out var suffixWidth))
@ -141,15 +155,21 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
}
} }
} }
} finally
else {
{ objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
collapsedRuns.Add(shapedSymbol); objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
}
results = collapsedRuns.ToArray();
objectPool.TextRunLists.Return(ref collapsedRuns);
return results;
} }
return collapsedRuns; return new TextRun[] { shapedSymbol };
} }
availableWidth -= shapedRun.Size.Width; availableWidth -= shapedRun.Size.Width;

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

@ -1,19 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
internal class TextLineImpl : TextLine internal sealed class TextLineImpl : TextLine
{ {
private IReadOnlyList<TextRun> _textRuns; private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth; private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics; private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _resolvedFlowDirection; private readonly FlowDirection _resolvedFlowDirection;
public TextLineImpl(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth, public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false) TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{ {
@ -147,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties, var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
_resolvedFlowDirection, TextLineBreak, true); _resolvedFlowDirection, TextLineBreak, true);
if (collapsedRuns.Count > 0) if (collapsedRuns.Length > 0)
{ {
collapsedLine.FinalizeLine(); collapsedLine.FinalizeLine();
} }
@ -166,7 +165,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetCharacterHitFromDistance(double distance) public override CharacterHit GetCharacterHitFromDistance(double distance)
{ {
if (_textRuns.Count == 0) if (_textRuns.Length == 0)
{ {
return new CharacterHit(); return new CharacterHit();
} }
@ -182,7 +181,7 @@ namespace Avalonia.Media.TextFormatting
if (distance >= WidthIncludingTrailingWhitespace) if (distance >= WidthIncludingTrailingWhitespace)
{ {
var lastRun = _textRuns[_textRuns.Count - 1]; var lastRun = _textRuns[_textRuns.Length - 1];
var size = 0.0; var size = 0.0;
@ -199,7 +198,7 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex; var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0; var currentDistance = 0.0;
for (var i = 0; i < _textRuns.Count; i++) for (var i = 0; i < _textRuns.Length; i++)
{ {
var currentRun = _textRuns[i]; var currentRun = _textRuns[i];
@ -208,7 +207,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftIndex = i; var rightToLeftIndex = i;
currentPosition += currentRun.Length; currentPosition += currentRun.Length;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1) while (rightToLeftIndex + 1 <= _textRuns.Length - 1)
{ {
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun; var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun;
@ -224,7 +223,7 @@ namespace Avalonia.Media.TextFormatting
for (var j = i; i <= rightToLeftIndex; j++) for (var j = i; i <= rightToLeftIndex; j++)
{ {
if (j > _textRuns.Count - 1) if (j > _textRuns.Length - 1)
{ {
break; break;
} }
@ -254,7 +253,7 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is DrawableTextRun drawableTextRun) if (currentRun is DrawableTextRun drawableTextRun)
{ {
if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance)
{ {
currentDistance += drawableTextRun.Size.Width; currentDistance += drawableTextRun.Size.Width;
@ -328,7 +327,7 @@ namespace Avalonia.Media.TextFormatting
if (flowDirection == FlowDirection.LeftToRight) if (flowDirection == FlowDirection.LeftToRight)
{ {
for (var index = 0; index < _textRuns.Count; index++) for (var index = 0; index < _textRuns.Length; index++)
{ {
var currentRun = _textRuns[index]; var currentRun = _textRuns[index];
@ -338,7 +337,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftWidth = shapedRun.Size.Width; var rightToLeftWidth = shapedRun.Size.Width;
while (i + 1 <= _textRuns.Count - 1) while (i + 1 <= _textRuns.Length - 1)
{ {
var nextRun = _textRuns[i + 1]; var nextRun = _textRuns[i + 1];
@ -402,7 +401,7 @@ namespace Avalonia.Media.TextFormatting
{ {
currentDistance += WidthIncludingTrailingWhitespace; currentDistance += WidthIncludingTrailingWhitespace;
for (var index = _textRuns.Count - 1; index >= 0; index--) for (var index = _textRuns.Length - 1; index >= 0; index--)
{ {
var currentRun = _textRuns[index]; var currentRun = _textRuns[index];
@ -502,7 +501,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{ {
if (_textRuns.Count == 0) if (_textRuns.Length == 0)
{ {
return new CharacterHit(); return new CharacterHit();
} }
@ -568,7 +567,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var characterIndex = firstTextSourceIndex + textLength; var characterIndex = firstTextSourceIndex + textLength;
var result = new List<TextBounds>(TextRuns.Count); var result = new List<TextBounds>(_textRuns.Length);
var lastDirection = FlowDirection.LeftToRight; var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection; var currentDirection = lastDirection;
@ -581,9 +580,9 @@ namespace Avalonia.Media.TextFormatting
TextRunBounds lastRunBounds = default; TextRunBounds lastRunBounds = default;
for (var index = 0; index < TextRuns.Count; index++) for (var index = 0; index < _textRuns.Length; index++)
{ {
if (TextRuns[index] is not DrawableTextRun currentRun) if (_textRuns[index] is not DrawableTextRun currentRun)
{ {
continue; continue;
} }
@ -637,7 +636,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftIndex = index; var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width; var rightToLeftWidth = currentShapedRun.Size.Width;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
{ {
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{ {
@ -669,12 +668,12 @@ namespace Avalonia.Media.TextFormatting
for (int i = rightToLeftIndex - 1; i >= index; i--) for (int i = rightToLeftIndex - 1; i >= index; i--)
{ {
if (TextRuns[i] is not ShapedTextRun) if (_textRuns[i] is not ShapedTextRun shapedRun)
{ {
continue; continue;
} }
currentShapedRun = (ShapedTextRun)TextRuns[i]; currentShapedRun = shapedRun;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
@ -784,7 +783,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var characterIndex = firstTextSourceIndex + textLength; var characterIndex = firstTextSourceIndex + textLength;
var result = new List<TextBounds>(TextRuns.Count); var result = new List<TextBounds>(_textRuns.Length);
var lastDirection = FlowDirection.LeftToRight; var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection; var currentDirection = lastDirection;
@ -795,9 +794,9 @@ namespace Avalonia.Media.TextFormatting
double currentWidth = 0; double currentWidth = 0;
var currentRect = default(Rect); var currentRect = default(Rect);
for (var index = TextRuns.Count - 1; index >= 0; index--) for (var index = _textRuns.Length - 1; index >= 0; index--)
{ {
if (TextRuns[index] is not DrawableTextRun currentRun) if (_textRuns[index] is not DrawableTextRun currentRun)
{ {
continue; continue;
} }
@ -981,7 +980,7 @@ namespace Avalonia.Media.TextFormatting
public override void Dispose() public override void Dispose()
{ {
for (int i = 0; i < _textRuns.Count; i++) for (int i = 0; i < _textRuns.Length; i++)
{ {
if (_textRuns[i] is ShapedTextRun shapedTextRun) if (_textRuns[i] is ShapedTextRun shapedTextRun)
{ {
@ -990,186 +989,11 @@ namespace Avalonia.Media.TextFormatting
} }
} }
public TextLineImpl FinalizeLine() public void FinalizeLine()
{ {
_textLineMetrics = CreateLineMetrics(); _textLineMetrics = CreateLineMetrics();
BidiReorder(); BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
return this;
}
private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
{
if (run is ShapedTextRun shapedTextCharacters)
{
return shapedTextCharacters.BidiLevel;
}
var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
return (sbyte)defaultLevel;
}
private void BidiReorder()
{
if (_textRuns.Count == 0)
{
return;
}
// Build up the collection of ordered runs.
var run = _textRuns[0];
OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection));
var current = orderedRun;
for (var i = 1; i < _textRuns.Count; i++)
{
run = _textRuns[i];
current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection));
current = current.Next;
}
// Reorder them into visual order.
orderedRun = LinearReOrder(orderedRun);
// Now perform a recursive reversal of each run.
// From the highest level found in the text to the lowest odd level on each line, including intermediate levels
// not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
// https://unicode.org/reports/tr9/#L2
sbyte max = 0;
var min = sbyte.MaxValue;
for (var i = 0; i < _textRuns.Count; i++)
{
var currentRun = _textRuns[i];
var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection);
if (level > max)
{
max = level;
}
if ((level & 1) != 0 && level < min)
{
min = level;
}
}
if (min > max)
{
min = max;
}
if (max == 0 || (min == max && (max & 1) == 0))
{
// Nothing to reverse.
return;
}
// Now apply the reversal and replace the original contents.
var minLevelToReverse = max;
while (minLevelToReverse >= min)
{
current = orderedRun;
while (current != null)
{
if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{
if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
{
shapedTextCharacters.Reverse();
}
}
current = current.Next;
}
minLevelToReverse--;
}
var textRuns = new List<TextRun>(_textRuns.Count);
current = orderedRun;
while (current != null)
{
textRuns.Add(current.Run);
current = current.Next;
}
_textRuns = textRuns;
}
/// <summary>
/// Reorders a series of runs from logical to visual order, returning the left most run.
/// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
/// </summary>
/// <param name="run">The ordered bidi run.</param>
/// <returns>The <see cref="OrderedBidiRun"/>.</returns>
private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run)
{
BidiRange? range = null;
while (run != null)
{
var next = run.Next;
while (range != null && range.Level > run.Level
&& range.Previous != null && range.Previous.Level >= run.Level)
{
range = BidiRange.MergeWithPrevious(range);
}
if (range != null && range.Level >= run.Level)
{
// Attach run to the range.
if ((run.Level & 1) != 0)
{
// Odd, range goes to the right of run.
run.Next = range.Left;
range.Left = run;
}
else
{
// Even, range goes to the left of run.
range.Right!.Next = run;
range.Right = run;
}
range.Level = run.Level;
}
else
{
var r = new BidiRange();
r.Left = r.Right = run;
r.Level = run.Level;
r.Previous = range;
range = r;
}
run = next;
}
while (range?.Previous != null)
{
range = BidiRange.MergeWithPrevious(range);
}
// Terminate.
range!.Right!.Next = null;
return range.Left!;
} }
/// <summary> /// <summary>
@ -1197,7 +1021,7 @@ namespace Avalonia.Media.TextFormatting
var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Count) while (runIndex < _textRuns.Length)
{ {
var currentRun = _textRuns[runIndex]; var currentRun = _textRuns[runIndex];
@ -1346,7 +1170,7 @@ namespace Avalonia.Media.TextFormatting
textPosition = FirstTextSourceIndex; textPosition = FirstTextSourceIndex;
TextRun? previousRun = null; TextRun? previousRun = null;
while (runIndex < _textRuns.Count) while (runIndex < _textRuns.Length)
{ {
var currentRun = _textRuns[runIndex]; var currentRun = _textRuns[runIndex];
@ -1395,7 +1219,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
if (runIndex + 1 >= _textRuns.Count) if (runIndex + 1 >= _textRuns.Length)
{ {
return runIndex; return runIndex;
} }
@ -1411,7 +1235,7 @@ namespace Avalonia.Media.TextFormatting
return runIndex; return runIndex;
} }
if (runIndex + 1 >= _textRuns.Count) if (runIndex + 1 >= _textRuns.Length)
{ {
return runIndex; return runIndex;
} }
@ -1448,14 +1272,14 @@ namespace Avalonia.Media.TextFormatting
var lineHeight = _paragraphProperties.LineHeight; var lineHeight = _paragraphProperties.LineHeight;
var lastRunIndex = _textRuns.Count - 1; var lastRunIndex = _textRuns.Length - 1;
if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine)
{ {
lastRunIndex--; lastRunIndex--;
} }
for (var index = 0; index < _textRuns.Count; index++) for (var index = 0; index < _textRuns.Length; index++)
{ {
switch (_textRuns[index]) switch (_textRuns[index])
{ {
@ -1620,59 +1444,5 @@ namespace Avalonia.Media.TextFormatting
return 0; return 0;
} }
} }
private sealed class OrderedBidiRun
{
public OrderedBidiRun(TextRun run, sbyte level)
{
Run = run;
Level = level;
}
public sbyte Level { get; }
public TextRun Run { get; }
public OrderedBidiRun? Next { get; set; }
}
private sealed class BidiRange
{
public int Level { get; set; }
public OrderedBidiRun? Left { get; set; }
public OrderedBidiRun? Right { get; set; }
public BidiRange? Previous { get; set; }
public static BidiRange MergeWithPrevious(BidiRange range)
{
var previous = range.Previous;
BidiRange left;
BidiRange right;
if ((previous!.Level & 1) != 0)
{
// Odd, previous goes to the right of range.
left = range;
right = previous;
}
else
{
// Even, previous goes to the left of range.
left = previous;
right = range;
}
// Stitch them
left.Right!.Next = right.Left;
previous.Left = left.Left;
previous.Right = right.Right;
return previous;
}
}
} }
} }

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

@ -93,6 +93,9 @@ namespace Avalonia.Media.TextFormatting
internal TextRunProperties WithTypeface(Typeface typeface) internal TextRunProperties WithTypeface(Typeface typeface)
{ {
if (this is GenericTextRunProperties other && other.Typeface == typeface)
return this;
return new GenericTextRunProperties(typeface, FontRenderingEmSize, return new GenericTextRunProperties(typeface, FontRenderingEmSize,
TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment);
} }

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

@ -1,6 +1,4 @@
using System.Collections.Generic; namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{ {
/// <summary> /// <summary>
/// A collapsing properties to collapse whole line toward the end /// A collapsing properties to collapse whole line toward the end
@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextRun Symbol { get; } public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine) /// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{ {
return TextEllipsisHelper.Collapse(textLine, this, false); return TextEllipsisHelper.Collapse(textLine, this, false);
} }

8
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -1,7 +1,4 @@
using System.Collections.Generic; namespace Avalonia.Media.TextFormatting
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{ {
/// <summary> /// <summary>
/// a collapsing properties to collapse whole line toward the end /// a collapsing properties to collapse whole line toward the end
@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextRun Symbol { get; } public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine) /// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{ {
return TextEllipsisHelper.Collapse(textLine, this, true); return TextEllipsisHelper.Collapse(textLine, this, true);
} }

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

@ -5,8 +5,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading;
using Avalonia.Collections.Pooled;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
@ -28,8 +26,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// as much as possible. /// as much as possible.
/// </para> /// </para>
/// </remarks> /// </remarks>
internal struct BidiAlgorithm : IDisposable internal sealed class BidiAlgorithm
{ {
/// <summary>
/// Whether the state is clean and can be reused without a reset.
/// </summary>
private bool _hasCleanState = true;
/// <summary> /// <summary>
/// The original BiDiClass classes as provided by the caller /// The original BiDiClass classes as provided by the caller
/// </summary> /// </summary>
@ -67,7 +70,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// The forward mapping maps the start index to the end index. /// The forward mapping maps the start index to the end index.
/// The reverse mapping maps the end index to the start index. /// The reverse mapping maps the end index to the start index.
/// </remarks> /// </remarks>
private BidiDictionary<int, int>? _isolatePairs; private readonly BidiDictionary<int, int> _isolatePairs = new();
/// <summary> /// <summary>
/// The working BiDi classes /// The working BiDi classes
@ -98,7 +101,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// The status stack used during resolution of explicit /// The status stack used during resolution of explicit
/// embedding and isolating runs /// embedding and isolating runs
/// </summary> /// </summary>
private readonly Stack<Status> _statusStack = new Stack<Status>(); private readonly Stack<Status> _statusStack = new();
/// <summary> /// <summary>
/// Mapping used to virtually remove characters for rule X9 /// Mapping used to virtually remove characters for rule X9
@ -108,7 +111,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// Re-usable list of level runs /// Re-usable list of level runs
/// </summary> /// </summary>
private readonly List<LevelRun> _levelRuns = new List<LevelRun>(); private readonly List<LevelRun> _levelRuns = new();
/// <summary> /// <summary>
/// Mapping for the current isolating sequence, built /// Mapping for the current isolating sequence, built
@ -119,7 +122,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// A stack of pending isolate openings used by FindIsolatePairs() /// A stack of pending isolate openings used by FindIsolatePairs()
/// </summary> /// </summary>
private Stack<int>? _pendingIsolateOpenings; private readonly Stack<int> _pendingIsolateOpenings = new();
/// <summary> /// <summary>
/// The level of the isolating run currently being processed /// The level of the isolating run currently being processed
@ -175,12 +178,12 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Reusable list of pending opening brackets used by the /// Reusable list of pending opening brackets used by the
/// LocatePairedBrackets method /// LocatePairedBrackets method
/// </summary> /// </summary>
private readonly List<int> _pendingOpeningBrackets = new List<int>(); private readonly List<int> _pendingOpeningBrackets = new();
/// <summary> /// <summary>
/// Resolved list of paired brackets /// Resolved list of paired brackets
/// </summary> /// </summary>
private readonly List<BracketPair> _pairedBrackets = new List<BracketPair>(); private readonly List<BracketPair> _pairedBrackets = new();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BidiAlgorithm"/> class. /// Initializes a new instance of the <see cref="BidiAlgorithm"/> class.
@ -228,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
ArraySlice<sbyte>? outLevels) ArraySlice<sbyte>? outLevels)
{ {
// Reset state // Reset state
_isolatePairs?.Clear(); Reset();
_workingClassesBuffer.Clear();
_levelRuns.Clear();
_resolvedLevelsBuffer.Clear();
if (types.IsEmpty) if (types.IsEmpty)
{ {
return; return;
} }
_hasCleanState = false;
// Setup original types and working types // Setup original types and working types
_originalClasses = types; _originalClasses = types;
_workingClasses = _workingClassesBuffer.Add(types); _workingClasses = _workingClassesBuffer.Add(types);
@ -324,7 +326,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Skip isolate pairs // Skip isolate pairs
// (Because we're working with a slice, we need to adjust the indices // (Because we're working with a slice, we need to adjust the indices
// we're using for the isolatePairs map) // we're using for the isolatePairs map)
if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true) if (_isolatePairs.TryGetValue(data.Start + i, out i))
{ {
i -= data.Start; i -= data.Start;
} }
@ -359,7 +361,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_hasIsolates = false; _hasIsolates = false;
// BD9... // BD9...
_pendingIsolateOpenings?.Clear(); _pendingIsolateOpenings.Clear();
for (var i = 0; i < _originalClasses.Length; i++) for (var i = 0; i < _originalClasses.Length; i++)
{ {
@ -371,16 +373,14 @@ namespace Avalonia.Media.TextFormatting.Unicode
case BidiClass.RightToLeftIsolate: case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate: case BidiClass.FirstStrongIsolate:
{ {
_pendingIsolateOpenings ??= new Stack<int>();
_pendingIsolateOpenings.Push(i); _pendingIsolateOpenings.Push(i);
_hasIsolates = true; _hasIsolates = true;
break; break;
} }
case BidiClass.PopDirectionalIsolate: case BidiClass.PopDirectionalIsolate:
{ {
if (_pendingIsolateOpenings?.Count > 0) if (_pendingIsolateOpenings.Count > 0)
{ {
_isolatePairs ??= new BidiDictionary<int, int>();
_isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i);
} }
@ -501,7 +501,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (resolvedIsolate == BidiClass.FirstStrongIsolate) if (resolvedIsolate == BidiClass.FirstStrongIsolate)
{ {
if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate)) if (!_isolatePairs.TryGetValue(i, out var endOfIsolate))
{ {
endOfIsolate = _originalClasses.Length; endOfIsolate = _originalClasses.Length;
} }
@ -832,7 +832,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
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 ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) &&
_isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true) _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
{ {
// Find the continuing run index // Find the continuing run index
runIndex = FindRunForIndex(nextRunIndex); runIndex = FindRunForIndex(nextRunIndex);
@ -855,13 +855,14 @@ namespace Avalonia.Media.TextFormatting.Unicode
private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel) private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel)
{ {
// Create mappings onto the underlying data // Create mappings onto the underlying data
_runResolvedClasses = new MappedArraySlice<BidiClass>(_workingClasses, _isolatedRunMapping.AsSlice()); var isolatedRunMapping = _isolatedRunMapping.AsSlice();
_runOriginalClasses = new MappedArraySlice<BidiClass>(_originalClasses, _isolatedRunMapping.AsSlice()); _runResolvedClasses = new MappedArraySlice<BidiClass>(_workingClasses, isolatedRunMapping);
_runLevels = new MappedArraySlice<sbyte>(_resolvedLevels, _isolatedRunMapping.AsSlice()); _runOriginalClasses = new MappedArraySlice<BidiClass>(_originalClasses, isolatedRunMapping);
_runLevels = new MappedArraySlice<sbyte>(_resolvedLevels, isolatedRunMapping);
if (_hasBrackets) if (_hasBrackets)
{ {
_runBiDiPairedBracketTypes = new MappedArraySlice<BidiPairedBracketType>(_pairedBracketTypes, _isolatedRunMapping.AsSlice()); _runBiDiPairedBracketTypes = new MappedArraySlice<BidiPairedBracketType>(_pairedBracketTypes, isolatedRunMapping);
_runPairedBracketValues = new MappedArraySlice<int>(_pairedBracketValues, _isolatedRunMapping.AsSlice()); _runPairedBracketValues = new MappedArraySlice<int>(_pairedBracketValues, isolatedRunMapping);
} }
_runLevel = runLevel; _runLevel = runLevel;
@ -1642,6 +1643,47 @@ namespace Avalonia.Media.TextFormatting.Unicode
} }
} }
/// <summary>
/// Resets the bidi algorithm to a clean state.
/// </summary>
public void Reset()
{
if (_hasCleanState)
{
return;
}
_originalClasses = default;
_pairedBracketTypes = default;
_pairedBracketValues = default;
_hasBrackets = default;
_hasEmbeddings = default;
_hasIsolates = default;
_isolatePairs.ClearThenResetIfTooLarge();
_workingClasses = default;
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer);
_resolvedLevels = default;
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer);
_paragraphEmbeddingLevel = default;
FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map);
FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping);
FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings);
_runLevel = default;
_runDirection = default;
_runLength = default;
_runResolvedClasses = default;
_runOriginalClasses = default;
_runLevels = default;
_runBiDiPairedBracketTypes = default;
_runPairedBracketValues = default;
FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets);
FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets);
_hasCleanState = true;
}
/// <summary> /// <summary>
/// Hold the start and end index of a pair of brackets /// Hold the start and end index of a pair of brackets
/// </summary> /// </summary>
@ -1717,13 +1759,5 @@ namespace Avalonia.Media.TextFormatting.Unicode
public BidiClass Eos { get; } public BidiClass Eos { get; }
} }
public void Dispose()
{
_workingClassesBuffer.Dispose();
_resolvedLevelsBuffer.Dispose();
_x9Map.Dispose();
_isolatedRunMapping.Dispose();
}
} }
} }

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

@ -11,8 +11,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Represents a unicode string and all associated attributes /// Represents a unicode string and all associated attributes
/// for each character required for the bidirectional Unicode algorithm /// for each character required for the bidirectional Unicode algorithm
/// </summary> /// </summary>
internal struct BidiData : IDisposable /// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
internal sealed class BidiData
{ {
private bool _hasCleanState = true;
private ArrayBuilder<BidiClass> _classes; private ArrayBuilder<BidiClass> _classes;
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes; private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
private ArrayBuilder<int> _pairedBracketValues; private ArrayBuilder<int> _pairedBracketValues;
@ -20,12 +22,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes; private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes;
private ArrayBuilder<sbyte> _tempLevelBuffer; private ArrayBuilder<sbyte> _tempLevelBuffer;
public BidiData(sbyte paragraphEmbeddingLevel) public sbyte ParagraphEmbeddingLevel { get; set; }
{
ParagraphEmbeddingLevel = paragraphEmbeddingLevel;
}
public sbyte ParagraphEmbeddingLevel { get; private set; }
public bool HasBrackets { get; private set; } public bool HasBrackets { get; private set; }
@ -36,7 +33,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// Gets the length of the data held by the BidiData /// Gets the length of the data held by the BidiData
/// </summary> /// </summary>
public int Length{get; private set; } public int Length { get; private set; }
/// <summary> /// <summary>
/// Gets the bidi character type of each code point /// Gets the bidi character type of each code point
@ -66,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="text">The text to process.</param> /// <param name="text">The text to process.</param>
public void Append(ReadOnlySpan<char> text) public void Append(ReadOnlySpan<char> text)
{ {
_hasCleanState = false;
_classes.Add(text.Length); _classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length); _pairedBracketTypes.Add(text.Length);
_pairedBracketValues.Add(text.Length); _pairedBracketValues.Add(text.Length);
@ -151,6 +150,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </remarks> /// </remarks>
public void SaveTypes() public void SaveTypes()
{ {
_hasCleanState = false;
// Capture the types data // Capture the types data
_savedClasses.Clear(); _savedClasses.Clear();
_savedClasses.Add(_classes.AsSlice()); _savedClasses.Add(_classes.AsSlice());
@ -163,6 +164,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary> /// </summary>
public void RestoreTypes() public void RestoreTypes()
{ {
_hasCleanState = false;
_classes.Clear(); _classes.Clear();
_classes.Add(_savedClasses.AsSlice()); _classes.Add(_savedClasses.AsSlice());
_pairedBracketTypes.Clear(); _pairedBracketTypes.Clear();
@ -182,14 +185,34 @@ namespace Avalonia.Media.TextFormatting.Unicode
return _tempLevelBuffer.Add(length, false); return _tempLevelBuffer.Add(length, false);
} }
public void Dispose() /// <summary>
/// Resets the bidi data to a clean state.
/// </summary>
public void Reset()
{ {
_classes.Dispose(); if (_hasCleanState)
_pairedBracketTypes.Dispose(); {
_pairedBracketValues.Dispose(); return;
_savedClasses.Dispose(); }
_savedPairedBracketTypes.Dispose();
_tempLevelBuffer.Dispose(); FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer);
ParagraphEmbeddingLevel = 0;
HasBrackets = false;
HasEmbeddings = false;
HasIsolates = false;
Length = 0;
Classes = default;
PairedBracketTypes = default;
PairedBracketValues = default;
_hasCleanState = true;
} }
} }
} }

9
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Platform namespace Avalonia.Platform
@ -166,11 +167,9 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param> /// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param> /// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="glyphIndices">The glyph indices.</param> /// <param name="glyphInfos">The list of glyphs.</param>
/// <param name="glyphAdvances">The glyph advances.</param> /// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
/// <param name="glyphOffsets">The glyph offsets.</param> IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos);
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
/// <summary> /// <summary>
/// Creates a backend-specific object using a low-level API graphics context /// Creates a backend-specific object using a low-level API graphics context

2
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@ -32,7 +32,7 @@ internal class FpsCounter
{ {
var s = new string((char)c, 1); var s = new string((char)c, 1);
var glyph = typeface.GetGlyph((uint)(s[0])); var glyph = typeface.GetGlyph((uint)(s[0]));
_runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph });
} }
} }

66
src/Avalonia.Base/Utilities/ArrayBuilder.cs

@ -3,7 +3,6 @@
// Ported from: https://github.com/SixLabors/Fonts/ // Ported from: https://github.com/SixLabors/Fonts/
using System; using System;
using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Avalonia.Utilities namespace Avalonia.Utilities
@ -12,14 +11,13 @@ namespace Avalonia.Utilities
/// A helper type for avoiding allocations while building arrays. /// A helper type for avoiding allocations while building arrays.
/// </summary> /// </summary>
/// <typeparam name="T">The type of item contained in the array.</typeparam> /// <typeparam name="T">The type of item contained in the array.</typeparam>
internal struct ArrayBuilder<T> : IDisposable internal struct ArrayBuilder<T>
where T : struct
{ {
private const int DefaultCapacity = 4; private const int DefaultCapacity = 4;
private const int MaxCoreClrArrayLength = 0x7FeFFFFF; private const int MaxCoreClrArrayLength = 0x7FeFFFFF;
// Starts out null, initialized on first Add. // Starts out null, initialized on first Add.
private T[] _data; private T[]? _data;
private int _size; private int _size;
/// <summary> /// <summary>
@ -49,6 +47,12 @@ namespace Avalonia.Utilities
} }
} }
/// <summary>
/// Gets the current capacity of the array.
/// </summary>
public int Capacity
=> _data?.Length ?? 0;
/// <summary> /// <summary>
/// Returns a reference to specified element of the array. /// Returns a reference to specified element of the array.
/// </summary> /// </summary>
@ -116,14 +120,44 @@ namespace Avalonia.Utilities
return slice; return slice;
} }
/// <summary>
/// Appends an item.
/// </summary>
/// <param name="value">The item to append.</param>
public void AddItem(T value)
{
var index = Length++;
_data![index] = value;
}
/// <summary> /// <summary>
/// Clears the array. /// Clears the array.
/// Allocated memory is left intact for future usage. /// Allocated memory is left intact for future usage.
/// </summary> /// </summary>
public void Clear() public void Clear()
{ {
// No need to actually clear since we're not allowing reference types. #if NET6_0_OR_GREATER
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
{
ClearArray();
}
else
{
_size = 0;
}
#else
ClearArray();
#endif
}
private void ClearArray()
{
var size = _size;
_size = 0; _size = 0;
if (size > 0)
{
Array.Clear(_data!, 0, size);
}
} }
private void EnsureCapacity(int min) private void EnsureCapacity(int min)
@ -136,7 +170,7 @@ namespace Avalonia.Utilities
} }
// Same expansion algorithm as List<T>. // Same expansion algorithm as List<T>.
var newCapacity = length == 0 ? DefaultCapacity : length * 2; var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u;
if (newCapacity > MaxCoreClrArrayLength) if (newCapacity > MaxCoreClrArrayLength)
{ {
@ -145,15 +179,14 @@ namespace Avalonia.Utilities
if (newCapacity < min) if (newCapacity < min)
{ {
newCapacity = min; newCapacity = (uint)min;
} }
var array = ArrayPool<T>.Shared.Rent(newCapacity); var array = new T[newCapacity];
if (_size > 0) if (_size > 0)
{ {
Array.Copy(_data!, array, _size); Array.Copy(_data!, array, _size);
ArrayPool<T>.Shared.Return(_data!);
} }
_data = array; _data = array;
@ -182,13 +215,12 @@ namespace Avalonia.Utilities
/// <returns>The <see cref="ArraySlice{T}"/>.</returns> /// <returns>The <see cref="ArraySlice{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length); public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length);
public void Dispose() /// <summary>
{ /// Returns the current state of the array as a span.
if (_data != null) /// </summary>
{ /// <returns>The <see cref="Span{T}"/>.</returns>
ArrayPool<T>.Shared.Return(_data); [MethodImpl(MethodImplOptions.AggressiveInlining)]
} public Span<T> AsSpan() => _data.AsSpan(0, _size);
}
} }
} }

11
src/Avalonia.Base/Utilities/ArraySlice.cs

@ -3,7 +3,6 @@
// Ported from: https://github.com/SixLabors/Fonts/ // Ported from: https://github.com/SixLabors/Fonts/
using System; using System;
using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -18,7 +17,6 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <typeparam name="T">The type of item contained in the slice.</typeparam> /// <typeparam name="T">The type of item contained in the slice.</typeparam>
internal readonly struct ArraySlice<T> : IReadOnlyList<T> internal readonly struct ArraySlice<T> : IReadOnlyList<T>
where T : struct
{ {
/// <summary> /// <summary>
/// Gets an empty <see cref="ArraySlice{T}"/> /// Gets an empty <see cref="ArraySlice{T}"/>
@ -186,13 +184,6 @@ namespace Avalonia.Utilities
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<T>.Count => Length; int IReadOnlyCollection<T>.Count => Length;
public void ReturnRent()
{
if (_data != null)
{
ArrayPool<T>.Shared.Return(_data);
}
}
} }
} }

29
src/Avalonia.Base/Utilities/BidiDictionary.cs

@ -1,4 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
@ -9,32 +11,27 @@ namespace Avalonia.Utilities
/// <typeparam name="T2">Value type</typeparam> /// <typeparam name="T2">Value type</typeparam>
internal sealed class BidiDictionary<T1, T2> where T1 : notnull where T2 : notnull internal sealed class BidiDictionary<T1, T2> where T1 : notnull where T2 : notnull
{ {
public Dictionary<T1, T2> Forward { get; } = new Dictionary<T1, T2>(); private Dictionary<T1, T2> _forward = new();
private Dictionary<T2, T1> _reverse = new();
public Dictionary<T2, T1> Reverse { get; } = new Dictionary<T2, T1>(); public void ClearThenResetIfTooLarge()
public void Clear()
{ {
Forward.Clear(); FormattingBufferHelper.ClearThenResetIfTooLarge(ref _forward);
Reverse.Clear(); FormattingBufferHelper.ClearThenResetIfTooLarge(ref _reverse);
} }
public void Add(T1 key, T2 value) public void Add(T1 key, T2 value)
{ {
Forward.Add(key, value); _forward.Add(key, value);
Reverse.Add(value, key); _reverse.Add(value, key);
} }
#pragma warning disable CS8601 public bool TryGetValue(T1 key, [MaybeNullWhen(false)] out T2 value) => _forward.TryGetValue(key, out value);
public bool TryGetValue(T1 key, out T2 value) => Forward.TryGetValue(key, out value);
#pragma warning restore CS8601
#pragma warning disable CS8601 public bool TryGetKey(T2 value, [MaybeNullWhen(false)] out T1 key) => _reverse.TryGetValue(value, out key);
public bool TryGetKey(T2 value, out T1 key) => Reverse.TryGetValue(value, out key);
#pragma warning restore CS8601
public bool ContainsKey(T1 key) => Forward.ContainsKey(key); public bool ContainsKey(T1 key) => _forward.ContainsKey(key);
public bool ContainsValue(T2 value) => Reverse.ContainsKey(value); public bool ContainsValue(T2 value) => _reverse.ContainsKey(value);
} }
} }

3
src/Avalonia.Base/Utilities/BinarySearchExtension.cs

@ -14,7 +14,6 @@
// under the License. // under the License.
// Copied from: https://github.com/toptensoftware/RichTextKit // Copied from: https://github.com/toptensoftware/RichTextKit
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Avalonia.Utilities namespace Avalonia.Utilities
@ -39,7 +38,7 @@ namespace Avalonia.Utilities
/// <param name="value">The value to search for</param> /// <param name="value">The value to search for</param>
/// <param name="comparer">The comparer</param> /// <param name="comparer">The comparer</param>
/// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns> /// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
public static int BinarySearch<T>(this IReadOnlyList<T> list, T value, IComparer<T> comparer) where T : IComparable public static int BinarySearch<T>(this IReadOnlyList<T> list, T value, IComparer<T> comparer)
{ {
return list.BinarySearch(0, list.Count, value, comparer); return list.BinarySearch(0, list.Count, value, comparer);
} }

2
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -79,7 +79,7 @@ namespace Avalonia.Controls
{ {
if(run.Length > 0) if(run.Length > 0)
{ {
#if NET6_0 #if NET6_0_OR_GREATER
builder.Append(run.Text.Span); builder.Append(run.Text.Span);
#else #else
builder.Append(run.Text.Span.ToArray()); builder.Append(run.Text.Span.ToArray());

3
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -9,6 +9,7 @@ using Avalonia.Rendering;
using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Headless namespace Avalonia.Headless
{ {
@ -118,7 +119,7 @@ namespace Avalonia.Headless
return new HeadlessGeometryStub(new Rect(glyphRun.Size)); return new HeadlessGeometryStub(new Rect(glyphRun.Size));
} }
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets) public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
{ {
return new HeadlessGlyphRunStub(); return new HeadlessGlyphRunStub();
} }

83
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -1,16 +1,11 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.OpenGL; using Avalonia.OpenGL;
using Avalonia.OpenGL.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
@ -88,9 +83,9 @@ namespace Avalonia.Skia
var (currentX, currentY) = glyphRun.BaselineOrigin; var (currentX, currentY) = glyphRun.BaselineOrigin;
for (var i = 0; i < glyphRun.GlyphIndices.Count; i++) for (var i = 0; i < glyphRun.GlyphInfos.Count; i++)
{ {
var glyph = glyphRun.GlyphIndices[i]; var glyph = glyphRun.GlyphInfos[i].GlyphIndex;
var glyphPath = skFont.GetGlyphPath(glyph); var glyphPath = skFont.GetGlyphPath(glyph);
if (!glyphPath.IsEmpty) if (!glyphPath.IsEmpty)
@ -98,14 +93,7 @@ namespace Avalonia.Skia
path.AddPath(glyphPath, (float)currentX, (float)currentY); path.AddPath(glyphPath, (float)currentX, (float)currentY);
} }
if (glyphRun.GlyphAdvances != null) currentX += glyphRun.GlyphInfos[i].GlyphAdvance;
{
currentX += glyphRun.GlyphAdvances[i];
}
else
{
currentX += glyphPath.Bounds.Right;
}
} }
return new StreamGeometryImpl(path); return new StreamGeometryImpl(path);
@ -213,17 +201,16 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format, alphaFormat); return new WriteableBitmapImpl(size, dpi, format, alphaFormat);
} }
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{ {
if (glyphTypeface == null) if (glyphTypeface == null)
{ {
throw new ArgumentNullException(nameof(glyphTypeface)); throw new ArgumentNullException(nameof(glyphTypeface));
} }
if (glyphIndices == null) if (glyphInfos == null)
{ {
throw new ArgumentNullException(nameof(glyphIndices)); throw new ArgumentNullException(nameof(glyphInfos));
} }
var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl;
@ -242,59 +229,25 @@ namespace Avalonia.Skia
var builder = new SKTextBlobBuilder(); var builder = new SKTextBlobBuilder();
var count = glyphIndices.Count; var count = glyphInfos.Count;
if (glyphOffsets != null && glyphAdvances != null) var runBuffer = builder.AllocatePositionedRun(font, count);
{
var runBuffer = builder.AllocatePositionedRun(font, count);
var glyphSpan = runBuffer.GetGlyphSpan(); var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan(); var positionSpan = runBuffer.GetPositionSpan();
var currentX = 0.0; var currentX = 0.0;
for (int i = 0; i < glyphOffsets.Count; i++) for (int i = 0; i < count; i++)
{
var offset = glyphOffsets[i];
glyphSpan[i] = glyphIndices[i];
positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
currentX += glyphAdvances[i];
}
}
else
{ {
if (glyphAdvances != null) var glyphInfo = glyphInfos[i];
{ var offset = glyphInfo.GlyphOffset;
var runBuffer = builder.AllocateHorizontalRun(font, count, 0);
var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan();
var currentX = 0.0;
for (int i = 0; i < glyphAdvances.Count; i++) glyphSpan[i] = glyphInfo.GlyphIndex;
{
glyphSpan[i] = glyphIndices[i];
positionSpan[i] = (float)currentX; positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
currentX += glyphAdvances[i]; currentX += glyphInfo.GlyphAdvance;
}
}
else
{
var runBuffer = builder.AllocateRun(font, count, 0, 0);
var glyphSpan = runBuffer.GetGlyphSpan();
for (int i = 0; i < glyphIndices.Count; i++)
{
glyphSpan[i] = glyphIndices[i];
}
}
} }
return new GlyphRunImpl(builder.Build()); return new GlyphRunImpl(builder.Build());

2
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -161,6 +161,8 @@ namespace Avalonia.Skia
if (MemoryMarshal.TryGetArray(memory, out var segment)) if (MemoryMarshal.TryGetArray(memory, out var segment))
{ {
start = segment.Offset;
length = segment.Count;
return segment.Array.AsMemory(); return segment.Array.AsMemory();
} }

42
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -7,6 +7,7 @@ using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Direct2D1.Media.Imaging;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.DirectWrite; using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun; using GlyphRun = Avalonia.Media.GlyphRun;
@ -157,12 +158,11 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets)
{ {
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
var glyphCount = glyphIndices.Count; var glyphCount = glyphInfos.Count;
var run = new SharpDX.DirectWrite.GlyphRun var run = new SharpDX.DirectWrite.GlyphRun
{ {
@ -174,44 +174,23 @@ namespace Avalonia.Direct2D1
for (var i = 0; i < glyphCount; i++) for (var i = 0; i < glyphCount; i++)
{ {
indices[i] = (short)glyphIndices[i]; indices[i] = (short)glyphInfos[i].GlyphIndex;
} }
run.Indices = indices; run.Indices = indices;
run.Advances = new float[glyphCount]; run.Advances = new float[glyphCount];
var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight); for (var i = 0; i < glyphCount; i++)
if (glyphAdvances == null)
{
for (var i = 0; i < glyphCount; i++)
{
var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale;
run.Advances[i] = advance;
}
}
else
{
for (var i = 0; i < glyphCount; i++)
{
var advance = (float)glyphAdvances[i];
run.Advances[i] = advance;
}
}
if (glyphOffsets == null)
{ {
return new GlyphRunImpl(run); run.Advances[i] = (float)glyphInfos[i].GlyphAdvance;
} }
run.Offsets = new GlyphOffset[glyphCount]; run.Offsets = new GlyphOffset[glyphCount];
for (var i = 0; i < glyphCount; i++) for (var i = 0; i < glyphCount; i++)
{ {
var (x, y) = glyphOffsets[i]; var (x, y) = glyphInfos[i].GlyphOffset;
run.Offsets[i] = new GlyphOffset run.Offsets[i] = new GlyphOffset
{ {
@ -254,11 +233,12 @@ namespace Avalonia.Direct2D1
using (var sink = pathGeometry.Open()) using (var sink = pathGeometry.Open())
{ {
var glyphs = new short[glyphRun.GlyphIndices.Count]; var glyphInfos = glyphRun.GlyphInfos;
var glyphs = new short[glyphInfos.Count];
for (int i = 0; i < glyphRun.GlyphIndices.Count; i++) for (int i = 0; i < glyphInfos.Count; i++)
{ {
glyphs[i] = (short)glyphRun.GlyphIndices[i]; glyphs[i] = (short)glyphInfos[i].GlyphIndex;
} }
glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink); glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink);

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

@ -161,6 +161,8 @@ namespace Avalonia.Direct2D1.Media
if (MemoryMarshal.TryGetArray(memory, out var segment)) if (MemoryMarshal.TryGetArray(memory, out var segment))
{ {
start = segment.Offset;
length = segment.Count;
return segment.Array.AsMemory(); return segment.Array.AsMemory();
} }

14
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@ -1,5 +1,7 @@
using System.Linq; using System;
using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -179,12 +181,14 @@ namespace Avalonia.Base.UnitTests.Media
private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0) private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0)
{ {
var count = glyphAdvances.Length; var count = glyphAdvances.Length;
var glyphIndices = new ushort[count];
var characters = Enumerable.Repeat('a', count).ToArray(); var glyphInfos = new GlyphInfo[count];
for (var i = 0; i < count; ++i)
{
glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]);
}
return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances, return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel);
glyphClusters: glyphClusters, biDiLevel: bidiLevel);
} }
} }
} }

6
tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs

@ -1,8 +1,6 @@
using System; using System.Linq;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -32,7 +30,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
private bool Run(BiDiClassData t) private bool Run(BiDiClassData t)
{ {
var bidi = new BidiAlgorithm(); var bidi = new BidiAlgorithm();
var bidiData = new BidiData(t.ParagraphLevel); var bidiData = new BidiData { ParagraphEmbeddingLevel = t.ParagraphLevel };
var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.CodePoints).ToArray()); var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.CodePoints).ToArray());

151
tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs

@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
using Xunit;
namespace Avalonia.Base.UnitTests.Media.TextFormatting
{
public class FormattingBufferHelperTests
{
public static TheoryData<int> SmallSizes => new() { 1, 500, 10_000, 125_000 };
public static TheoryData<int> LargeSizes => new() { 500_000, 1_000_000 };
[Theory]
[MemberData(nameof(SmallSizes))]
public void Should_Keep_Small_Buffer_List(int itemCount)
{
var capacity = FillAndClearList(itemCount);
Assert.True(capacity >= itemCount);
}
[Theory]
[MemberData(nameof(LargeSizes))]
public void Should_Reset_Large_Buffer_List(int itemCount)
{
var capacity = FillAndClearList(itemCount);
Assert.Equal(0, capacity);
}
private static int FillAndClearList(int itemCount)
{
var list = new List<int>();
for (var i = 0; i < itemCount; ++i)
{
list.Add(i);
}
FormattingBufferHelper.ClearThenResetIfTooLarge(list);
return list.Capacity;
}
[Theory]
[MemberData(nameof(SmallSizes))]
public void Should_Keep_Small_Buffer_ArrayBuilder(int itemCount)
{
var capacity = FillAndClearArrayBuilder(itemCount);
Assert.True(capacity >= itemCount);
}
[Theory]
[MemberData(nameof(LargeSizes))]
public void Should_Reset_Large_Buffer_ArrayBuilder(int itemCount)
{
var capacity = FillAndClearArrayBuilder(itemCount);
Assert.Equal(0, capacity);
}
private static int FillAndClearArrayBuilder(int itemCount)
{
var arrayBuilder = new ArrayBuilder<int>();
for (var i = 0; i < itemCount; ++i)
{
arrayBuilder.AddItem(i);
}
FormattingBufferHelper.ClearThenResetIfTooLarge(ref arrayBuilder);
return arrayBuilder.Capacity;
}
[Theory]
[MemberData(nameof(SmallSizes))]
public void Should_Keep_Small_Buffer_Stack(int itemCount)
{
var capacity = FillAndClearStack(itemCount);
Assert.True(capacity >= itemCount);
}
[Theory]
[MemberData(nameof(LargeSizes))]
public void Should_Reset_Large_Buffer_Stack(int itemCount)
{
var capacity = FillAndClearStack(itemCount);
Assert.Equal(0, capacity);
}
private static int FillAndClearStack(int itemCount)
{
var stack = new Stack<int>();
for (var i = 0; i < itemCount; ++i)
{
stack.Push(i);
}
FormattingBufferHelper.ClearThenResetIfTooLarge(stack);
var array = (Array) stack.GetType()
.GetField("_array", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(stack)!;
return array.Length;
}
[Theory]
[MemberData(nameof(SmallSizes))]
public void Should_Keep_Small_Buffer_Dictionary(int itemCount)
{
var capacity = FillAndClearDictionary(itemCount);
Assert.True(capacity >= itemCount);
}
[Theory]
[MemberData(nameof(LargeSizes))]
public void Should_Reset_Large_Buffer_Dictionary(int itemCount)
{
var capacity = FillAndClearDictionary(itemCount);
Assert.True(capacity <= 3); // dictionary trims to the nearest prime starting with 3
}
private static int FillAndClearDictionary(int itemCount)
{
var dictionary = new Dictionary<int, int>();
for (var i = 0; i < itemCount; ++i)
{
dictionary.Add(i, i);
}
FormattingBufferHelper.ClearThenResetIfTooLarge(ref dictionary);
var array = (Array) dictionary.GetType()
.GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(dictionary)!;
return array.Length;
}
}
}

3
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Base.UnitTests.VisualTree namespace Avalonia.Base.UnitTests.VisualTree
{ {
@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets) public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

3
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Microsoft.Diagnostics.Runtime; using Microsoft.Diagnostics.Runtime;
namespace Avalonia.Benchmarks namespace Avalonia.Benchmarks
@ -120,7 +121,7 @@ namespace Avalonia.Benchmarks
return new MockStreamGeometryImpl(); return new MockStreamGeometryImpl();
} }
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets) public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
{ {
return new MockGlyphRun(); return new MockGlyphRun();
} }

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

@ -9,6 +9,8 @@ using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Text; namespace Avalonia.Benchmarks.Text;
[MemoryDiagnoser] [MemoryDiagnoser]
[MinIterationTime(150)]
[MaxWarmupCount(15)]
public class HugeTextLayout : IDisposable public class HugeTextLayout : IDisposable
{ {
private readonly IDisposable _app; private readonly IDisposable _app;
@ -44,7 +46,13 @@ Admitting that the possibility of achieving the results of the constructive crit
Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor.
It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory. It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory.
In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias.";
[Params(false, true)]
public bool Wrap { get; set; }
[Params(false, true)]
public bool Trim { get; set; }
[Benchmark] [Benchmark]
public TextLayout BuildTextLayout() => MakeLayout(Text); public TextLayout BuildTextLayout() => MakeLayout(Text);
@ -91,9 +99,12 @@ In respect that the structure of the sufficient amount poses problems and challe
} }
} }
private static TextLayout MakeLayout(string str) private TextLayout MakeLayout(string str)
{ {
var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120); var wrapping = Wrap ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap;
var trimming = Trim ? TextTrimming.CharacterEllipsis : TextTrimming.None;
var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120,
textTrimming: trimming, textWrapping: wrapping);
layout.Dispose(); layout.Dispose();
return layout; return layout;
} }

10
tests/Avalonia.RenderTests/Media/GlyphRunTests.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Documents;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Xunit; using Xunit;
#if AVALONIA_SKIA #if AVALONIA_SKIA
@ -170,11 +171,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media
var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[0]) * scale; var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[0]) * scale;
var advances = new[] { advance, advance, advance}; var glyphInfos = new[]
{
new GlyphInfo(glyphIndices[0], 0, advance),
new GlyphInfo(glyphIndices[1], 1, advance),
new GlyphInfo(glyphIndices[2], 2, advance)
};
var characters = new[] { 'A', 'B', 'C' }; var characters = new[] { 'A', 'B', 'C' };
GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices, advances); GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphInfos);
} }
public GlyphRun GlyphRun { get; } public GlyphRun GlyphRun { get; }

21
tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs

@ -134,11 +134,11 @@ namespace Avalonia.Skia.UnitTests.Media
foreach (var rect in rects) foreach (var rect in rects)
{ {
var currentCluster = glyphRun.GlyphClusters[index]; var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphClusters.Count) while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphInfos.Count)
{ {
currentCluster = glyphRun.GlyphClusters[++index]; currentCluster = glyphRun.GlyphInfos[++index].GlyphCluster;
} }
//Non trailing edge //Non trailing edge
@ -161,15 +161,15 @@ namespace Avalonia.Skia.UnitTests.Media
var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace; var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace;
var rects = new List<Rect>(glyphRun.GlyphAdvances!.Count); var rects = new List<Rect>(glyphRun.GlyphInfos!.Count);
var lastCluster = -1; var lastCluster = -1;
for (var index = 0; index < glyphRun.GlyphAdvances.Count; index++) for (var index = 0; index < glyphRun.GlyphInfos.Count; index++)
{ {
var currentCluster = glyphRun.GlyphClusters![index]; var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster;
var advance = glyphRun.GlyphAdvances[index]; var advance = glyphRun.GlyphInfos[index].GlyphAdvance;
if (lastCluster != currentCluster) if (lastCluster != currentCluster)
{ {
@ -216,10 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media
shapedBuffer.GlyphTypeface, shapedBuffer.GlyphTypeface,
shapedBuffer.FontRenderingEmSize, shapedBuffer.FontRenderingEmSize,
shapedBuffer.Text, shapedBuffer.Text,
shapedBuffer.GlyphIndices, shapedBuffer.GlyphInfos,
shapedBuffer.GlyphAdvances,
shapedBuffer.GlyphOffsets,
shapedBuffer.GlyphClusters,
shapedBuffer.BidiLevel); shapedBuffer.BidiLevel);
if(shapedBuffer.BidiLevel == 1) if(shapedBuffer.BidiLevel == 1)
@ -233,7 +230,7 @@ namespace Avalonia.Skia.UnitTests.Media
private static IDisposable Start() private static IDisposable Start()
{ {
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null), .With(renderInterface: new PlatformRenderInterface(),
textShaperImpl: new TextShaperImpl(), textShaperImpl: new TextShaperImpl(),
fontManagerImpl: new CustomFontManagerImpl())); fontManagerImpl: new CustomFontManagerImpl()));

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

@ -585,7 +585,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var expectedRuns = expectedTextLine.TextRuns.Cast<ShapedTextRun>().ToList(); var expectedRuns = expectedTextLine.TextRuns.Cast<ShapedTextRun>().ToList();
var expectedGlyphs = expectedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); var expectedGlyphs = expectedRuns
.SelectMany(run => run.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex)
.ToList();
for (var i = 0; i < text.Length; i++) for (var i = 0; i < text.Length; i++)
{ {
@ -604,7 +606,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList(); var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); var actualGlyphs = shapedRuns
.SelectMany(x => x.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex)
.ToList();
Assert.Equal(expectedGlyphs, actualGlyphs); Assert.Equal(expectedGlyphs, actualGlyphs);
} }
@ -706,7 +710,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public static IDisposable Start() public static IDisposable Start()
{ {
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null), .With(renderInterface: new PlatformRenderInterface(),
textShaperImpl: new TextShaperImpl())); textShaperImpl: new TextShaperImpl()));
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable

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

@ -142,8 +142,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
black, black,
textWrapping: TextWrapping.Wrap); textWrapping: TextWrapping.Wrap);
var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextRun>() var expectedGlyphs = GetGlyphs(expected);
.SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
var outer = new GraphemeEnumerator(text); var outer = new GraphemeEnumerator(text);
var inner = new GraphemeEnumerator(text); var inner = new GraphemeEnumerator(text);
@ -175,8 +174,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textWrapping: TextWrapping.Wrap, textWrapping: TextWrapping.Wrap,
textStyleOverrides: spans); textStyleOverrides: spans);
var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextRun>() var actualGlyphs = GetGlyphs(actual);
.SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count); Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count);
@ -196,6 +194,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
i += outer.Current.Text.Length; i += outer.Current.Text.Length;
} }
} }
static List<string> GetGlyphs(TextLayout textLayout)
=> textLayout.TextLines
.Select(line => string.Join('|', line.TextRuns
.Cast<ShapedTextRun>()
.SelectMany(run => run.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphIndex)))
.ToList();
} }
[Fact] [Fact]
@ -484,13 +489,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
var shapedRun = (ShapedTextRun)textRun; var shapedRun = (ShapedTextRun)textRun;
var glyphClusters = shapedRun.ShapedBuffer.GlyphClusters; var glyphClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToArray();
var expected = clusters.Skip(index).Take(glyphClusters.Count).ToArray(); var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray();
Assert.Equal(expected, glyphClusters); Assert.Equal(expected, glyphClusters);
index += glyphClusters.Count; index += glyphClusters.Length;
} }
} }
} }
@ -515,13 +520,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(1, layout.TextLines[0].TextRuns.Count); Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count); Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphInfos.Count);
Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]); Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[5].GlyphCluster);
if (expectedLength == 7) if (expectedLength == 7)
{ {
Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]); Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[6].GlyphCluster);
} }
} }
} }
@ -562,9 +567,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint);
foreach (var glyph in textRun.GlyphRun.GlyphIndices) foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos)
{ {
Assert.Equal(replacementGlyph, glyph); Assert.Equal(replacementGlyph, glyphInfo.GlyphIndex);
} }
} }
} }
@ -776,8 +781,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width);
} }
var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast<ShapedTextRun>()) var rects = layout.TextLines
.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray(); .SelectMany(x => x.TextRuns.Cast<ShapedTextRun>())
.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance)
.ToArray();
for (var i = 0; i < SingleLineText.Length; i++) for (var i = 0; i < SingleLineText.Length; i++)
{ {
@ -865,10 +872,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var currentX = 0.0; var currentX = 0.0;
for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++) for (var i = 0; i < firstRun.GlyphRun.GlyphInfos.Count; i++)
{ {
var cluster = firstRun.GlyphRun.GlyphClusters[i]; var cluster = firstRun.GlyphRun.GlyphInfos[i].GlyphCluster;
var advance = firstRun.GlyphRun.GlyphAdvances[i]; var advance = firstRun.GlyphRun.GlyphInfos[i].GlyphAdvance;
hit = layout.HitTestPoint(new Point(currentX, 0)); hit = layout.HitTestPoint(new Point(currentX, 0));
@ -895,10 +902,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
currentX = firstRun.Size.Width + 0.5; currentX = firstRun.Size.Width + 0.5;
for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++) for (var i = 0; i < secondRun.GlyphRun.GlyphInfos.Count; i++)
{ {
var cluster = secondRun.GlyphRun.GlyphClusters[i]; var cluster = secondRun.GlyphRun.GlyphInfos[i].GlyphCluster;
var advance = secondRun.GlyphRun.GlyphAdvances[i]; var advance = secondRun.GlyphRun.GlyphInfos[i].GlyphAdvance;
hit = layout.HitTestPoint(new Point(currentX, 0)); hit = layout.HitTestPoint(new Point(currentX, 0));
@ -932,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var firstRun = (ShapedTextRun)textLine.TextRuns[0]; var firstRun = (ShapedTextRun)textLine.TextRuns[0];
var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var characterHit = textLine.GetCharacterHitFromDistance(0); var characterHit = textLine.GetCharacterHitFromDistance(0);
@ -946,7 +953,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0]; var firstAdvance = firstRun.ShapedBuffer.GlyphInfos[0].GlyphAdvance;
Assert.Equal(firstAdvance, distance, 5); Assert.Equal(firstAdvance, distance, 5);
@ -991,9 +998,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList(); var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList(); var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster).ToList();
var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList(); var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance).ToList();
var currentX = 0.0; var currentX = 0.0;

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

@ -95,9 +95,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
var shapedRun = (ShapedTextRun)textRun; var shapedRun = (ShapedTextRun)textRun;
clusters.AddRange(shapedRun.IsReversed ? var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster);
shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
shapedRun.ShapedBuffer.GlyphClusters); clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters);
} }
var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
@ -142,9 +142,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
var shapedRun = (ShapedTextRun)textRun; var shapedRun = (ShapedTextRun)textRun;
clusters.AddRange(shapedRun.IsReversed ? var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster);
shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
shapedRun.ShapedBuffer.GlyphClusters); clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters);
} }
clusters.Reverse(); clusters.Reverse();
@ -247,7 +247,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity, formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties)); new GenericTextParagraphProperties(defaultProperties));
var clusters = textLine.TextRuns.Cast<ShapedTextRun>().SelectMany(x => x.ShapedBuffer.GlyphClusters) var clusters = textLine.TextRuns
.Cast<ShapedTextRun>()
.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster)
.ToArray(); .ToArray();
var previousCharacterHit = new CharacterHit(text.Length); var previousCharacterHit = new CharacterHit(text.Length);
@ -313,11 +315,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var glyphRun = textRun.GlyphRun; var glyphRun = textRun.GlyphRun;
for (var i = 0; i < glyphRun.GlyphClusters!.Count; i++) for (var i = 0; i < glyphRun.GlyphInfos.Count; i++)
{ {
var cluster = glyphRun.GlyphClusters[i]; var cluster = glyphRun.GlyphInfos[i].GlyphCluster;
var advance = glyphRun.GlyphAdvances[i]; var advance = glyphRun.GlyphInfos[i].GlyphAdvance;
var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
@ -750,7 +752,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
var shapedBuffer = textRun.ShapedBuffer; var shapedBuffer = textRun.ShapedBuffer;
var currentClusters = shapedBuffer.GlyphClusters.ToList(); var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList();
foreach (var currentCluster in currentClusters) foreach (var currentCluster in currentClusters)
{ {
@ -783,11 +785,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
var shapedBuffer = textRun.ShapedBuffer; var shapedBuffer = textRun.ShapedBuffer;
for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) for (var index = 0; index < shapedBuffer.GlyphInfos.Length; index++)
{ {
var currentCluster = shapedBuffer.GlyphClusters[index]; var currentCluster = shapedBuffer.GlyphInfos[index].GlyphCluster;
var advance = shapedBuffer.GlyphAdvances[index]; var advance = shapedBuffer.GlyphInfos[index].GlyphAdvance;
if (lastCluster != currentCluster) if (lastCluster != currentCluster)
{ {

10
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs

@ -19,10 +19,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var shapedBuffer = TextShaper.Current.ShapeText(text, options); var shapedBuffer = TextShaper.Current.ShapeText(text, options);
Assert.Equal(shapedBuffer.Length, text.Length); Assert.Equal(shapedBuffer.Length, text.Length);
Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); Assert.Equal(shapedBuffer.GlyphInfos.Length, text.Length);
Assert.Equal(0, shapedBuffer.GlyphClusters[0]); Assert.Equal(0, shapedBuffer.GlyphInfos[0].GlyphCluster);
Assert.Equal(1, shapedBuffer.GlyphClusters[1]); Assert.Equal(1, shapedBuffer.GlyphInfos[1].GlyphCluster);
Assert.Equal(1, shapedBuffer.GlyphClusters[2]); Assert.Equal(1, shapedBuffer.GlyphInfos[2].GlyphCluster);
} }
} }
@ -36,7 +36,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var shapedBuffer = TextShaper.Current.ShapeText(text, options); var shapedBuffer = TextShaper.Current.ShapeText(text, options);
Assert.Equal(shapedBuffer.Length, text.Length); Assert.Equal(shapedBuffer.Length, text.Length);
Assert.Equal(100, shapedBuffer.GlyphAdvances[0]); Assert.Equal(100, shapedBuffer.GlyphInfos[0].GlyphAdvance);
} }
} }

2
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@ -161,6 +161,8 @@ namespace Avalonia.UnitTests
if (MemoryMarshal.TryGetArray(memory, out var segment)) if (MemoryMarshal.TryGetArray(memory, out var segment))
{ {
start = segment.Offset;
length = segment.Count;
return segment.Array.AsMemory(); return segment.Array.AsMemory();
} }

3
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -4,6 +4,7 @@ using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Rendering; using Avalonia.Rendering;
using Moq; using Moq;
@ -146,7 +147,7 @@ namespace Avalonia.UnitTests
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double> glyphAdvances, IReadOnlyList<Vector> glyphOffsets) public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos)
{ {
return Mock.Of<IGlyphRunImpl>(); return Mock.Of<IGlyphRunImpl>();
} }

5
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -24,7 +24,10 @@ namespace Avalonia.UnitTests
var glyphIndex = typeface.GetGlyph(codepoint); var glyphIndex = typeface.GetGlyph(codepoint);
shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster, 10); for (var j = 0; j < count; ++j)
{
shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
}
i += count; i += count;
} }

BIN
tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Loading…
Cancel
Save