Browse Source

Used Memory/Span instead of CharacterBufferReference/Range

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

+ 2 - 5
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);
             }
         }
     }

+ 46 - 47
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;
 

+ 0 - 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();
-    }
-}

+ 0 - 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);
-        }
-    }
-}
-

+ 7 - 7
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)
                 {

+ 3 - 3
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)
                 {

+ 12 - 12
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);

+ 9 - 7
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,

+ 39 - 84
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;

+ 1 - 3
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())
                                         {

+ 102 - 51
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);
         }

+ 4 - 17
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;
         }

+ 3 - 3
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);
         }
     }
 }

+ 1 - 1
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);

+ 1 - 66
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;
 

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

+ 4 - 10
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; }
     }
 }

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

+ 6 - 7
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;
 

+ 6 - 38
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;
-        }
     }
 }

+ 3 - 3
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);
     }   
 }

+ 5 - 10
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;

+ 2 - 4
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)

+ 2 - 4
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
                 }
             }

+ 2 - 4
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);
         }
     }
 

+ 29 - 4
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");
+        }
     }
 }

+ 32 - 8
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");
+        }
     }
 }

+ 1 - 1
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++)

+ 5 - 10
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++;
             }

+ 4 - 4
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>();
             

+ 1 - 1
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);
             }

+ 6 - 6
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,

+ 6 - 5
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);
         }
     }
 }

+ 2 - 2
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));
             }
         }
 

+ 21 - 17
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

+ 5 - 4
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);
                 }

+ 1 - 1
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]);

+ 51 - 18
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");
+        }
     }
 }

+ 8 - 6
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 - 0
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;
+        }
+    }
+}