Browse Source

Used Memory/Span instead of CharacterBufferReference/Range

+ GlyphRun uses ReadOnlyMemory for characters
pull/9982/head
Julien Lebosquain 3 years ago
parent
commit
dec8c1c586
  1. 7
      src/Avalonia.Base/Media/FormattedText.cs
  2. 93
      src/Avalonia.Base/Media/GlyphRun.cs
  3. 275
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  4. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  5. 14
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  6. 6
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  7. 24
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  8. 16
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  9. 123
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  10. 4
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  11. 153
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  12. 21
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  13. 6
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  14. 2
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  15. 67
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  16. 6
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  17. 14
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  18. 14
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  19. 13
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  20. 44
      src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs
  21. 6
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  22. 15
      src/Avalonia.Controls/TextBlock.cs
  23. 6
      src/Avalonia.Controls/TextBox.cs
  24. 6
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  25. 6
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  26. 33
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  27. 40
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  28. 2
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs
  29. 15
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
  30. 8
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs
  31. 2
      tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs
  32. 12
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  33. 11
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
  34. 4
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  35. 38
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  36. 9
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  37. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
  38. 69
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  39. 14
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs
  40. 15
      tests/Avalonia.UnitTests/TextTestHelper.cs

7
src/Avalonia.Base/Media/FormattedText.cs

@ -1610,12 +1610,9 @@ namespace Avalonia.Media
var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);
var text = _that._text.AsMemory(textSourceCharacterIndex, thatFormatRider.Length);
TextRunProperties properties = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
var textCharacters = new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length,
properties);
return textCharacters;
return new TextCharacters(text, properties);
}
}
}

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

@ -21,7 +21,7 @@ namespace Avalonia.Media
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private IReadOnlyList<char> _characters;
private ReadOnlyMemory<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
@ -41,7 +41,7 @@ namespace Avalonia.Media
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
IReadOnlyList<char> characters,
ReadOnlyMemory<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
@ -141,7 +141,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public IReadOnlyList<char> Characters
public ReadOnlyMemory<char> Characters
{
get => _characters;
set => Set(ref _characters, value);
@ -600,9 +600,9 @@ namespace Avalonia.Media
}
}
if (Characters != null)
if (!Characters.IsEmpty)
{
clusterLength = Characters.Count - characterLength;
clusterLength = Characters.Length - characterLength;
}
else
{
@ -653,10 +653,10 @@ namespace Avalonia.Media
}
else
{
if (Characters != null && Characters.Count > 0)
if (!Characters.IsEmpty)
{
firstCluster = 0;
lastCluster = Characters.Count - 1;
lastCluster = Characters.Length - 1;
}
}
@ -716,14 +716,15 @@ namespace Avalonia.Media
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0;
var charactersSpan = _characters.Span;
if (Characters != null)
if (!charactersSpan.IsEmpty)
{
if (GlyphClusters == null)
{
for (var i = _characters.Count - 1; i >= 0;)
for (var i = charactersSpan.Length - 1; i >= 0;)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace)
{
@ -743,55 +744,52 @@ namespace Avalonia.Media
}
else
{
if (Characters.Count > 0)
var characterIndex = charactersSpan.Length - 1;
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var characterIndex = Characters.Count - 1;
var currentCluster = GlyphClusters[i];
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(_characters, 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 = GlyphClusters[i - 1];
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(_characters, 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++;
}
}
}
@ -804,14 +802,15 @@ namespace Avalonia.Media
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0;
var charactersSpan = Characters.Span;
if (Characters != null)
if (!charactersSpan.IsEmpty)
{
if (GlyphClusters == null)
{
for (var i = 0; i < Characters.Count;)
for (var i = 0; i < charactersSpan.Length;)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace)
{
@ -836,7 +835,7 @@ namespace Avalonia.Media
for (var i = 0; i < GlyphClusters.Count; i++)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex += characterLength;

275
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@ -1,275 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
public readonly struct CharacterBufferRange : IReadOnlyList<char>
{
/// <summary>
/// Getting an empty character string
/// </summary>
public static CharacterBufferRange Empty => new CharacterBufferRange();
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
char[] characterArray,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
string characterString,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterString, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
/// </summary>
/// <param name="characterBufferReference">character buffer reference</param>
/// <param name="characterLength">number of characters</param>
public CharacterBufferRange(
CharacterBufferReference characterBufferReference,
int characterLength
)
{
if (characterLength < 0)
{
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative");
}
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ?
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar :
0;
if (characterLength > maxLength)
{
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}");
}
CharacterBufferReference = characterBufferReference;
Length = characterLength;
}
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
/// </summary>
internal CharacterBufferRange(
CharacterBufferRange characterBufferRange,
int offsetToFirstChar,
int characterLength
) :
this(
characterBufferRange.CharacterBuffer,
characterBufferRange.OffsetToFirstChar + offsetToFirstChar,
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from string
/// </summary>
internal CharacterBufferRange(
string charString
) :
this(
charString,
0,
charString.Length
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
/// </summary>
internal CharacterBufferRange(
ReadOnlyMemory<char> charBuffer,
int offsetToFirstChar,
int characterLength
) :
this(
new CharacterBufferReference(charBuffer, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
/// </summary>
internal CharacterBufferRange(TextRun textRun)
{
CharacterBufferReference = textRun.CharacterBufferReference;
Length = textRun.Length;
}
public char this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
}
}
/// <summary>
/// Gets a reference to the character buffer
/// </summary>
public CharacterBufferReference CharacterBufferReference { get; }
/// <summary>
/// Gets the number of characters in text source character store
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a span from the character buffer range
/// </summary>
public ReadOnlySpan<char> Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length);
/// <summary>
/// Gets the character memory buffer
/// </summary>
internal ReadOnlyMemory<char> CharacterBuffer => CharacterBufferReference.CharacterBuffer;
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar;
/// <summary>
/// Indicate whether the character buffer range is empty
/// </summary>
internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0;
internal CharacterBufferRange Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new CharacterBufferRange(CharacterBufferReference, length);
}
internal CharacterBufferRange Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if (length == Length)
{
return new CharacterBufferRange(new CharacterBufferReference(), 0);
}
var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length);
return new CharacterBufferRange(characterBufferReference, Length - length);
}
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBufferReference.GetHashCode() ^ Length;
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferRange range)
{
return Equals(range);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferRange
/// </summary>
/// <param name="value"> The CharacterBufferRange value to test </param>
public bool Equals(CharacterBufferRange value)
{
return CharacterBufferReference.Equals(value.CharacterBufferReference)
&& Length == value.Length;
}
/// <summary>
/// Compare two CharacterBufferRange for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferRange for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right)
{
return !(left == right);
}
int IReadOnlyCollection<char>.Count => Length;
public IEnumerator<char> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<char>(this);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

115
src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs

@ -1,115 +0,0 @@
using System;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Text character buffer reference
/// </summary>
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference>
{
/// <summary>
/// Construct character buffer reference from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0)
: this(characterArray.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0)
: this(characterString.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from memory buffer
/// </summary>
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0)
{
if (offsetToFirstChar < 0)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative");
}
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
// even in the case of an empty or null character buffer
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1);
if (offsetToFirstChar > maxOffset)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}");
}
CharacterBuffer = characterBuffer;
OffsetToFirstChar = offsetToFirstChar;
}
/// <summary>
/// Gets the character memory buffer
/// </summary>
public ReadOnlyMemory<char> CharacterBuffer { get; }
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
public int OffsetToFirstChar { get; }
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode();
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test. </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferReference reference)
{
return Equals(reference);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferReference
/// </summary>
/// <param name="value"> The characterBufferReference value to test </param>
public bool Equals(CharacterBufferReference value)
{
return CharacterBuffer.Equals(value.CharacterBuffer);
}
/// <summary>
/// Compare two CharacterBufferReference for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferReference for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right)
{
return !(left == right);
}
}
}

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

@ -7,14 +7,14 @@ namespace Avalonia.Media.TextFormatting
{
internal readonly struct FormattedTextSource : ITextSource
{
private readonly CharacterBufferRange _text;
private readonly string _text;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(string text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
_text = new CharacterBufferRange(text);
_text = text;
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
return null;
}
var runText = _text.Skip(textSourceIndex);
var runText = _text.AsSpan(textSourceIndex);
if (runText.IsEmpty)
{
@ -35,7 +35,7 @@ namespace Avalonia.Media.TextFormatting
var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value);
return new TextCharacters(_text.AsMemory(textSourceIndex, textStyleRun.Length), textStyleRun.Value);
}
/// <summary>
@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex,
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySpan<char> text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting
return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
}
private static int CoerceLength(CharacterBufferRange text, int length)
private static int CoerceLength(ReadOnlySpan<char> text, int length)
{
var finalLength = 0;
@ -132,7 +132,7 @@ namespace Avalonia.Media.TextFormatting
{
var grapheme = graphemeEnumerator.Current;
finalLength += grapheme.Length;
finalLength += grapheme.Text.Length;
if (finalLength >= length)
{

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

@ -50,14 +50,14 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in lineImpl.TextRuns)
{
var text = new CharacterBufferRange(textRun);
var text = textRun.Text;
if (text.IsEmpty)
{
continue;
}
var lineBreakEnumerator = new LineBreakEnumerator(text);
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext())
{
@ -84,7 +84,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.CharacterBufferReference.CharacterBuffer;
var text = textRun.Text;
if (text.IsEmpty)
{

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

@ -9,21 +9,21 @@ namespace Avalonia.Media.TextFormatting
{
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
private bool _bufferRented;
public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(characterBufferRange,
new ArraySlice<GlyphInfo>(ArrayPool<GlyphInfo>.Shared.Rent(bufferLength), 0, bufferLength),
glyphTypeface,
fontRenderingEmSize,
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;
Length = bufferLength;
}
internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
CharacterBufferRange = characterBufferRange;
Text = text;
GlyphInfos = glyphInfos;
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
@ -51,7 +51,7 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public CharacterBufferRange CharacterBufferRange { get; }
public ReadOnlyMemory<char> Text { get; }
/// <summary>
/// Finds a glyph index for given character index.
@ -113,7 +113,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
if (CharacterBufferRange.Length == length)
if (Text.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
}
@ -125,10 +125,10 @@ namespace Avalonia.Media.TextFormatting
var glyphCount = FindGlyphIndex(start + length);
var first = new ShapedBuffer(CharacterBufferRange.Take(length),
var first = new ShapedBuffer(Text.Slice(0, length),
GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(CharacterBufferRange.Skip(length),
var second = new ShapedBuffer(Text.Slice(length),
GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);

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

@ -13,8 +13,6 @@ namespace Avalonia.Media.TextFormatting
public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties)
{
ShapedBuffer = shapedBuffer;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
Length = shapedBuffer.CharacterBufferRange.Length;
Properties = properties;
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
}
@ -26,13 +24,15 @@ namespace Avalonia.Media.TextFormatting
public ShapedBuffer ShapedBuffer { get; }
/// <inheritdoc/>
public override CharacterBufferReference CharacterBufferReference { get; }
public override ReadOnlyMemory<char> Text
=> ShapedBuffer.Text;
/// <inheritdoc/>
public override TextRunProperties Properties { get; }
/// <inheritdoc/>
public override int Length { get; }
public override int Length
=> ShapedBuffer.Text.Length;
public TextMetrics TextMetrics { get; }
@ -113,6 +113,7 @@ namespace Avalonia.Media.TextFormatting
{
length = 0;
var currentWidth = 0.0;
var charactersSpan = GlyphRun.Characters.Span;
for (var i = 0; i < ShapedBuffer.Length; i++)
{
@ -123,7 +124,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
Codepoint.ReadAt(charactersSpan, length, out var count);
length += count;
currentWidth += advance;
@ -136,6 +137,7 @@ namespace Avalonia.Media.TextFormatting
{
length = 0;
width = 0;
var charactersSpan = GlyphRun.Characters.Span;
for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{
@ -146,7 +148,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
Codepoint.ReadAt(charactersSpan, length, out var count);
length += count;
width += advance;
@ -192,7 +194,7 @@ namespace Avalonia.Media.TextFormatting
return new GlyphRun(
ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
new CharacterBufferRange(CharacterBufferReference, Length),
Text,
ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,

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

@ -10,82 +10,34 @@ namespace Avalonia.Media.TextFormatting
public class TextCharacters : TextRun
{
/// <summary>
/// Construct a run of text content from character array
/// Constructs a run for text content from a string.
/// </summary>
public TextCharacters(
char[] characterArray,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
TextRunProperties textRunProperties
) :
this(
characterString,
0, // offsetToFirstChar
(characterString == null) ? 0 : characterString.Length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterString, offsetToFirstChar),
length,
textRunProperties
)
{ }
public TextCharacters(string text, TextRunProperties textRunProperties)
: this(text.AsMemory(), textRunProperties)
{
}
/// <summary>
/// Internal constructor of TextContent
/// Constructs a run for text content from a memory region.
/// </summary>
public TextCharacters(
CharacterBufferReference characterBufferReference,
int length,
TextRunProperties textRunProperties
)
public TextCharacters(ReadOnlyMemory<char> text, TextRunProperties textRunProperties)
{
if (length <= 0)
{
throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
}
if (textRunProperties.FontRenderingEmSize <= 0)
{
throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero");
throw new ArgumentOutOfRangeException(nameof(textRunProperties), textRunProperties.FontRenderingEmSize,
$"Invalid {nameof(TextRunProperties.FontRenderingEmSize)}");
}
CharacterBufferReference = characterBufferReference;
Length = length;
Text = text;
Properties = textRunProperties;
}
/// <inheritdoc />
public override int Length { get; }
public override int Length
=> Text.Length;
/// <inheritdoc />
public override CharacterBufferReference CharacterBufferReference { get; }
public override ReadOnlyMemory<char> Text { get; }
/// <inheritdoc />
public override TextRunProperties Properties { get; }
@ -94,17 +46,19 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary>
/// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List<UnshapedTextRun>(2);
var properties = Properties;
while (characterBufferRange.Length > 0)
while (!text.IsEmpty)
{
var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties);
var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun);
characterBufferRange = characterBufferRange.Skip(shapeableRun.Length);
text = text.Slice(shapeableRun.Length);
previousProperties = shapeableRun.Properties;
}
@ -115,45 +69,46 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Creates a shapeable text run with unique properties.
/// </summary>
/// <param name="characterBufferRange">The character buffer range to create text runs from.</param>
/// <param name="text">The characters to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns>
private static UnshapedTextRun CreateShapeableRun(CharacterBufferRange characterBufferRange,
private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> text,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
var textSpan = text.Span;
if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script))
if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
{
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _))
{
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, fallbackCount,
return new UnshapedTextRun(text.Slice(0, fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
if (previousTypeface is not null)
{
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _))
{
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count,
return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
var codepoint = Codepoint.ReplacementCodepoint;
var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count));
var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span);
while (codepointEnumerator.MoveNext())
{
@ -173,10 +128,10 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
@ -185,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(characterBufferRange);
var enumerator = new GraphemeEnumerator(textSpan);
while (enumerator.MoveNext())
{
@ -196,23 +151,23 @@ namespace Avalonia.Media.TextFormatting
break;
}
count += grapheme.Length;
count += grapheme.Text.Length;
}
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel);
}
/// <summary>
/// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary>
/// <param name="characterBufferRange">The character buffer range to shape.</param>
/// <param name="text">The characters to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
internal static bool TryGetShapeableLength(
CharacterBufferRange characterBufferRange,
ReadOnlySpan<char> text,
Typeface typeface,
Typeface? defaultTypeface,
out int length,
@ -221,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
length = 0;
script = Script.Unknown;
if (characterBufferRange.Length == 0)
if (text.IsEmpty)
{
return false;
}
@ -229,7 +184,7 @@ namespace Avalonia.Media.TextFormatting
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(characterBufferRange);
var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext())
{
@ -264,7 +219,7 @@ namespace Avalonia.Media.TextFormatting
}
}
length += currentGrapheme.Length;
length += currentGrapheme.Text.Length;
}
return length > 0;

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

@ -45,9 +45,7 @@ namespace Avalonia.Media.TextFormatting
{
var currentBreakPosition = 0;
var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(text);
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{

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

@ -1,5 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@ -162,17 +164,13 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in textRuns)
{
if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0)
if (textRun.Text.IsEmpty)
{
var characterBuffer = new CharacterBufferReference(new char[textRun.Length]);
biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length));
biDiData.Append(new char[textRun.Length]);
}
else
{
var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
biDiData.Append(text);
biDiData.Append(textRun.Text.Span);
}
}
@ -198,9 +196,7 @@ namespace Avalonia.Media.TextFormatting
case UnshapedTextRun shapeableRun:
{
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
var characterBufferReference = currentRun.CharacterBufferReference;
var length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
var text = shapeableRun.Text;
while (index + 1 < processedRuns.Count)
{
@ -209,23 +205,14 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (shapeableRun.CanShapeTogether(nextRun))
if (shapeableRun.BidiLevel == nextRun.BidiLevel
&& TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
&& CanShapeTogether(shapeableRun.Properties, nextRun.Properties))
{
groupedRuns.Add(nextRun);
length += nextRun.Length;
if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
{
offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
}
characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
index++;
shapeableRun = nextRun;
text = joinedText;
continue;
}
@ -237,7 +224,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
break;
}
@ -253,12 +240,81 @@ namespace Avalonia.Media.TextFormatting
return shapedRuns;
}
/// <summary>
/// Tries to join two potnetially contiguous memory regions.
/// </summary>
/// <param name="x">The first memory region.</param>
/// <param name="y">The second memory region.</param>
/// <param name="joinedMemory">On success, a memory region representing the union of the two regions.</param>
/// <returns>true if the two regions were contigous; false otherwise.</returns>
private static bool TryJoinContiguousMemories(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y,
out ReadOnlyMemory<char> joinedMemory)
{
if (MemoryMarshal.TryGetString(x, out var xString, out var xStart, out var xLength))
{
if (MemoryMarshal.TryGetString(y, out var yString, out var yStart, out var yLength)
&& ReferenceEquals(xString, yString)
&& TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart))
{
joinedMemory = xString.AsMemory(joinedStart, xLength + yLength);
return true;
}
}
else if (MemoryMarshal.TryGetArray(x, out var xSegment))
{
if (MemoryMarshal.TryGetArray(y, out var ySegment)
&& ReferenceEquals(xSegment.Array, ySegment.Array)
&& TryGetContiguousStart(xSegment.Offset, xSegment.Count, ySegment.Offset, ySegment.Count, out var joinedStart))
{
joinedMemory = xSegment.Array.AsMemory(joinedStart, xSegment.Count + ySegment.Count);
return true;
}
}
else if (MemoryMarshal.TryGetMemoryManager(x, out MemoryManager<char>? xManager, out xStart, out xLength))
{
if (MemoryMarshal.TryGetMemoryManager(y, out MemoryManager<char>? yManager, out var yStart, out var yLength)
&& ReferenceEquals(xManager, yManager)
&& TryGetContiguousStart(xStart, xLength, yStart, yLength, out var joinedStart))
{
joinedMemory = xManager.Memory.Slice(joinedStart, xLength + yLength);
return true;
}
}
joinedMemory = default;
return false;
static bool TryGetContiguousStart(int xStart, int xLength, int yStart, int yLength, out int joinedStart)
{
var xRange = (Start: xStart, Length: xLength);
var yRange = (Start: yStart, Length: yLength);
var (firstRange, secondRange) = xStart <= yStart ? (xRange, yRange) : (yRange, xRange);
if (firstRange.Start + firstRange.Length == secondRange.Start)
{
joinedStart = firstRange.Start;
return true;
}
joinedStart = default;
return false;
}
}
private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y)
=> MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize)
&& x.Typeface == y.Typeface
&& x.BaselineAlignment == y.BaselineAlignment;
private static IReadOnlyList<ShapedTextRun> ShapeTogether(
IReadOnlyList<UnshapedTextRun> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, TextShaperOptions options)
{
var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
@ -294,7 +350,7 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
CharacterBufferRange runText = default;
ReadOnlyMemory<char> runText = default;
for (var i = 0; i < textCharacters.Count; i++)
{
@ -312,11 +368,12 @@ namespace Avalonia.Media.TextFormatting
continue;
}
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
runText = currentRun.Text;
var runTextSpan = runText.Span;
for (; j < runText.Length;)
for (; j < runTextSpan.Length;)
{
Codepoint.ReadAt(runText, j, out var count);
Codepoint.ReadAt(runTextSpan, j, out var count);
if (levelIndex + 1 == levels.Length)
{
@ -326,9 +383,9 @@ namespace Avalonia.Media.TextFormatting
levelIndex++;
j += count;
if (j == runText.Length)
if (j == runTextSpan.Length)
{
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties));
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
runLevel = levels[levelIndex];
@ -341,9 +398,10 @@ namespace Avalonia.Media.TextFormatting
}
// End of this run
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties));
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
runText = runText.Skip(j);
runText = runText.Slice(j);
runTextSpan = runText.Span;
j = 0;
@ -411,7 +469,7 @@ namespace Avalonia.Media.TextFormatting
{
if (TryGetLineBreak(textCharacters, out var runLineBreak))
{
var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap,
var splitResult = new TextCharacters(textCharacters.Text.Slice(0, runLineBreak.PositionWrap),
textCharacters.Properties);
textRuns.Add(splitResult);
@ -442,14 +500,14 @@ namespace Avalonia.Media.TextFormatting
{
lineBreak = default;
if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty)
var text = textRun.Text;
if (text.IsEmpty)
{
return false;
}
var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext())
{
@ -541,8 +599,7 @@ namespace Avalonia.Media.TextFormatting
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) };
@ -589,10 +646,8 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextRun:
{
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(runText);
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (lineBreaker.MoveNext())
{
@ -651,9 +706,7 @@ namespace Avalonia.Media.TextFormatting
currentRun = textRuns[index];
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
lineBreaker = new LineBreakEnumerator(runText);
lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
}
}
else
@ -771,9 +824,7 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
var characterBuffer = textRun.CharacterBufferReference;
var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
return new ShapedTextRun(shapedBuffer, textRun.Properties);
}

21
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting
{
@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text run's text.
/// </summary>
public virtual CharacterBufferReference CharacterBufferReference => default;
public virtual ReadOnlyMemory<char> Text => default;
/// <summary>
/// A set of properties shared by every characters in the run
@ -34,21 +35,7 @@ namespace Avalonia.Media.TextFormatting
_textRun = textRun;
}
public string Text
{
get
{
unsafe
{
var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length);
fixed (char* charsPtr = characterBuffer.Span)
{
return new string(charsPtr, 0, _textRun.Length);
}
}
}
}
public string Text => _textRun.Text.ToString();
public TextRunProperties? Properties => _textRun.Properties;
}

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

@ -40,14 +40,14 @@ namespace Avalonia.Media.TextFormatting
}
/// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default)
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options = default)
{
return _platformImpl.ShapeText(text, length, options);
return _platformImpl.ShapeText(text, options);
}
public ShapedBuffer ShapeText(string text, TextShaperOptions options = default)
{
return ShapeText(new CharacterBufferReference(text), text.Length, options);
return ShapeText(text.AsMemory(), options);
}
}
}

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

@ -64,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Appends text to the bidi data.
/// </summary>
/// <param name="text">The text to process.</param>
public void Append(CharacterBufferRange text)
public void Append(ReadOnlySpan<char> text)
{
_classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length);

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

@ -166,72 +166,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(IReadOnlyList<char> text, int index, out int count)
{
count = 1;
if (index >= text.Count)
{
return ReplacementCodepoint;
}
var code = text[index];
ushort hi, low;
//# High surrogate
if (0xD800 <= code && code <= 0xDBFF)
{
hi = code;
if (index + 1 == text.Count)
{
return ReplacementCodepoint;
}
low = text[index + 1];
if (0xDC00 <= low && low <= 0xDFFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
//# Low surrogate
if (0xDC00 <= code && code <= 0xDFFF)
{
if (index == 0)
{
return ReplacementCodepoint;
}
hi = text[index - 1];
low = code;
if (0xD800 <= hi && hi <= 0xDBFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
return new Codepoint(code);
}
/// <summary>
/// Reads the <see cref="Codepoint"/> at specified position.
/// </summary>
/// <param name="text">The buffer to read from.</param>
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(CharacterBufferRange text, int index, out int count)
public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
{
count = 1;

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

@ -4,9 +4,9 @@ namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct CodepointEnumerator
{
private CharacterBufferRange _text;
private ReadOnlySpan<char> _text;
public CodepointEnumerator(CharacterBufferRange text)
public CodepointEnumerator(ReadOnlySpan<char> text)
{
_text = text;
Current = Codepoint.ReplacementCodepoint;
@ -32,7 +32,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
Current = Codepoint.ReadAt(_text, 0, out var count);
_text = _text.Skip(count);
_text = _text.Slice(count);
return true;
}

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

@ -7,11 +7,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, int offset, int length)
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
{
FirstCodepoint = firstCodepoint;
Offset = offset;
Length = length;
Text = text;
}
/// <summary>
@ -20,13 +19,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; }
/// <summary>
/// The Offset to the FirstCodepoint
/// The text of the grapheme cluster
/// </summary>
public int Offset { get; }
/// <summary>
/// The length of the grapheme cluster
/// </summary>
public int Length { get; }
public ReadOnlySpan<char> Text { get; }
}
}

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

@ -3,16 +3,16 @@
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct GraphemeEnumerator
{
private CharacterBufferRange _text;
private ReadOnlySpan<char> _text;
public GraphemeEnumerator(CharacterBufferRange text)
public GraphemeEnumerator(ReadOnlySpan<char> text)
{
_text = text;
Current = default;
@ -185,9 +185,9 @@ namespace Avalonia.Media.TextFormatting.Unicode
Return:
Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, processor.CurrentCodeUnitOffset);
Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset));
_text = _text.Skip(processor.CurrentCodeUnitOffset);
_text = _text.Slice(processor.CurrentCodeUnitOffset);
return true; // rules GB2, GB999
}
@ -195,10 +195,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
[StructLayout(LayoutKind.Auto)]
private ref struct Processor
{
private readonly CharacterBufferRange _buffer;
private readonly ReadOnlySpan<char> _buffer;
private int _codeUnitLengthOfCurrentScalar;
internal Processor(CharacterBufferRange buffer)
internal Processor(ReadOnlySpan<char> buffer)
{
_buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0;

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

@ -3,7 +3,6 @@
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting.Unicode
{
@ -13,7 +12,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public ref struct LineBreakEnumerator
{
private readonly IReadOnlyList<char> _text;
private readonly ReadOnlySpan<char> _text;
private int _position;
private int _lastPosition;
private LineBreakClass _currentClass;
@ -29,7 +28,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private int _lb30a;
private bool _lb31;
public LineBreakEnumerator(IReadOnlyList<char> text)
public LineBreakEnumerator(ReadOnlySpan<char> text)
: this()
{
_text = text;
@ -63,7 +62,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30a = 0;
}
while (_position < _text.Count)
while (_position < _text.Length)
{
_lastPosition = _position;
var lastClass = _nextClass;
@ -93,11 +92,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
}
}
if (_position >= _text.Count)
if (_position >= _text.Length)
{
if (_lastPosition < _text.Count)
if (_lastPosition < _text.Length)
{
_lastPosition = _text.Count;
_lastPosition = _text.Length;
var required = false;

44
src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs

@ -1,4 +1,4 @@
using Avalonia.Utilities;
using System;
namespace Avalonia.Media.TextFormatting
{
@ -7,52 +7,20 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public sealed class UnshapedTextRun : TextRun
{
public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length,
TextRunProperties properties, sbyte biDiLevel)
public UnshapedTextRun(ReadOnlyMemory<char> text, TextRunProperties properties, sbyte biDiLevel)
{
CharacterBufferReference = characterBufferReference;
Length = length;
Text = text;
Properties = properties;
BidiLevel = biDiLevel;
}
public override int Length { get; }
public override int Length
=> Text.Length;
public override CharacterBufferReference CharacterBufferReference { get; }
public override ReadOnlyMemory<char> Text { get; }
public override TextRunProperties Properties { get; }
public sbyte BidiLevel { get; }
public bool CanShapeTogether(UnshapedTextRun unshapedTextRun)
{
if (!CharacterBufferReference.Equals(unshapedTextRun.CharacterBufferReference))
{
return false;
}
if (BidiLevel != unshapedTextRun.BidiLevel)
{
return false;
}
if (!MathUtilities.AreClose(Properties.FontRenderingEmSize,
unshapedTextRun.Properties.FontRenderingEmSize))
{
return false;
}
if (Properties.Typeface != unshapedTextRun.Properties.Typeface)
{
return false;
}
if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment)
{
return false;
}
return true;
}
}
}

6
src/Avalonia.Base/Platform/ITextShaperImpl.cs

@ -1,4 +1,5 @@
using Avalonia.Media.TextFormatting;
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
namespace Avalonia.Platform
@ -13,9 +14,8 @@ namespace Avalonia.Platform
/// Shapes the specified region within the text and returns a shaped buffer.
/// </summary>
/// <param name="text">The text buffer.</param>
/// <param name="length">The length of text.</param>
/// <param name="options">Text shaper options to customize the shaping process.</param>
/// <returns>A shaped glyph run.</returns>
ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options);
ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options);
}
}

15
src/Avalonia.Controls/TextBlock.cs

@ -826,12 +826,12 @@ namespace Avalonia.Controls
protected readonly record struct SimpleTextSource : ITextSource
{
private readonly CharacterBufferRange _text;
private readonly string _text;
private readonly TextRunProperties _defaultProperties;
public SimpleTextSource(string text, TextRunProperties defaultProperties)
{
_text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length);
_text = text;
_defaultProperties = defaultProperties;
}
@ -842,14 +842,14 @@ namespace Avalonia.Controls
return new TextEndOfParagraph();
}
var runText = _text.Skip(textSourceIndex);
var runText = _text.AsMemory(textSourceIndex);
if (runText.IsEmpty)
{
return new TextEndOfParagraph();
}
return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties);
return new TextCharacters(runText, _defaultProperties);
}
}
@ -884,14 +884,9 @@ namespace Avalonia.Controls
if (textRun is TextCharacters)
{
var characterBufferReference = textRun.CharacterBufferReference;
var skip = Math.Max(0, textSourceIndex - currentPosition);
return new TextCharacters(
new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip),
textRun.Length - skip,
textRun.Properties!);
return new TextCharacters(textRun.Text.Slice(skip), textRun.Properties!);
}
return textRun;

6
src/Avalonia.Controls/TextBox.cs

@ -961,9 +961,7 @@ namespace Avalonia.Controls
var length = 0;
var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length);
var graphemeEnumerator = new GraphemeEnumerator(inputRange);
var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan());
while (graphemeEnumerator.MoveNext())
{
@ -981,7 +979,7 @@ namespace Avalonia.Controls
}
}
length += grapheme.Length;
length += grapheme.Text.Length;
}
if (length < input.Length)

6
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -79,12 +79,10 @@ namespace Avalonia.Controls
{
if(run.Length > 0)
{
var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length);
#if NET6_0
builder.Append(characterBufferRange.Span);
builder.Append(run.Text.Span);
#else
builder.Append(characterBufferRange.Span.ToArray());
builder.Append(run.Text.Span.ToArray());
#endif
}
}

6
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -145,15 +145,13 @@ namespace Avalonia.Headless
class HeadlessTextShaperStub : ITextShaperImpl
{
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options)
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var characterBufferRange = new CharacterBufferRange(text, length);
return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel);
return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
}
}

33
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -1,5 +1,7 @@
using System;
using System.Buffers;
using System.Globalization;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
@ -11,9 +13,9 @@ namespace Avalonia.Skia
{
internal class TextShaperImpl : ITextShaperImpl
{
public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options)
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
var text = new CharacterBufferRange(characterBufferReference, length);
var textSpan = text.Span;
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
@ -21,7 +23,9 @@ namespace Avalonia.Skia
using (var buffer = new Buffer())
{
buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length);
// HarfBuzz needs the surrounding characters to correctly shape the text
var containingText = GetContainingMemory(text, out var start, out var length);
buffer.AddUtf16(containingText.Span, start, length);
MergeBreakPair(buffer);
@ -64,7 +68,7 @@ namespace Avalonia.Skia
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (text[i] == '\t')
if (textSpan[i] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
@ -147,5 +151,26 @@ namespace Avalonia.Skia
// glyphPositions[index].YAdvance * textScale;
return glyphPositions[index].XAdvance * textScale;
}
private static ReadOnlyMemory<char> GetContainingMemory(ReadOnlyMemory<char> memory, out int start, out int length)
{
if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length))
{
return containingString.AsMemory();
}
if (MemoryMarshal.TryGetArray(memory, out var segment))
{
return segment.Array.AsMemory();
}
if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager<char> memoryManager, out start, out length))
{
return memoryManager.Memory;
}
// should never happen
throw new InvalidOperationException("Memory not backed by string, array or manager");
}
}
}

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

@ -1,5 +1,7 @@
using System;
using System.Buffers;
using System.Globalization;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
@ -11,8 +13,9 @@ namespace Avalonia.Direct2D1.Media
{
internal class TextShaperImpl : ITextShaperImpl
{
public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options)
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
var textSpan = text.Span;
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
@ -20,7 +23,9 @@ namespace Avalonia.Direct2D1.Media
using (var buffer = new Buffer())
{
buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length);
// HarfBuzz needs the surrounding characters to correctly shape the text
var containingText = GetContainingMemory(text, out var start, out var length);
buffer.AddUtf16(containingText.Span, start, length);
MergeBreakPair(buffer);
@ -34,7 +39,7 @@ namespace Avalonia.Direct2D1.Media
font.Shape(buffer);
if(buffer.Direction == Direction.RightToLeft)
if (buffer.Direction == Direction.RightToLeft)
{
buffer.Reverse();
}
@ -45,9 +50,7 @@ namespace Avalonia.Direct2D1.Media
var bufferLength = buffer.Length;
var characterBufferRange = new CharacterBufferRange(characterBufferReference, length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var glyphInfos = buffer.GetGlyphInfoSpan();
@ -61,11 +64,11 @@ namespace Avalonia.Direct2D1.Media
var glyphCluster = (int)(sourceInfo.Cluster);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing;
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (characterBufferRange[i] == '\t')
if (textSpan[i] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
@ -148,5 +151,26 @@ namespace Avalonia.Direct2D1.Media
// glyphPositions[index].YAdvance * textScale;
return glyphPositions[index].XAdvance * textScale;
}
private static ReadOnlyMemory<char> GetContainingMemory(ReadOnlyMemory<char> memory, out int start, out int length)
{
if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length))
{
return containingString.AsMemory();
}
if (MemoryMarshal.TryGetArray(memory, out var segment))
{
return segment.Array.AsMemory();
}
if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager<char> memoryManager, out start, out length))
{
return memoryManager.Memory;
}
// should never happen
throw new InvalidOperationException("Memory not backed by string, array or manager");
}
}
}

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

@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.CodePoints).ToArray());
// Append
bidiData.Append(new CharacterBufferRange(text));
bidiData.Append(text);
// Act
for (int i = 0; i < 10; i++)

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

@ -38,18 +38,13 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.Codepoints).ToArray());
var grapheme = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.Grapheme).ToArray()).AsSpan();
var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
var enumerator = new GraphemeEnumerator(text);
enumerator.MoveNext();
var actual = text.AsSpan(enumerator.Current.Offset, enumerator.Current.Length);
var actual = enumerator.Current.Text;
var pass = true;
if(actual.Length != grapheme.Length)
{
pass = false;
}
bool pass = actual.Length == grapheme.Length;
if (pass)
{
@ -87,13 +82,13 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
{
const string text = "ABCDEFGHIJ";
var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
var enumerator = new GraphemeEnumerator(text);
var count = 0;
while (enumerator.MoveNext())
{
Assert.Equal(1, enumerator.Current.Length);
Assert.Equal(1, enumerator.Current.Text.Length);
count++;
}

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

@ -23,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
[Fact]
public void BasicLatinTest()
{
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Hello World\r\nThis is a test."));
var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.");
Assert.True(lineBreaker.MoveNext());
Assert.Equal(6, lineBreaker.Current.PositionWrap);
@ -56,7 +56,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
[Fact]
public void ForwardTextWithOuterWhitespace()
{
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(" Apples Pears Bananas "));
var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas ");
var positionsF = GetBreaks(lineBreaker);
Assert.Equal(1, positionsF[0].PositionWrap);
Assert.Equal(0, positionsF[0].PositionMeasure);
@ -83,7 +83,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
[Fact]
public void ForwardTest()
{
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Apples Pears Bananas"));
var lineBreaker = new LineBreakEnumerator("Apples Pears Bananas");
var positionsF = GetBreaks(lineBreaker);
Assert.Equal(7, positionsF[0].PositionWrap);
@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
{
var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32));
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text));
var lineBreaker = new LineBreakEnumerator(text);
var foundBreaks = new List<int>();

2
tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs

@ -46,7 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.NotNull(target.TextLayout);
var actual = string.Join(null,
target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.CharacterBufferReference.CharacterBuffer.Span.ToString()));
target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.Text.ToString()));
Assert.Equal("****", actual);
}

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

@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
@ -60,7 +60,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
@ -103,7 +103,7 @@ namespace Avalonia.Skia.UnitTests.Media
{
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
var shapedBuffer =
TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
TextShaper.Current.ShapeText(text, options);
var glyphRun = CreateGlyphRun(shapedBuffer);
@ -112,14 +112,14 @@ namespace Avalonia.Skia.UnitTests.Media
var characterHit =
glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _);
Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
else
{
var characterHit =
glyphRun.GetCharacterHitFromDistance(0, out _);
Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var rects = BuildRects(glyphRun);
@ -215,7 +215,7 @@ namespace Avalonia.Skia.UnitTests.Media
var glyphRun = new GlyphRun(
shapedBuffer.GlyphTypeface,
shapedBuffer.FontRenderingEmSize,
shapedBuffer.CharacterBufferRange,
shapedBuffer.Text,
shapedBuffer.GlyphIndices,
shapedBuffer.GlyphAdvances,
shapedBuffer.GlyphOffsets,

11
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs

@ -1,15 +1,16 @@
using Avalonia.Media.TextFormatting;
using System;
using Avalonia.Media.TextFormatting;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
internal class SingleBufferTextSource : ITextSource
{
private readonly CharacterBufferRange _text;
private readonly string _text;
private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties;
public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties)
{
_text = new CharacterBufferRange(text);
_text = text;
_defaultGenericPropertiesRunProperties = defaultProperties;
}
@ -20,14 +21,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return null;
}
var runText = _text.Skip(textSourceIndex);
var runText = _text.AsMemory(textSourceIndex);
if (runText.IsEmpty)
{
return null;
}
return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultGenericPropertiesRunProperties);
return new TextCharacters(runText, _defaultGenericPropertiesRunProperties);
}
}
}

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

@ -279,7 +279,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
using (Start())
{
var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text));
var lineBreaker = new LineBreakEnumerator(text);
var expected = new List<int>();
@ -677,7 +677,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green);
}
return new TextCharacters(_text, 0, _text.Length, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
return new TextCharacters(_text, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
}
}

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

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
@ -62,7 +63,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Length);
var actual = new CharacterBufferRange(textRun).Span.ToString();
var actual = textRun.Text.ToString();
Assert.Equal("1 ", actual);
@ -144,8 +145,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextRun>()
.SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
var outer = new GraphemeEnumerator(new CharacterBufferRange(text));
var inner = new GraphemeEnumerator(new CharacterBufferRange(text));
var outer = new GraphemeEnumerator(text);
var inner = new GraphemeEnumerator(text);
var i = 0;
var j = 0;
@ -153,7 +154,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
while (inner.MoveNext())
{
j += inner.Current.Length;
j += inner.Current.Text.Length;
if (j + i > text.Length)
{
@ -190,9 +191,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
break;
}
inner = new GraphemeEnumerator(new CharacterBufferRange(text));
inner = new GraphemeEnumerator(text);
i += outer.Current.Length;
i += outer.Current.Text.Length;
}
}
}
@ -261,7 +262,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Length);
var actual = new CharacterBufferRange(textRun).Span.ToString();
var actual = textRun.Text.ToString();
Assert.Equal("89", actual);
@ -331,7 +332,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(2, textRun.Length);
var actual = new CharacterBufferRange(textRun).Span.ToString();
var actual = textRun.Text.ToString();
Assert.Equal("😄", actual);
@ -667,10 +668,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(5, layout.TextLines.Count);
Assert.Equal("123\r\n", new CharacterBufferRange(layout.TextLines[0].TextRuns[0]));
Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[1].TextRuns[0]));
Assert.Equal("456\r\n", new CharacterBufferRange(layout.TextLines[2].TextRuns[0]));
Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[3].TextRuns[0]));
Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text.ToString());
Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text.ToString());
Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text.ToString());
Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text.ToString());
}
}
@ -815,8 +816,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.True(textLine.Width <= maxWidth);
var actual = new string(textLine.TextRuns.Cast<ShapedTextRun>()
.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)
.SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray());
.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text))
.SelectMany(x => x.Text.ToString())
.ToArray());
var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
@ -968,15 +970,15 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var i = 0;
var graphemeEnumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
var graphemeEnumerator = new GraphemeEnumerator(text);
while (graphemeEnumerator.MoveNext())
{
var grapheme = graphemeEnumerator.Current;
var textStyleOverrides = new[] { new ValueSpan<TextRunProperties>(i, grapheme.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
var textStyleOverrides = new[] { new ValueSpan<TextRunProperties>(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
i += grapheme.Length;
i += grapheme.Text.Length;
var layout = new TextLayout(
text,
@ -1020,6 +1022,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
private static IDisposable Start()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

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

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
@ -90,7 +91,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List<int>();
foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar))
foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text)))
{
var shapedRun = (ShapedTextRun)textRun;
@ -137,7 +138,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List<int>();
foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar))
foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text)))
{
var shapedRun = (ShapedTextRun)textRun;
@ -410,7 +411,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.True(collapsedLine.HasCollapsed);
var trimmedText = collapsedLine.TextRuns.SelectMany(x => new CharacterBufferRange(x)).ToArray();
var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text.ToString()).ToArray();
Assert.Equal(expected.Length, trimmedText.Length);
@ -653,7 +654,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var run = textRuns[i];
var bounds = runBounds[i];
Assert.Equal(run.CharacterBufferReference.OffsetToFirstChar, bounds.TextSourceCharacterIndex);
Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex);
Assert.Equal(run, bounds.TextRun);
Assert.Equal(run.Size.Width, bounds.Rectangle.Width);
}

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

@ -18,7 +18,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
Assert.Equal(shapedBuffer.CharacterBufferRange.Length, text.Length);
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]);

69
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@ -1,18 +1,21 @@
using System;
using System.Buffers;
using System.Globalization;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
using HarfBuzzSharp;
using Buffer = HarfBuzzSharp.Buffer;
using GlyphInfo = HarfBuzzSharp.GlyphInfo;
namespace Avalonia.UnitTests
{
public class HarfBuzzTextShaperImpl : ITextShaperImpl
internal class HarfBuzzTextShaperImpl : ITextShaperImpl
{
public ShapedBuffer ShapeText(CharacterBufferReference text, int textLength, TextShaperOptions options)
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
var textSpan = text.Span;
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
@ -20,15 +23,17 @@ namespace Avalonia.UnitTests
using (var buffer = new Buffer())
{
buffer.AddUtf16(text.CharacterBuffer.Span, text.OffsetToFirstChar, textLength);
// HarfBuzz needs the surrounding characters to correctly shape the text
var containingText = GetContainingMemory(text, out var start, out var length);
buffer.AddUtf16(containingText.Span, start, length);
MergeBreakPair(buffer);
buffer.GuessSegmentProperties();
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
var font = ((HarfBuzzGlyphTypefaceImpl)typeface).Font;
@ -45,9 +50,7 @@ namespace Avalonia.UnitTests
var bufferLength = buffer.Length;
var characterBufferRange = new CharacterBufferRange(text, textLength);
var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
var glyphInfos = buffer.GetGlyphInfoSpan();
@ -59,12 +62,21 @@ namespace Avalonia.UnitTests
var glyphIndex = (ushort)sourceInfo.Codepoint;
var glyphCluster = (int)sourceInfo.Cluster;
var glyphCluster = (int)(sourceInfo.Cluster);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing;
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (textSpan[i] == '\t')
{
glyphIndex = typeface.GetGlyph(' ');
glyphAdvance = options.IncrementalTabWidth > 0 ?
options.IncrementalTabWidth :
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
}
var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
@ -79,7 +91,7 @@ namespace Avalonia.UnitTests
var length = buffer.Length;
var glyphInfos = buffer.GetGlyphInfoSpan();
var second = glyphInfos[length - 1];
if (!new Codepoint(second.Codepoint).IsBreakChar)
@ -90,19 +102,19 @@ namespace Avalonia.UnitTests
if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n')
{
var first = glyphInfos[length - 2];
first.Codepoint = '\u200C';
second.Codepoint = '\u200C';
second.Cluster = first.Cluster;
unsafe
{
fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 2])
fixed (GlyphInfo* p = &glyphInfos[length - 2])
{
*p = first;
}
fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 1])
fixed (GlyphInfo* p = &glyphInfos[length - 1])
{
*p = second;
}
@ -114,7 +126,7 @@ namespace Avalonia.UnitTests
unsafe
{
fixed (HarfBuzzSharp.GlyphInfo* p = &glyphInfos[length - 1])
fixed (GlyphInfo* p = &glyphInfos[length - 1])
{
*p = second;
}
@ -136,8 +148,29 @@ namespace Avalonia.UnitTests
private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale)
{
// Depends on direction of layout
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
// glyphPositions[index].YAdvance * textScale;
return glyphPositions[index].XAdvance * textScale;
}
private static ReadOnlyMemory<char> GetContainingMemory(ReadOnlyMemory<char> memory, out int start, out int length)
{
if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length))
{
return containingString.AsMemory();
}
if (MemoryMarshal.TryGetArray(memory, out var segment))
{
return segment.Array.AsMemory();
}
if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager<char> memoryManager, out start, out length))
{
return memoryManager.Memory;
}
// should never happen
throw new InvalidOperationException("Memory not backed by string, array or manager");
}
}
}

14
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -1,4 +1,5 @@
using Avalonia.Media.TextFormatting;
using System;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
@ -6,19 +7,20 @@ namespace Avalonia.UnitTests
{
public class MockTextShaperImpl : ITextShaperImpl
{
public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options)
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var characterBufferRange = new CharacterBufferRange(text, length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel);
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
var textSpan = text.Span;
var textStartIndex = TextTestHelper.GetStartCharIndex(text);
for (var i = 0; i < shapedBuffer.Length;)
{
var glyphCluster = i + text.OffsetToFirstChar;
var glyphCluster = i + textStartIndex;
var codepoint = Codepoint.ReadAt(characterBufferRange, i, out var count);
var codepoint = Codepoint.ReadAt(textSpan, i, out var count);
var glyphIndex = typeface.GetGlyph(codepoint);

15
tests/Avalonia.UnitTests/TextTestHelper.cs

@ -0,0 +1,15 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.UnitTests
{
public static class TextTestHelper
{
public static int GetStartCharIndex(ReadOnlyMemory<char> text)
{
if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
throw new InvalidOperationException("text memory should have been a string");
return start;
}
}
}
Loading…
Cancel
Save