Browse Source

Pass GlyphInfo directly to GlyphRun

pull/10013/head
Julien Lebosquain 3 years ago
parent
commit
4144be11fe
  1. 518
      src/Avalonia.Base/Media/GlyphRun.cs
  2. 36
      src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs
  3. 2
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  4. 189
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  5. 24
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  6. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  7. 9
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  8. 2
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  9. 3
      src/Avalonia.Base/Utilities/BinarySearchExtension.cs
  10. 3
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  11. 83
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  12. 42
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  13. 14
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  14. 3
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  15. 3
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  16. 10
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  17. 21
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  18. 10
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  19. 55
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  20. 30
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  21. 10
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
  22. 3
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  23. 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.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -11,64 +13,112 @@ namespace Avalonia.Media
/// </summary>
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 IGlyphTypeface _glyphTypeface;
private double _fontRenderingEmSize;
private int _biDiLevel;
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlyMemory<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
private IReadOnlyList<GlyphInfo> _glyphInfos;
private bool _hasOneCharPerCluster; // if true, character index and cluster are similar
/// <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>
/// <param name="glyphTypeface">The glyph typeface.</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="glyphClusters">The glyph clusters.</param>
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="biDiLevel">The bidi level.</param>
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlyMemory<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
IReadOnlyList<int>? glyphClusters = null,
int biDiLevel = 0)
: this(glyphTypeface, fontRenderingEmSize, characters,
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)
{
_glyphTypeface = glyphTypeface;
GlyphTypeface = glyphTypeface;
_fontRenderingEmSize = fontRenderingEmSize;
_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>
/// Gets the <see cref="IGlyphTypeface"/> for the <see cref="GlyphRun"/>.
/// </summary>
public IGlyphTypeface GlyphTypeface => _glyphTypeface;
public IGlyphTypeface GlyphTypeface { get; }
/// <summary>
/// Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
@ -88,56 +138,17 @@ namespace Avalonia.Media
///
/// </summary>
public GlyphRunMetrics Metrics
{
get
{
_glyphRunMetrics ??= CreateGlyphRunMetrics();
return _glyphRunMetrics.Value;
}
}
=> _glyphRunMetrics ??= CreateGlyphRunMetrics();
/// <summary>
/// Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
/// </summary>
public Point BaselineOrigin
{
get
{
_baselineOrigin ??= CalculateBaselineOrigin();
return _baselineOrigin.Value;
}
get => _baselineOrigin ??= CalculateBaselineOrigin();
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>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
@ -148,12 +159,16 @@ namespace Avalonia.Media
}
/// <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>
public IReadOnlyList<int>? GlyphClusters
public IReadOnlyList<GlyphInfo> GlyphInfos
{
get => _glyphClusters;
set => Set(ref _glyphClusters, value);
get => _glyphInfos;
set
{
Set(ref _glyphInfos, value);
_hasOneCharPerCluster = false;
}
}
/// <summary>
@ -179,17 +194,7 @@ namespace Avalonia.Media
/// The platform implementation of the <see cref="GlyphRun"/>.
/// </summary>
public IGlyphRunImpl GlyphRunImpl
{
get
{
if (_glyphRunImpl == null)
{
Initialize();
}
return _glyphRunImpl!;
}
}
=> _glyphRunImpl ??= CreateGlyphRunImpl();
/// <summary>
/// Obtains geometry for the glyph run.
@ -221,38 +226,32 @@ namespace Avalonia.Media
if (IsLeftToRight)
{
if (GlyphClusters != null)
if (characterIndex < Metrics.FirstCluster)
{
if (characterIndex < Metrics.FirstCluster)
{
return 0;
}
return 0;
}
if (characterIndex > Metrics.LastCluster)
{
return Metrics.WidthIncludingTrailingWhitespace;
}
if (characterIndex > Metrics.LastCluster)
{
return Metrics.WidthIncludingTrailingWhitespace;
}
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
var currentCluster = GlyphClusters[glyphIndex];
var currentCluster = _glyphInfos[glyphIndex].GlyphCluster;
//Move to the end of the glyph cluster
if (characterHit.TrailingLength > 0)
//Move to the end of the glyph cluster
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++)
{
distance += GetGlyphAdvance(i, out _);
distance += _glyphInfos[i].GlyphAdvance;
}
return distance;
@ -262,22 +261,19 @@ namespace Avalonia.Media
//RightToLeft
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)
{
return Size.Width;
}
if (characterIndex <= Metrics.FirstCluster)
{
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;
@ -322,11 +318,12 @@ namespace Avalonia.Media
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)
{
@ -340,11 +337,12 @@ namespace Avalonia.Media
{
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;
@ -424,7 +422,7 @@ namespace Avalonia.Media
/// </returns>
public int FindGlyphIndex(int characterIndex)
{
if (GlyphClusters == null || GlyphClusters.Count == 0)
if (_hasOneCharPerCluster)
{
return characterIndex;
}
@ -433,7 +431,7 @@ namespace Avalonia.Media
{
if (IsLeftToRight)
{
return GlyphIndices.Count - 1;
return _glyphInfos.Count - 1;
}
return 0;
@ -446,15 +444,13 @@ namespace Avalonia.Media
return 0;
}
return GlyphIndices.Count - 1;
return _glyphInfos.Count - 1;
}
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
var clusters = GlyphClusters;
var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer;
// 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.
if (start < 0)
@ -463,40 +459,38 @@ namespace Avalonia.Media
{
characterIndex--;
start = clusters.BinarySearch(characterIndex, comparer);
start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer);
}
if (start < 0)
{
goto result;
return 0;
}
}
if (IsLeftToRight)
{
while (start > 0 && clusters[start - 1] == clusters[start])
while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster)
{
start--;
}
}
else
{
while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start])
while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster)
{
start++;
}
}
result:
if (start < 0)
{
return 0;
}
if (start > GlyphIndices.Count - 1)
if (start > _glyphInfos.Count - 1)
{
return GlyphIndices.Count - 1;
return _glyphInfos.Count - 1;
}
return start;
@ -516,14 +510,14 @@ namespace Avalonia.Media
var glyphIndex = FindGlyphIndex(index);
if (GlyphClusters == null)
if (_hasOneCharPerCluster)
{
width = GetGlyphAdvance(index, out _);
width = _glyphInfos[index].GlyphAdvance;
return new CharacterHit(glyphIndex, 1);
}
var cluster = GlyphClusters[glyphIndex];
var cluster = _glyphInfos[glyphIndex].GlyphCluster;
var nextCluster = cluster;
@ -531,13 +525,13 @@ namespace Avalonia.Media
while (nextCluster == cluster)
{
width += GetGlyphAdvance(currentIndex, out _);
width += _glyphInfos[currentIndex].GlyphAdvance;
if (IsLeftToRight)
{
currentIndex++;
if (currentIndex == GlyphClusters.Count)
if (currentIndex == _glyphInfos.Count)
{
break;
}
@ -552,7 +546,7 @@ namespace Avalonia.Media
}
}
nextCluster = GlyphClusters[currentIndex];
nextCluster = _glyphInfos[currentIndex].GlyphCluster;
}
var clusterLength = Math.Max(0, nextCluster - cluster);
@ -565,9 +559,9 @@ namespace Avalonia.Media
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)
{
@ -583,9 +577,9 @@ namespace Avalonia.Media
}
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)
{
@ -613,26 +607,6 @@ namespace Avalonia.Media
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>
/// Calculates the default baseline origin of the <see cref="GlyphRun"/>.
/// </summary>
@ -644,20 +618,17 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics()
{
int firstCluster = 0, lastCluster = 0;
int firstCluster, lastCluster;
if (_glyphClusters != null && _glyphClusters.Count > 0)
if (Characters.IsEmpty)
{
firstCluster = _glyphClusters[0];
lastCluster = _glyphClusters[_glyphClusters.Count - 1];
firstCluster = 0;
lastCluster = 0;
}
else
{
if (!Characters.IsEmpty)
{
firstCluster = 0;
lastCluster = Characters.Length - 1;
}
firstCluster = _glyphInfos[0].GlyphCluster;
lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster;
}
if (!IsLeftToRight)
@ -671,9 +642,9 @@ namespace Avalonia.Media
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;
}
@ -684,14 +655,14 @@ namespace Avalonia.Media
{
for (var index = 0; index < glyphCount; index++)
{
width -= GetGlyphAdvance(index, out _);
width -= _glyphInfos[index].GlyphAdvance;
}
}
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)
{
return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
}
glyphCount = 0;
@ -720,84 +691,59 @@ namespace Avalonia.Media
if (!charactersSpan.IsEmpty)
{
if (GlyphClusters == null)
{
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++;
var characterIndex = charactersSpan.Length - 1;
i -= count;
glyphCount++;
}
}
else
for (var i = _glyphInfos.Count - 1; i >= 0; i--)
{
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--)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex -= characterLength;
characterIndex -= characterLength;
if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
var clusterLength = 1;
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++;
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
characterIndex -= characterLength;
}
continue;
characterIndex -= characterLength;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
continue;
}
trailingWhitespaceLength += clusterLength;
break;
}
glyphCount++;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
return trailingWhitespaceLength;
}
private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
{
glyphCount = 0;
newLineLength = 0;
@ -806,71 +752,46 @@ namespace Avalonia.Media
if (!charactersSpan.IsEmpty)
{
if (GlyphClusters == null)
{
for (var i = 0; i < charactersSpan.Length;)
{
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
var characterIndex = 0;
if (!codepoint.IsWhiteSpace)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i += count;
glyphCount++;
}
}
else
for (var i = 0; i < _glyphInfos.Count; i++)
{
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++)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex += characterLength;
characterIndex += characterLength;
if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
var clusterLength = 1;
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];
if (currentCluster == nextCluster)
{
clusterLength++;
clusterLength++;
continue;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
continue;
}
trailingWhitespaceLength += clusterLength;
break;
}
glyphCount += clusterLength;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount += clusterLength;
}
}
@ -890,44 +811,17 @@ namespace Avalonia.Media
field = value;
}
/// <summary>
/// Initializes the <see cref="GlyphRun"/>.
/// </summary>
private void Initialize()
private IGlyphRunImpl CreateGlyphRunImpl()
{
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>();
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos);
}
public void Dispose()
{
_glyphRunImpl?.Dispose();
}
private class ReverseComparer<T> : IComparer<T>
{
public int Compare(T? x, T? y)
{
return Comparer<T>.Default.Compare(y, x);
}
_glyphRunImpl = null;
}
}
}

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;
}
}

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

@ -111,7 +111,7 @@ namespace Avalonia.Media.TextFormatting
shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing);
}
glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances;
glyphRun.GlyphInfos = shapedBuffer.GlyphInfos;
}
currentPosition += textRun.Length;

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

@ -1,14 +1,13 @@
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Utilities;
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;
public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting
public sbyte BidiLevel { get; }
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; }
@ -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)
{
@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting
{
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)
@ -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--;
}
@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult<ShapedBuffer>(this, null);
}
var firstCluster = GlyphClusters[0];
var lastCluster = GlyphClusters[GlyphClusters.Count - 1];
var firstCluster = GlyphInfos[0].GlyphCluster;
var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster;
var start = firstCluster < lastCluster ? firstCluster : lastCluster;
@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult<ShapedBuffer>(first, second);
}
int ICollection<GlyphInfo>.Count => throw new NotImplementedException();
bool ICollection<GlyphInfo>.IsReadOnly => true;
int IReadOnlyCollection<GlyphInfo>.Count => GlyphInfos.Length;
public GlyphInfo this[int index]
{
@ -144,130 +133,9 @@ namespace Avalonia.Media.TextFormatting
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();
System.Collections.IEnumerator System.Collections.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();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
@ -279,35 +147,4 @@ namespace Avalonia.Media.TextFormatting
}
}
}
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 GlyphRun GlyphRun
{
get
{
if(_glyphRun is null)
{
_glyphRun = CreateGlyphRun();
}
return _glyphRun;
}
}
public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun();
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin)
{
using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin)))
{
if (GlyphRun.GlyphIndices.Count == 0)
if (GlyphRun.GlyphInfos.Count == 0)
{
return;
}
@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < ShapedBuffer.Length; i++)
{
var advance = ShapedBuffer.GlyphAdvances[i];
var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
if (currentWidth + advance > availableWidth)
{
@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting
for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{
var advance = ShapedBuffer.GlyphAdvances[i];
var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
if (width + advance > availableWidth)
{
@ -195,10 +184,7 @@ namespace Avalonia.Media.TextFormatting
ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
Text,
ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,
ShapedBuffer.GlyphClusters,
ShapedBuffer,
BidiLevel);
}

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

@ -611,7 +611,7 @@ namespace Avalonia.Media.TextFormatting
var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
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,
(sbyte)flowDirection);

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
namespace Avalonia.Platform
@ -166,11 +167,9 @@ namespace Avalonia.Platform
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The font 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>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
/// <param name="glyphInfos">The list of glyphs.</param>
/// <returns>An <see cref="IGlyphRunImpl"/>.</returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos);
/// <summary>
/// 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 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 });
}
}

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

@ -14,7 +14,6 @@
// under the License.
// Copied from: https://github.com/toptensoftware/RichTextKit
using System;
using System.Collections.Generic;
namespace Avalonia.Utilities
@ -39,7 +38,7 @@ namespace Avalonia.Utilities
/// <param name="value">The value to search for</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>
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);
}

3
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -9,6 +9,7 @@ using Avalonia.Rendering;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Headless
{
@ -118,7 +119,7 @@ namespace Avalonia.Headless
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();
}

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

@ -1,16 +1,11 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Imaging;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using SkiaSharp;
namespace Avalonia.Skia
@ -88,9 +83,9 @@ namespace Avalonia.Skia
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);
if (!glyphPath.IsEmpty)
@ -98,14 +93,7 @@ namespace Avalonia.Skia
path.AddPath(glyphPath, (float)currentX, (float)currentY);
}
if (glyphRun.GlyphAdvances != null)
{
currentX += glyphRun.GlyphAdvances[i];
}
else
{
currentX += glyphPath.Bounds.Right;
}
currentX += glyphRun.GlyphInfos[i].GlyphAdvance;
}
return new StreamGeometryImpl(path);
@ -213,17 +201,16 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format, alphaFormat);
}
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)
{
if (glyphTypeface == null)
{
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;
@ -242,59 +229,25 @@ namespace Avalonia.Skia
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 positionSpan = runBuffer.GetPositionSpan();
var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan();
var currentX = 0.0;
var currentX = 0.0;
for (int i = 0; i < glyphOffsets.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
for (int i = 0; i < count; i++)
{
if (glyphAdvances != null)
{
var runBuffer = builder.AllocateHorizontalRun(font, count, 0);
var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan();
var currentX = 0.0;
var glyphInfo = glyphInfos[i];
var offset = glyphInfo.GlyphOffset;
for (int i = 0; i < glyphAdvances.Count; i++)
{
glyphSpan[i] = glyphIndices[i];
glyphSpan[i] = glyphInfo.GlyphIndex;
positionSpan[i] = (float)currentX;
positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y);
currentX += glyphAdvances[i];
}
}
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];
}
}
currentX += glyphInfo.GlyphAdvance;
}
return new GlyphRunImpl(builder.Build());

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

@ -7,6 +7,7 @@ using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using SharpDX.DirectWrite;
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 CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
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)
{
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
var glyphCount = glyphIndices.Count;
var glyphCount = glyphInfos.Count;
var run = new SharpDX.DirectWrite.GlyphRun
{
@ -174,44 +174,23 @@ namespace Avalonia.Direct2D1
for (var i = 0; i < glyphCount; i++)
{
indices[i] = (short)glyphIndices[i];
indices[i] = (short)glyphInfos[i].GlyphIndex;
}
run.Indices = indices;
run.Advances = new float[glyphCount];
var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight);
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)
for (var i = 0; i < glyphCount; i++)
{
return new GlyphRunImpl(run);
run.Advances[i] = (float)glyphInfos[i].GlyphAdvance;
}
run.Offsets = new GlyphOffset[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
var (x, y) = glyphOffsets[i];
var (x, y) = glyphInfos[i].GlyphOffset;
run.Offsets[i] = new GlyphOffset
{
@ -254,11 +233,12 @@ namespace Avalonia.Direct2D1
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);

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.TextFormatting;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Utilities;
@ -179,12 +181,14 @@ namespace Avalonia.Base.UnitTests.Media
private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0)
{
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,
glyphClusters: glyphClusters, biDiLevel: bidiLevel);
return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel);
}
}
}

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

@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Base.UnitTests.VisualTree
{
@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
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();
}

3
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -5,6 +5,7 @@ using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Microsoft.Diagnostics.Runtime;
namespace Avalonia.Benchmarks
@ -120,7 +121,7 @@ namespace Avalonia.Benchmarks
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();
}

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

@ -5,6 +5,7 @@ using Avalonia.Controls.Documents;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Xunit;
#if AVALONIA_SKIA
@ -170,11 +171,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media
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' };
GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices, advances);
GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphInfos);
}
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)
{
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
@ -161,15 +161,15 @@ namespace Avalonia.Skia.UnitTests.Media
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;
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)
{
@ -216,10 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media
shapedBuffer.GlyphTypeface,
shapedBuffer.FontRenderingEmSize,
shapedBuffer.Text,
shapedBuffer.GlyphIndices,
shapedBuffer.GlyphAdvances,
shapedBuffer.GlyphOffsets,
shapedBuffer.GlyphClusters,
shapedBuffer.GlyphInfos,
shapedBuffer.BidiLevel);
if(shapedBuffer.BidiLevel == 1)
@ -233,7 +230,7 @@ namespace Avalonia.Skia.UnitTests.Media
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null),
.With(renderInterface: new PlatformRenderInterface(),
textShaperImpl: new TextShaperImpl(),
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 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++)
{
@ -604,7 +606,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
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);
}
@ -706,7 +710,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null),
.With(renderInterface: new PlatformRenderInterface(),
textShaperImpl: new TextShaperImpl()));
AvaloniaLocator.CurrentMutable

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

@ -142,8 +142,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
black,
textWrapping: TextWrapping.Wrap);
var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextRun>()
.SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
var expectedGlyphs = GetGlyphs(expected);
var outer = new GraphemeEnumerator(text);
var inner = new GraphemeEnumerator(text);
@ -175,8 +174,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
textWrapping: TextWrapping.Wrap,
textStyleOverrides: spans);
var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextRun>()
.SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
var actualGlyphs = GetGlyphs(actual);
Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count);
@ -196,6 +194,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
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]
@ -484,13 +489,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
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);
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(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)
{
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);
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);
}
var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast<ShapedTextRun>())
.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray();
var rects = layout.TextLines
.SelectMany(x => x.TextRuns.Cast<ShapedTextRun>())
.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance)
.ToArray();
for (var i = 0; i < SingleLineText.Length; i++)
{
@ -865,10 +872,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
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 advance = firstRun.GlyphRun.GlyphAdvances[i];
var cluster = firstRun.GlyphRun.GlyphInfos[i].GlyphCluster;
var advance = firstRun.GlyphRun.GlyphInfos[i].GlyphAdvance;
hit = layout.HitTestPoint(new Point(currentX, 0));
@ -895,10 +902,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
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 advance = secondRun.GlyphRun.GlyphAdvances[i];
var cluster = secondRun.GlyphRun.GlyphInfos[i].GlyphCluster;
var advance = secondRun.GlyphRun.GlyphInfos[i].GlyphAdvance;
hit = layout.HitTestPoint(new Point(currentX, 0));
@ -932,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var firstRun = (ShapedTextRun)textLine.TextRuns[0];
var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0];
var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var characterHit = textLine.GetCharacterHitFromDistance(0);
@ -946,7 +953,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
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);
@ -991,9 +998,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
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;

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

@ -95,9 +95,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var shapedRun = (ShapedTextRun)textRun;
clusters.AddRange(shapedRun.IsReversed ?
shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
shapedRun.ShapedBuffer.GlyphClusters);
var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster);
clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters);
}
var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
@ -142,9 +142,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var shapedRun = (ShapedTextRun)textRun;
clusters.AddRange(shapedRun.IsReversed ?
shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
shapedRun.ShapedBuffer.GlyphClusters);
var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster);
clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters);
}
clusters.Reverse();
@ -247,7 +247,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
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();
var previousCharacterHit = new CharacterHit(text.Length);
@ -313,11 +315,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
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));
@ -750,7 +752,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var shapedBuffer = textRun.ShapedBuffer;
var currentClusters = shapedBuffer.GlyphClusters.ToList();
var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList();
foreach (var currentCluster in currentClusters)
{
@ -783,11 +785,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
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)
{

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

3
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -4,6 +4,7 @@ using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Rendering;
using Moq;
@ -146,7 +147,7 @@ namespace Avalonia.UnitTests
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>();
}

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