Ver Fonte

Implement CharacterBufferReference and related classes

Benedikt Stebner há 3 anos atrás
pai
commit
895d85aa89
61 ficheiros alterados com 1427 adições e 1077 exclusões
  1. 1 1
      samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
  2. 2 6
      src/Avalonia.Base/Media/FormattedText.cs
  3. 236 183
      src/Avalonia.Base/Media/GlyphRun.cs
  4. 12 6
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  5. 308 0
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  6. 176 0
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  7. 7 6
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  8. 12 7
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  9. 8 12
      src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs
  10. 19 12
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  11. 9 10
      src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs
  12. 1 1
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  13. 107 37
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  14. 52 50
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  15. 2 2
      src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs
  16. 52 48
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  17. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  18. 2 3
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  19. 177 181
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  20. 3 3
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  21. 2 2
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  22. 6 5
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  23. 7 4
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  24. 1 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  25. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  26. 2 1
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  27. 4 5
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  28. 4 3
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  29. 4 4
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  30. 6 6
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  31. 8 7
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  32. 3 8
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  33. 3 8
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  34. 1 1
      src/Avalonia.Base/Media/TextTrimming.cs
  35. 2 3
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  36. 2 1
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  37. 0 8
      src/Avalonia.Base/Utilities/ArraySlice.cs
  38. 0 239
      src/Avalonia.Base/Utilities/ReadOnlySlice.cs
  39. 2 2
      src/Avalonia.Controls/Documents/LineBreak.cs
  40. 1 1
      src/Avalonia.Controls/Documents/Run.cs
  41. 17 10
      src/Avalonia.Controls/TextBlock.cs
  42. 3 1
      src/Avalonia.Controls/TextBox.cs
  43. 5 3
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  44. 4 2
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  45. 12 12
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  46. 6 5
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  47. 1 3
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  48. 2 1
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs
  49. 4 5
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs
  50. 5 4
      tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs
  51. 0 37
      tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs
  52. 1 1
      tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs
  53. 15 14
      tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
  54. 1 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs
  55. 11 8
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs
  56. 10 11
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  57. 24 22
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  58. 46 44
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  59. 3 3
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs
  60. 5 3
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  61. 6 6
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

+ 1 - 1
samples/RenderDemo/Pages/TextFormatterPage.axaml.cs

@@ -90,7 +90,7 @@ namespace RenderDemo.Pages
                     return new ControlRun(_control, _defaultProperties);
                 }
 
-                return new TextCharacters(_text.AsMemory(), _defaultProperties);
+                return new TextCharacters(_text, _defaultProperties);
             }
         }
 

+ 2 - 6
src/Avalonia.Base/Media/FormattedText.cs

@@ -1,10 +1,8 @@
 using System;
 using System.Collections;
-using System.Collections.Generic;
 using System.ComponentModel;
 using System.Diagnostics;
 using System.Globalization;
-using Avalonia.Controls;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Utilities;
 
@@ -25,7 +23,7 @@ namespace Avalonia.Media
         private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm;
 
         // properties and format runs
-        private ReadOnlySlice<char> _text;
+        private string _text;
         private readonly SpanVector _formatRuns = new SpanVector(null);
         private SpanPosition _latestPosition;
 
@@ -69,9 +67,7 @@ namespace Avalonia.Media
 
             ValidateFontSize(emSize);
 
-            _text = textToFormat != null ?
-                new ReadOnlySlice<char>(textToFormat.AsMemory()) :
-                throw new ArgumentNullException(nameof(textToFormat));
+            _text = textToFormat;
 
             var runProps = new GenericTextRunProperties(
                 typeface,

+ 236 - 183
src/Avalonia.Base/Media/GlyphRun.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Drawing;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Platform;
 using Avalonia.Utilities;
@@ -22,15 +21,12 @@ namespace Avalonia.Media
         private Point? _baselineOrigin;
         private GlyphRunMetrics? _glyphRunMetrics;
 
-        private ReadOnlySlice<char> _characters;
-
+        private IReadOnlyList<char> _characters;
         private IReadOnlyList<ushort> _glyphIndices;
         private IReadOnlyList<double>? _glyphAdvances;
         private IReadOnlyList<Vector>? _glyphOffsets;
         private IReadOnlyList<int>? _glyphClusters;
 
-        private int _offsetToFirstCharacter;
-
         /// <summary>
         ///     Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
         /// </summary>
@@ -45,7 +41,7 @@ namespace Avalonia.Media
         public GlyphRun(
             IGlyphTypeface glyphTypeface,
             double fontRenderingEmSize,
-            ReadOnlySlice<char> characters,
+            IReadOnlyList<char> characters,
             IReadOnlyList<ushort> glyphIndices,
             IReadOnlyList<double>? glyphAdvances = null,
             IReadOnlyList<Vector>? glyphOffsets = null,
@@ -54,19 +50,19 @@ namespace Avalonia.Media
         {
             _glyphTypeface = glyphTypeface;
 
-            FontRenderingEmSize = fontRenderingEmSize;
+            _fontRenderingEmSize = fontRenderingEmSize;
 
-            Characters = characters;
+            _characters = characters;
 
             _glyphIndices = glyphIndices;
 
-            GlyphAdvances = glyphAdvances;
+            _glyphAdvances = glyphAdvances;
 
-            GlyphOffsets = glyphOffsets;
+            _glyphOffsets = glyphOffsets;
 
-            GlyphClusters = glyphClusters;
+            _glyphClusters = glyphClusters;
 
-            BiDiLevel = biDiLevel;
+            _biDiLevel = biDiLevel;
         }
 
         /// <summary>
@@ -145,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 ReadOnlySlice<char> Characters
+        public IReadOnlyList<char> Characters
         {
             get => _characters;
             set => Set(ref _characters, value);
@@ -219,7 +215,7 @@ namespace Avalonia.Media
         /// </returns>
         public double GetDistanceFromCharacterHit(CharacterHit characterHit)
         {
-            var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
+            var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
 
             var distance = 0.0;
 
@@ -227,12 +223,12 @@ namespace Avalonia.Media
             {
                 if (GlyphClusters != null)
                 {
-                    if (characterIndex < GlyphClusters[0])
+                    if (characterIndex < Metrics.FirstCluster)
                     {
                         return 0;
                     }
 
-                    if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
+                    if (characterIndex > Metrics.LastCluster)
                     {
                         return Metrics.WidthIncludingTrailingWhitespace;
                     }
@@ -268,12 +264,12 @@ namespace Avalonia.Media
 
                 if (GlyphClusters != null && GlyphClusters.Count > 0)
                 {
-                    if (characterIndex > GlyphClusters[0])
+                    if (characterIndex > Metrics.LastCluster)
                     {
                         return 0;
                     }
 
-                    if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
+                    if (characterIndex <= Metrics.FirstCluster)
                     {
                         return Size.Width;
                     }
@@ -299,19 +295,12 @@ namespace Avalonia.Media
         /// </returns>
         public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
         {
-            var characterIndex = 0;
-
             // Before
             if (distance <= 0)
             {
                 isInside = false;
 
-                if (GlyphClusters != null)
-                {
-                    characterIndex = GlyphClusters[characterIndex];
-                }
-
-                var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);
+                var firstCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.FirstCluster : Metrics.LastCluster, out _);
 
                 return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
             }
@@ -321,18 +310,13 @@ namespace Avalonia.Media
             {
                 isInside = false;
 
-                characterIndex = GlyphIndices.Count - 1;
-
-                if (GlyphClusters != null)
-                {
-                    characterIndex = GlyphClusters[characterIndex];
-                }
-
-                var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
+                var lastCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.LastCluster : Metrics.FirstCluster, out _);
 
                 return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
             }
 
+            var characterIndex = 0;
+
             //Within
             var currentX = 0d;
 
@@ -378,7 +362,7 @@ namespace Avalonia.Media
             var characterHit = FindNearestCharacterHit(characterIndex, out var width);
 
             var delta = width / 2;
-            
+
             var offset = IsLeftToRight ? Math.Round(distance - currentX, 3) : Math.Round(currentX - distance, 3);
 
             var isTrailing = offset > delta;
@@ -400,24 +384,15 @@ namespace Avalonia.Media
             {
                 characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
 
-                var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
-
-                return textPosition > _characters.End ?
-                    characterHit :
-                    new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
-            }
-
-            var nextCharacterHit =
-                FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+                if (characterHit.FirstCharacterIndex == Metrics.LastCluster)
+                {
+                    return characterHit;
+                }
 
-            if (characterHit == nextCharacterHit)
-            {
-                return characterHit;
+                return new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
             }
 
-            return characterHit.TrailingLength > 0 ?
-                nextCharacterHit :
-                new CharacterHit(nextCharacterHit.FirstCharacterIndex);
+            return FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
         }
 
         /// <summary>
@@ -454,29 +429,24 @@ namespace Avalonia.Media
                 return characterIndex;
             }
 
-            if (IsLeftToRight)
+            if (characterIndex > Metrics.LastCluster)
             {
-                if (characterIndex < GlyphClusters[0])
+                if (IsLeftToRight)
                 {
-                    return 0;
+                    return GlyphIndices.Count - 1;
                 }
 
-                if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
-                {
-                    return GlyphClusters.Count - 1;
-                }
+                return 0;
             }
-            else
-            {
-                if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
-                {
-                    return GlyphClusters.Count - 1;
-                }
 
-                if (characterIndex > GlyphClusters[0])
+            if (characterIndex < Metrics.FirstCluster)
+            {
+                if (IsLeftToRight)
                 {
                     return 0;
                 }
+
+                return GlyphIndices.Count - 1;
             }
 
             var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
@@ -498,7 +468,7 @@ namespace Avalonia.Media
 
                 if (start < 0)
                 {
-                    return -1;
+                    goto result;
                 }
             }
 
@@ -517,6 +487,18 @@ namespace Avalonia.Media
                 }
             }
 
+        result:
+
+            if (start < 0)
+            {
+                return 0;
+            }
+
+            if (start > GlyphIndices.Count - 1)
+            {
+                return GlyphIndices.Count - 1;
+            }
+
             return start;
         }
 
@@ -532,20 +514,20 @@ namespace Avalonia.Media
         {
             width = 0.0;
 
-            var start = FindGlyphIndex(index);
+            var glyphIndex = FindGlyphIndex(index);
 
             if (GlyphClusters == null)
             {
                 width = GetGlyphAdvance(index, out _);
 
-                return new CharacterHit(start, 1);
+                return new CharacterHit(glyphIndex, 1);
             }
 
-            var cluster = GlyphClusters[start];
+            var cluster = GlyphClusters[glyphIndex];
 
             var nextCluster = cluster;
 
-            var currentIndex = start;
+            var currentIndex = glyphIndex;
 
             while (nextCluster == cluster)
             {
@@ -571,20 +553,64 @@ namespace Avalonia.Media
                 }
 
                 nextCluster = GlyphClusters[currentIndex];
-            }           
+            }
 
-            int trailingLength;
+            var clusterLength = Math.Max(0, nextCluster - cluster);
 
-            if (nextCluster == cluster)
-            {
-                trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
-            }
-            else
+            if (cluster == Metrics.LastCluster && clusterLength == 0)
             {
-                trailingLength = nextCluster - cluster;
+                var characterLength = 0;
+
+                var currentCluster = Metrics.FirstCluster;
+
+                if (IsLeftToRight)
+                {
+                    for (int i = 1; i < GlyphClusters.Count; i++)
+                    {
+                        nextCluster = GlyphClusters[i];
+
+                        if (currentCluster > cluster)
+                        {
+                            break;
+                        }
+
+                        var length = nextCluster - currentCluster;
+
+                        characterLength += length;
+
+                        currentCluster = nextCluster;
+                    }
+                }
+                else
+                {
+                    for (int i = GlyphClusters.Count - 1; i >= 0; i--)
+                    {
+                        nextCluster = GlyphClusters[i];
+
+                        if (currentCluster > cluster)
+                        {
+                            break;
+                        }
+
+                        var length = nextCluster - currentCluster;
+
+                        characterLength += length;
+
+                        currentCluster = nextCluster;
+                    }
+                }
+
+                if (Characters != null)
+                {
+                    clusterLength = Characters.Count - characterLength;
+                }
+                else
+                {
+                    clusterLength = 1;
+                }
             }
 
-            return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
+            return new CharacterHit(cluster, clusterLength);
         }
 
         /// <summary>
@@ -618,22 +644,25 @@ namespace Avalonia.Media
 
         private GlyphRunMetrics CreateGlyphRunMetrics()
         {
-            var firstCluster = 0;
-            var lastCluster = Characters.Length - 1;
+            int firstCluster = 0, lastCluster = 0;
 
-            if (!IsLeftToRight)
+            if (_glyphClusters != null && _glyphClusters.Count > 0)
             {
-                var cluster = firstCluster;
-                firstCluster = lastCluster;
-                lastCluster = cluster;
+                firstCluster = _glyphClusters[0];
+                lastCluster = _glyphClusters[_glyphClusters.Count - 1];
             }
-
-            if (GlyphClusters != null && GlyphClusters.Count > 0)
+            else
             {
-                firstCluster = GlyphClusters[0];
-                lastCluster = GlyphClusters[GlyphClusters.Count - 1];
+                if (Characters != null && Characters.Count > 0)
+                {
+                    firstCluster = 0;
+                    lastCluster = Characters.Count - 1;
+                }
+            }
 
-                _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
+            if (!IsLeftToRight)
+            {
+                (lastCluster, firstCluster) = (firstCluster, lastCluster);
             }
 
             var isReversed = firstCluster > lastCluster;
@@ -666,12 +695,19 @@ namespace Avalonia.Media
                 }
             }
 
-            return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
-                height);
+            return new GlyphRunMetrics(
+                width,
+                widthIncludingTrailingWhitespace,
+                height,
+                trailingWhitespaceLength,
+                newLineLength,
+                firstCluster,
+                lastCluster
+            );
         }
 
         private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
-        {          
+        {
             if (isReversed)
             {
                 return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
@@ -681,66 +717,82 @@ namespace Avalonia.Media
             newLineLength = 0;
             var trailingWhitespaceLength = 0;
 
-            if (GlyphClusters == null)
+            if (Characters != null)
             {
-                for (var i = _characters.Length - 1; i >= 0;)
+                if (GlyphClusters == null)
                 {
-                    var codepoint = Codepoint.ReadAt(_characters, i, out var count);
-
-                    if (!codepoint.IsWhiteSpace)
+                    for (var i = _characters.Count - 1; i >= 0;)
                     {
-                        break;
-                    }
+                        var codepoint = Codepoint.ReadAt(_characters, i, out var count);
 
-                    if (codepoint.IsBreakChar)
-                    {
-                        newLineLength++;
-                    }
+                        if (!codepoint.IsWhiteSpace)
+                        {
+                            break;
+                        }
 
-                    trailingWhitespaceLength++;
+                        if (codepoint.IsBreakChar)
+                        {
+                            newLineLength++;
+                        }
+
+                        trailingWhitespaceLength++;
 
-                    i -= count;
-                    glyphCount++;
+                        i -= count;
+                        glyphCount++;
+                    }
                 }
-            }
-            else
-            {
-                for (var i = GlyphClusters.Count - 1; i >= 0; i--)
+                else
                 {
-                    var currentCluster = GlyphClusters[i];
-                    var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
-                    var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
-
-                    if (!codepoint.IsWhiteSpace)
+                    if (Characters.Count > 0)
                     {
-                        break;
-                    }
+                        var characterIndex = Characters.Count - 1;
 
-                    var clusterLength = 1;
+                        for (var i = GlyphClusters.Count - 1; i >= 0; i--)
+                        {
+                            var currentCluster = GlyphClusters[i];
+                            var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
 
-                    while(i - 1 >= 0)
-                    {
-                        var nextCluster = GlyphClusters[i - 1];
+                            characterIndex -= characterLength;
 
-                        if(currentCluster == nextCluster)
-                        {
-                            clusterLength++;
-                            i--;
+                            if (!codepoint.IsWhiteSpace)
+                            {
+                                break;
+                            }
 
-                            continue;
-                        }
+                            var clusterLength = 1;
 
-                        break;
-                    }
+                            while (i - 1 >= 0)
+                            {
+                                var nextCluster = GlyphClusters[i - 1];
 
-                    if (codepoint.IsBreakChar)
-                    {
-                        newLineLength += clusterLength;
-                    }
+                                if (currentCluster == nextCluster)
+                                {
+                                    clusterLength++;
+                                    i--;
+
+                                    if(characterIndex >= 0)
+                                    {
+                                        codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength);
+
+                                        characterIndex -= characterLength;
+                                    }
+
+                                    continue;
+                                }
+
+                                break;
+                            }
+
+                            if (codepoint.IsBreakChar)
+                            {
+                                newLineLength += clusterLength;
+                            }
 
-                    trailingWhitespaceLength += clusterLength;
-                   
-                    glyphCount++;                   
+                            trailingWhitespaceLength += clusterLength;
+
+                            glyphCount++;
+                        }
+                    }
                 }
             }
 
@@ -753,67 +805,73 @@ namespace Avalonia.Media
             newLineLength = 0;
             var trailingWhitespaceLength = 0;
 
-            if (GlyphClusters == null)
+            if (Characters != null)
             {
-                for (var i = 0; i < Characters.Length;)
+                if (GlyphClusters == null)
                 {
-                    var codepoint = Codepoint.ReadAt(_characters, i, out var count);
-
-                    if (!codepoint.IsWhiteSpace)
+                    for (var i = 0; i < Characters.Count;)
                     {
-                        break;
-                    }
+                        var codepoint = Codepoint.ReadAt(_characters, i, out var count);
 
-                    if (codepoint.IsBreakChar)
-                    {
-                        newLineLength++;
-                    }
+                        if (!codepoint.IsWhiteSpace)
+                        {
+                            break;
+                        }
 
-                    trailingWhitespaceLength++;
+                        if (codepoint.IsBreakChar)
+                        {
+                            newLineLength++;
+                        }
 
-                    i += count;
-                    glyphCount++;
+                        trailingWhitespaceLength++;
+
+                        i += count;
+                        glyphCount++;
+                    }
                 }
-            }
-            else
-            {
-                for (var i = 0; i < GlyphClusters.Count; i++)
+                else
                 {
-                    var currentCluster = GlyphClusters[i];
-                    var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
-                    var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
+                    var characterIndex = 0;
 
-                    if (!codepoint.IsWhiteSpace)
+                    for (var i = 0; i < GlyphClusters.Count; i++)
                     {
-                        break;
-                    }
+                        var currentCluster = GlyphClusters[i];
+                        var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
 
-                    var clusterLength = 1;
+                        characterIndex += characterLength;
 
-                    var j = i;
+                        if (!codepoint.IsWhiteSpace)
+                        {
+                            break;
+                        }
 
-                    while (j - 1 >= 0)
-                    {
-                        var nextCluster = GlyphClusters[--j];
+                        var clusterLength = 1;
 
-                        if (currentCluster == nextCluster)
+                        var j = i;
+
+                        while (j - 1 >= 0)
                         {
-                            clusterLength++;                        
+                            var nextCluster = GlyphClusters[--j];
 
-                            continue;
-                        }
+                            if (currentCluster == nextCluster)
+                            {
+                                clusterLength++;
 
-                        break;
-                    }
+                                continue;
+                            }
 
-                    if (codepoint.IsBreakChar)
-                    {
-                        newLineLength += clusterLength;
-                    }
+                            break;
+                        }
+
+                        if (codepoint.IsBreakChar)
+                        {
+                            newLineLength += clusterLength;
+                        }
 
-                    trailingWhitespaceLength += clusterLength;
+                        trailingWhitespaceLength += clusterLength;
 
-                    glyphCount += clusterLength;
+                        glyphCount += clusterLength;
+                    }
                 }
             }
 
@@ -855,14 +913,9 @@ namespace Avalonia.Media
                 throw new InvalidOperationException();
             }
 
-            _glyphRunImpl = CreateGlyphRunImpl();
-        }
-
-        private IGlyphRunImpl CreateGlyphRunImpl()
-        {
             var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
 
-            return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
+            _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
         }
 
         void IDisposable.Dispose()

+ 12 - 6
src/Avalonia.Base/Media/GlyphRunMetrics.cs

@@ -2,24 +2,30 @@
 {
     public readonly struct GlyphRunMetrics
     {
-        public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
-            int newlineLength, double height)
+        public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height,
+            int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
         {
             Width = width;
             WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
-            TrailingWhitespaceLength = trailingWhitespaceLength;
-            NewlineLength = newlineLength;
             Height = height;
+            TrailingWhitespaceLength = trailingWhitespaceLength;
+            NewLineLength= newLineLength;
+            FirstCluster = firstCluster;
+            LastCluster = lastCluster;
         }
 
         public double Width { get; }
 
         public double WidthIncludingTrailingWhitespace { get; }
 
+        public double Height { get; }
+
         public int TrailingWhitespaceLength { get; }
         
-        public int NewlineLength { get; }
+        public int NewLineLength { get;  }
 
-        public double Height { get; }
+        public int FirstCluster { get; }
+
+        public int LastCluster { get; }
     }
 }

+ 308 - 0
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@@ -0,0 +1,308 @@
+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 <see cref="CharacterBufferRange"/> from unsafe character string
+        /// </summary>
+        /// <param name="unsafeCharacterString">pointer to character string</param>
+        /// <param name="characterLength">character length</param>
+        public unsafe CharacterBufferRange(
+            char* unsafeCharacterString,
+            int characterLength
+            )
+            : this(
+                new CharacterBufferReference(unsafeCharacterString, characterLength),
+                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 Span[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 =>
+            CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
+
+        /// <summary>
+        /// Gets the character memory buffer
+        /// </summary>
+        internal ReadOnlyMemory<char> CharacterBuffer
+        {
+            get { return CharacterBufferReference.CharacterBuffer; }
+        }
+
+        /// <summary>
+        /// Gets the character offset relative to the beginning of buffer to 
+        /// the first character of the run
+        /// </summary>
+        internal int OffsetToFirstChar
+        {
+            get { return CharacterBufferReference.OffsetToFirstChar; }
+        }
+
+        /// <summary>
+        /// Indicate whether the character buffer range is empty
+        /// </summary>
+        internal bool IsEmpty
+        {
+            get { return CharacterBufferReference.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(
+                CharacterBufferReference.CharacterBuffer,
+                CharacterBufferReference.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()
+        {
+            return new ImmutableReadOnlyListStructEnumerator<char>(this);
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+    }
+}

+ 176 - 0
src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs

@@ -0,0 +1,176 @@
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+
+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 unsafe character string
+        /// </summary>
+        /// <param name="unsafeCharacterString">pointer to character string</param>
+        /// <param name="characterLength">character length of unsafe string</param>
+        public unsafe CharacterBufferReference(char* unsafeCharacterString, int characterLength)
+            : this(new UnmanagedMemoryManager<char>(unsafeCharacterString, characterLength).Memory, 0)
+        { }
+      
+        /// <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>
+        /// 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);
+        }
+
+        public ReadOnlyMemory<char> CharacterBuffer { get; }
+
+        public int OffsetToFirstChar { get; }
+
+        /// <summary>
+        /// A MemoryManager over a raw pointer
+        /// </summary>
+        /// <remarks>The pointer is assumed to be fully unmanaged, or externally pinned - no attempt will be made to pin this data</remarks>
+        public sealed unsafe class UnmanagedMemoryManager<T> : MemoryManager<T>
+            where T : unmanaged
+        {
+            private readonly T* _pointer;
+            private readonly int _length;
+
+            /// <summary>
+            /// Create a new UnmanagedMemoryManager instance at the given pointer and size
+            /// </summary>
+            /// <remarks>It is assumed that the span provided is already unmanaged or externally pinned</remarks>
+            public UnmanagedMemoryManager(Span<T> span)
+            {
+                fixed (T* ptr = &MemoryMarshal.GetReference(span))
+                {
+                    _pointer = ptr;
+                    _length = span.Length;
+                }
+            }
+            /// <summary>
+            /// Create a new UnmanagedMemoryManager instance at the given pointer and size
+            /// </summary>
+            public UnmanagedMemoryManager(T* pointer, int length)
+            {
+                if (length < 0)
+                    throw new ArgumentOutOfRangeException(nameof(length));
+                _pointer = pointer;
+                _length = length;
+            }
+            /// <summary>
+            /// Obtains a span that represents the region
+            /// </summary>
+            public override Span<T> GetSpan() => new Span<T>(_pointer, _length);
+
+            /// <summary>
+            /// Provides access to a pointer that represents the data (note: no actual pin occurs)
+            /// </summary>
+            public override MemoryHandle Pin(int elementIndex = 0)
+            {
+                if (elementIndex < 0 || elementIndex >= _length)
+                    throw new ArgumentOutOfRangeException(nameof(elementIndex));
+                return new MemoryHandle(_pointer + elementIndex);
+            }
+            /// <summary>
+            /// Has no effect
+            /// </summary>
+            public override void Unpin() { }
+
+            /// <summary>
+            /// Releases all resources associated with this object
+            /// </summary>
+            protected override void Dispose(bool disposing) { }
+        }     
+    }
+}
+

+ 7 - 6
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

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

+ 12 - 7
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@@ -46,28 +46,30 @@ namespace Avalonia.Media.TextFormatting
 
             var breakOportunities = new Queue<int>();
 
+            var currentPosition = textLine.FirstTextSourceIndex;
+
             foreach (var textRun in lineImpl.TextRuns)
             {
-                var text = textRun.Text;
+                var text = new CharacterBufferRange(textRun);
 
                 if (text.IsEmpty)
                 {
                     continue;
                 }
 
-                var start = text.Start;
-
                 var lineBreakEnumerator = new LineBreakEnumerator(text);
 
                 while (lineBreakEnumerator.MoveNext())
                 {
                     var currentBreak = lineBreakEnumerator.Current;
 
-                    if (!currentBreak.Required && currentBreak.PositionWrap != text.Length)
+                    if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
                     {
-                        breakOportunities.Enqueue(start + currentBreak.PositionMeasure);
+                        breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);
                     }
                 }
+
+                currentPosition += textRun.Length;
             }
 
             if (breakOportunities.Count == 0)
@@ -78,9 +80,11 @@ namespace Avalonia.Media.TextFormatting
             var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace);
             var spacing = remainingSpace / breakOportunities.Count;
 
+            currentPosition = textLine.FirstTextSourceIndex;
+
             foreach (var textRun in lineImpl.TextRuns)
             {
-                var text = textRun.Text;
+                var text = textRun.CharacterBufferReference.CharacterBuffer;
 
                 if (text.IsEmpty)
                 {
@@ -91,7 +95,6 @@ namespace Avalonia.Media.TextFormatting
                 {
                     var glyphRun = shapedText.GlyphRun;
                     var shapedBuffer = shapedText.ShapedBuffer;
-                    var currentPosition = text.Start;
 
                     while (breakOportunities.Count > 0)
                     {
@@ -110,6 +113,8 @@ namespace Avalonia.Media.TextFormatting
 
                     glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances;
                 }
+
+                currentPosition += textRun.Length;
             }
         }
     }

+ 8 - 12
src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs

@@ -7,30 +7,26 @@ namespace Avalonia.Media.TextFormatting
     /// </summary>
     public sealed class ShapeableTextCharacters : TextRun
     {
-        public ShapeableTextCharacters(ReadOnlySlice<char> text, TextRunProperties properties, sbyte biDiLevel)
+        public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length,
+            TextRunProperties properties, sbyte biDiLevel)
         {
-            TextSourceLength = text.Length;
-            Text = text;
+            CharacterBufferReference = characterBufferReference;
+            Length = length;
             Properties = properties;
             BidiLevel = biDiLevel;
         }
 
-        public override int TextSourceLength { get; }
+        public override int Length { get; }
 
-        public override ReadOnlySlice<char> Text { get; }
+        public override CharacterBufferReference CharacterBufferReference { get; }
 
         public override TextRunProperties Properties { get; }
-        
+
         public sbyte BidiLevel { get; }
 
         public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters)
         {
-            if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer))
-            {
-                return false;
-            }
-
-            if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start)
+            if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference))
             {
                 return false;
             }

+ 19 - 12
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@@ -7,16 +7,16 @@ namespace Avalonia.Media.TextFormatting
     public sealed class ShapedBuffer : IList<GlyphInfo>
     {
         private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
-
-        public ShapedBuffer(ReadOnlySlice<char> text, int length, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
-            : this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel)
+        
+        public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : 
+            this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface,  fontRenderingEmSize,  bidiLevel)
         {
 
         }
 
-        internal ShapedBuffer(ReadOnlySlice<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
+        internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
         {
-            Text = text;
+            CharacterBufferRange = characterBufferRange;
             GlyphInfos = glyphInfos;
             GlyphTypeface = glyphTypeface;
             FontRenderingEmSize = fontRenderingEmSize;
@@ -24,9 +24,7 @@ namespace Avalonia.Media.TextFormatting
         }
 
         internal ArraySlice<GlyphInfo> GlyphInfos { get; }
-
-        public ReadOnlySlice<char> Text { get; }
-
+        
         public int Length => GlyphInfos.Length;
 
         public IGlyphTypeface GlyphTypeface { get; }
@@ -45,6 +43,8 @@ namespace Avalonia.Media.TextFormatting
 
         public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
 
+        public CharacterBufferRange CharacterBufferRange { get; }
+        
         /// <summary>
         /// Finds a glyph index for given character index.
         /// </summary>
@@ -105,16 +105,23 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>The split result.</returns>
         internal SplitResult<ShapedBuffer> Split(int length)
         {
-            if (Text.Length == length)
+            if (CharacterBufferRange.Length == length)
             {
                 return new SplitResult<ShapedBuffer>(this, null);
             }
 
-            var glyphCount = FindGlyphIndex(Text.Start + length);
+            var firstCluster = GlyphClusters[0];
+            var lastCluster = GlyphClusters[GlyphClusters.Count - 1];
+
+            var start = firstCluster < lastCluster ? firstCluster : lastCluster;
+
+            var glyphCount = FindGlyphIndex(start + length);
 
-            var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
+            var first = new ShapedBuffer(CharacterBufferRange.Take(length), 
+                GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
 
-            var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
+            var second = new ShapedBuffer(CharacterBufferRange.Skip(length),
+                GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
 
             return new SplitResult<ShapedBuffer>(first, second);
         }

+ 9 - 10
src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs

@@ -1,6 +1,5 @@
 using System;
 using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -14,10 +13,10 @@ namespace Avalonia.Media.TextFormatting
         public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
         {
             ShapedBuffer = shapedBuffer;
-            Text = shapedBuffer.Text;
+            CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
+            Length = shapedBuffer.CharacterBufferRange.Length;
             Properties = properties;
-            TextSourceLength = Text.Length;
-            TextMetrics = new TextMetrics(properties.Typeface, properties.FontRenderingEmSize);
+            TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
         }
 
         public bool IsReversed { get; private set; }
@@ -27,13 +26,13 @@ namespace Avalonia.Media.TextFormatting
         public ShapedBuffer ShapedBuffer { get; }
 
         /// <inheritdoc/>
-        public override ReadOnlySlice<char> Text { get; }
+        public override CharacterBufferReference CharacterBufferReference { get; }
 
         /// <inheritdoc/>
         public override TextRunProperties Properties { get; }
 
         /// <inheritdoc/>
-        public override int TextSourceLength { get; }
+        public override int Length { get; }
 
         public TextMetrics TextMetrics { get; }
 
@@ -176,12 +175,12 @@ namespace Avalonia.Media.TextFormatting
 
             #if DEBUG
 
-            if (first.Text.Length != length)
+            if (first.Length != length)
             {
                 throw new InvalidOperationException("Split length mismatch.");
             }
-            
-            #endif
+
+#endif
 
             var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
 
@@ -193,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
             return new GlyphRun(
                 ShapedBuffer.GlyphTypeface,
                 ShapedBuffer.FontRenderingEmSize,
-                Text,
+                new CharacterBufferRange(CharacterBufferReference, Length),
                 ShapedBuffer.GlyphIndices,
                 ShapedBuffer.GlyphAdvances,
                 ShapedBuffer.GlyphOffsets,

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/SplitResult.cs

@@ -1,6 +1,6 @@
 namespace Avalonia.Media.TextFormatting
 {
-    internal readonly struct SplitResult<T>
+    public readonly struct SplitResult<T>
     {
         public SplitResult(T first, T? second)
         {

+ 107 - 37
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -10,26 +9,98 @@ namespace Avalonia.Media.TextFormatting
     /// </summary>
     public class TextCharacters : TextRun
     {
-        public TextCharacters(ReadOnlySlice<char> text, TextRunProperties properties)
-        {
-            TextSourceLength = text.Length;
-            Text = text;
-            Properties = properties;
-        }
+        /// <summary>
+        /// Construct a run of text content from character array
+        /// </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(ReadOnlySlice<char> text, int offsetToFirstCharacter, int length,
-            TextRunProperties properties)
+        /// <summary>
+        /// Construct a run for text content from unsafe character string
+        /// </summary>
+        public unsafe TextCharacters(
+            char* unsafeCharacterString,
+            int length,
+            TextRunProperties textRunProperties
+            ) :
+            this(
+                new CharacterBufferReference(unsafeCharacterString, length),
+                length,
+                textRunProperties
+                )
+        { }
+
+        /// <summary>
+        /// Internal constructor of TextContent
+        /// </summary>
+        public TextCharacters(
+            CharacterBufferReference characterBufferReference,
+            int length,
+            TextRunProperties textRunProperties
+            )
         {
-            Text = text.Skip(offsetToFirstCharacter).Take(length);
-            TextSourceLength = length;
-            Properties = properties;
+            if (length <= 0)
+            {
+                throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
+            }
+
+            if (textRunProperties.FontRenderingEmSize <= 0)
+            {
+                throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero");
+            }
+
+            CharacterBufferReference = characterBufferReference;
+            Length = length;
+            Properties = textRunProperties;
         }
 
         /// <inheritdoc />
-        public override int TextSourceLength { get; }
+        public override int Length { get; }
 
         /// <inheritdoc />
-        public override ReadOnlySlice<char> Text { get; }
+        public override CharacterBufferReference CharacterBufferReference { get; }
 
         /// <inheritdoc />
         public override TextRunProperties Properties { get; }
@@ -38,18 +109,17 @@ namespace Avalonia.Media.TextFormatting
         /// Gets a list of <see cref="ShapeableTextCharacters"/>.
         /// </summary>
         /// <returns>The shapeable text characters.</returns>
-        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
-            ref TextRunProperties? previousProperties)
+        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
             var shapeableCharacters = new List<ShapeableTextCharacters>(2);
 
-            while (!runText.IsEmpty)
+            while (characterBufferRange.Length > 0)
             {
-                var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
+                var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties);
 
                 shapeableCharacters.Add(shapeableRun);
 
-                runText = runText.Skip(shapeableRun.Text.Length);
+                characterBufferRange = characterBufferRange.Skip(shapeableRun.Length);
 
                 previousProperties = shapeableRun.Properties;
             }
@@ -60,45 +130,45 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Creates a shapeable text run with unique properties.
         /// </summary>
-        /// <param name="text">The text to create text runs from.</param>
+        /// <param name="characterBufferRange">The character buffer range 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 ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
+        private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange,
             TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
             var defaultTypeface = defaultProperties.Typeface;
             var currentTypeface = defaultTypeface;
             var previousTypeface = previousProperties?.Typeface;
 
-            if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
+            if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script))
             {
                 if (script == Script.Common && previousTypeface is not null)
                 {
-                    if (TryGetShapeableLength(text, previousTypeface.Value, null, out var fallbackCount, out _))
+                    if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
                     {
-                        return new ShapeableTextCharacters(text.Take(fallbackCount),
+                        return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount,
                             defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
                     }
                 }
 
-                return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
+                return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
 
             if (previousTypeface is not null)
             {
-                if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
+                if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
                 {
-                    return new ShapeableTextCharacters(text.Take(count),
+                    return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count,
                         defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
                 }
             }
 
             var codepoint = Codepoint.ReplacementCodepoint;
 
-            var codepointEnumerator = new CodepointEnumerator(text.Skip(count));
+            var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count));
 
             while (codepointEnumerator.MoveNext())
             {
@@ -118,10 +188,10 @@ namespace Avalonia.Media.TextFormatting
                     defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
                     out currentTypeface);
 
-            if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
+            if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
             {
                 //Fallback found
-                return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
+                return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
 
@@ -130,7 +200,7 @@ namespace Avalonia.Media.TextFormatting
 
             var glyphTypeface = currentTypeface.GlyphTypeface;
 
-            var enumerator = new GraphemeEnumerator(text);
+            var enumerator = new GraphemeEnumerator(characterBufferRange);
 
             while (enumerator.MoveNext())
             {
@@ -144,20 +214,20 @@ namespace Avalonia.Media.TextFormatting
                 count += grapheme.Text.Length;
             }
 
-            return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel);
+            return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
         }
 
         /// <summary>
         /// Tries to get a shapeable length that is supported by the specified typeface.
         /// </summary>
-        /// <param name="text">The text.</param>
+        /// <param name="characterBufferRange">The character buffer range 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>
-        protected static bool TryGetShapeableLength(
-            ReadOnlySlice<char> text,
+        internal static bool TryGetShapeableLength(
+            CharacterBufferRange characterBufferRange,
             Typeface typeface,
             Typeface? defaultTypeface,
             out int length,
@@ -166,7 +236,7 @@ namespace Avalonia.Media.TextFormatting
             length = 0;
             script = Script.Unknown;
 
-            if (text.Length == 0)
+            if (characterBufferRange.Length == 0)
             {
                 return false;
             }
@@ -174,7 +244,7 @@ namespace Avalonia.Media.TextFormatting
             var font = typeface.GlyphTypeface;
             var defaultFont = defaultTypeface?.GlyphTypeface;
 
-            var enumerator = new GraphemeEnumerator(text);
+            var enumerator = new GraphemeEnumerator(characterBufferRange);
 
             while (enumerator.MoveNext())
             {

+ 52 - 50
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -32,86 +32,88 @@ namespace Avalonia.Media.TextFormatting
                 switch (currentRun)
                 {
                     case ShapedTextCharacters shapedRun:
-                    {
-                        currentWidth += shapedRun.Size.Width;
-
-                        if (currentWidth > availableWidth)
                         {
-                            if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+                            currentWidth += shapedRun.Size.Width;
+
+                            if (currentWidth > availableWidth)
                             {
-                                if (isWordEllipsis && measuredLength < textLine.Length)
+                                if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
                                 {
-                                    var currentBreakPosition = 0;
+                                    if (isWordEllipsis && measuredLength < textLine.Length)
+                                    {
+                                        var currentBreakPosition = 0;
 
-                                    var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+                                        var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
 
-                                    while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
-                                    {
-                                        var nextBreakPosition = lineBreaker.Current.PositionMeasure;
+                                        var lineBreaker = new LineBreakEnumerator(text);
 
-                                        if (nextBreakPosition == 0)
+                                        while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
                                         {
-                                            break;
-                                        }
+                                            var nextBreakPosition = lineBreaker.Current.PositionMeasure;
 
-                                        if (nextBreakPosition >= measuredLength)
-                                        {
-                                            break;
+                                            if (nextBreakPosition == 0)
+                                            {
+                                                break;
+                                            }
+
+                                            if (nextBreakPosition >= measuredLength)
+                                            {
+                                                break;
+                                            }
+
+                                            currentBreakPosition = nextBreakPosition;
                                         }
 
-                                        currentBreakPosition = nextBreakPosition;
+                                        measuredLength = currentBreakPosition;
                                     }
-
-                                    measuredLength = currentBreakPosition;
                                 }
-                            }
 
-                            collapsedLength += measuredLength;
+                                collapsedLength += measuredLength;
 
-                            var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
+                                var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
 
-                            if (collapsedLength > 0)
-                            {
-                                var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+                                if (collapsedLength > 0)
+                                {
+                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
 
-                                collapsedRuns.AddRange(splitResult.First);
-                            }
+                                    collapsedRuns.AddRange(splitResult.First);
+                                }
 
-                            collapsedRuns.Add(shapedSymbol);
+                                collapsedRuns.Add(shapedSymbol);
 
-                            return collapsedRuns;
-                        }
+                                return collapsedRuns;
+                            }
 
-                        availableWidth -= currentRun.Size.Width;
+                            availableWidth -= currentRun.Size.Width;
 
-                        
-                        break;
-                    }
+
+                            break;
+                        }
 
                     case { } drawableRun:
-                    {
-                        //The whole run needs to fit into available space
-                        if (currentWidth + drawableRun.Size.Width > availableWidth)
                         {
-                            var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
-
-                            if (collapsedLength > 0)
+                            //The whole run needs to fit into available space
+                            if (currentWidth + drawableRun.Size.Width > availableWidth)
                             {
-                                var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+                                var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
 
-                                collapsedRuns.AddRange(splitResult.First);
-                            }
+                                if (collapsedLength > 0)
+                                {
+                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+
+                                    collapsedRuns.AddRange(splitResult.First);
+                                }
+
+                                collapsedRuns.Add(shapedSymbol);
 
-                            collapsedRuns.Add(shapedSymbol);
+                                return collapsedRuns;
+                            }
 
-                            return collapsedRuns;
+                            break;
                         }
-                        
-                        break;
-                    }
                 }
 
-                collapsedLength += currentRun.TextSourceLength;
+                collapsedLength += currentRun.Length;
 
                 runIndex++;
             }

+ 2 - 2
src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs

@@ -7,9 +7,9 @@
     {
         public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
         {
-            TextSourceLength = textSourceLength;
+            Length = textSourceLength;
         }
 
-        public override int TextSourceLength { get; }
+        public override int Length { get; }
     }
 }

+ 52 - 48
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting
             {
                 var currentRun = textRuns[i];
 
-                if (currentLength + currentRun.TextSourceLength < length)
+                if (currentLength + currentRun.Length < length)
                 {
-                    currentLength += currentRun.TextSourceLength;
+                    currentLength += currentRun.Length;
 
                     continue;
                 }
 
-                var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
+                var firstCount = currentRun.Length >= 1 ? i + 1 : i;
 
                 var first = new List<DrawableTextRun>(firstCount);
 
@@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting
 
                 var secondCount = textRuns.Count - firstCount;
 
-                if (currentLength + currentRun.TextSourceLength == length)
+                if (currentLength + currentRun.Length == length)
                 {
                     var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
 
                     if (second != null)
                     {
-                        var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
+                        var offset = currentRun.Length >= 1 ? 1 : 0;
 
                         for (var j = 0; j < secondCount; j++)
                         {
@@ -163,15 +163,17 @@ namespace Avalonia.Media.TextFormatting
 
             foreach (var textRun in textRuns)
             {
-                if (textRun.Text.IsEmpty)
+                if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0)
                 {
-                    var text = new char[textRun.TextSourceLength];
+                    var characterBuffer = new CharacterBufferReference(new char[textRun.Length]);
 
-                    biDiData.Append(text);
+                    biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length));
                 }
                 else
                 {
-                    biDiData.Append(textRun.Text);
+                    var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
+
+                    biDiData.Append(text);
                 }
             }
 
@@ -207,10 +209,9 @@ namespace Avalonia.Media.TextFormatting
                     case ShapeableTextCharacters shapeableRun:
                         {
                             var groupedRuns = new List<ShapeableTextCharacters>(2) { shapeableRun };
-                            var text = currentRun.Text;
-                            var start = currentRun.Text.Start;
-                            var length = currentRun.Text.Length;
-                            var bufferOffset = currentRun.Text.BufferOffset;
+                            var characterBufferReference = currentRun.CharacterBufferReference;
+                            var length = currentRun.Length;
+                            var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
 
                             while (index + 1 < processedRuns.Count)
                             {
@@ -223,19 +224,14 @@ namespace Avalonia.Media.TextFormatting
                                 {
                                     groupedRuns.Add(nextRun);
 
-                                    length += nextRun.Text.Length;
-
-                                    if (start > nextRun.Text.Start)
-                                    {
-                                        start = nextRun.Text.Start;
-                                    }
+                                    length += nextRun.Length;
 
-                                    if (bufferOffset > nextRun.Text.BufferOffset)
+                                    if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
                                     {
-                                        bufferOffset = nextRun.Text.BufferOffset;
+                                        offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
                                     }
 
-                                    text = new ReadOnlySlice<char>(text.Buffer, start, length, bufferOffset);
+                                    characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
 
                                     index++;
 
@@ -252,7 +248,7 @@ namespace Avalonia.Media.TextFormatting
                                          shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, 
                                          paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
 
-                            drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
+                            drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
 
                             break;
                         }
@@ -263,17 +259,17 @@ namespace Avalonia.Media.TextFormatting
         }
 
         private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
-            IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text, TextShaperOptions options)
+            IReadOnlyList<ShapeableTextCharacters> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
         {
             var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count);
 
-            var shapedBuffer = TextShaper.Current.ShapeText(text, options);
+            var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
 
             for (var i = 0; i < textRuns.Count; i++)
             {
                 var currentRun = textRuns[i];
 
-                var splitResult = shapedBuffer.Split(currentRun.Text.Length);
+                var splitResult = shapedBuffer.Split(currentRun.Length);
 
                 shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties));
 
@@ -301,7 +297,7 @@ namespace Avalonia.Media.TextFormatting
 
             TextRunProperties? previousProperties = null;
             TextCharacters? currentRun = null;
-            var runText = ReadOnlySlice<char>.Empty;
+            CharacterBufferRange runText = default;
 
             for (var i = 0; i < textCharacters.Count; i++)
             {
@@ -314,12 +310,12 @@ namespace Avalonia.Media.TextFormatting
 
                     yield return new[] { drawableRun };
 
-                    levelIndex += drawableRun.TextSourceLength;
+                    levelIndex += drawableRun.Length;
 
                     continue;
                 }
 
-                runText = currentRun.Text;
+                runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
 
                 for (; j < runText.Length;)
                 {
@@ -401,7 +397,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     endOfLine = textEndOfLine;
 
-                    textSourceLength += textEndOfLine.TextSourceLength;
+                    textSourceLength += textEndOfLine.Length;
 
                     textRuns.Add(textRun);
 
@@ -414,7 +410,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if (TryGetLineBreak(textCharacters, out var runLineBreak))
                             {
-                                var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap),
+                                var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap,
                                     textCharacters.Properties);
 
                                 textRuns.Add(splitResult);
@@ -435,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                 }
 
-                textSourceLength += textRun.TextSourceLength;
+                textSourceLength += textRun.Length;
             }
 
             return textRuns;
@@ -445,12 +441,14 @@ namespace Avalonia.Media.TextFormatting
         {
             lineBreak = default;
 
-            if (textRun.Text.IsEmpty)
+            if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty)
             {
                 return false;
             }
 
-            var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text);
+            var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
+
+            var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
 
             while (lineBreakEnumerator.MoveNext())
             {
@@ -461,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
 
                 lineBreak = lineBreakEnumerator.Current;
 
-                return lineBreak.PositionWrap >= textRun.Text.Length || true;
+                return lineBreak.PositionWrap >= textRun.Length || true;
             }
 
             return false;
@@ -480,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if(shapedTextCharacters.ShapedBuffer.Length > 0)
                             {
-                                var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
+                                var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
                                 var lastCluster = firstCluster;
 
                                 for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
@@ -498,7 +496,7 @@ namespace Avalonia.Media.TextFormatting
                                     currentWidth += glyphInfo.GlyphAdvance;
                                 }
 
-                                measuredLength += currentRun.TextSourceLength;
+                                measuredLength += currentRun.Length;
                             }                         
 
                             break;
@@ -511,7 +509,7 @@ namespace Avalonia.Media.TextFormatting
                                 goto found;
                             }
 
-                            measuredLength += currentRun.TextSourceLength;
+                            measuredLength += currentRun.Length;
                             currentWidth += currentRun.Size.Width;
 
                             break;
@@ -533,11 +531,11 @@ namespace Avalonia.Media.TextFormatting
             var flowDirection = paragraphProperties.FlowDirection;
             var properties = paragraphProperties.DefaultTextRunProperties;
             var glyphTypeface = properties.Typeface.GlyphTypeface;
-            var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
             var glyph = glyphTypeface.GetGlyph(s_empty[0]);
             var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
 
-            var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
+            var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length);
+            var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
                 (sbyte)flowDirection);
 
             var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
@@ -579,7 +577,9 @@ namespace Avalonia.Media.TextFormatting
             {
                 var currentRun = textRuns[index];
 
-                var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+                var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
+
+                var lineBreaker = new LineBreakEnumerator(runText);
 
                 var breakFound = false;
 
@@ -612,7 +612,7 @@ namespace Avalonia.Media.TextFormatting
                             //Find next possible wrap position (overflow)
                             if (index < textRuns.Count - 1)
                             {
-                                if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
+                                if (lineBreaker.Current.PositionWrap != currentRun.Length)
                                 {
                                     //We already found the next possible wrap position.
                                     breakFound = true;
@@ -626,7 +626,7 @@ namespace Avalonia.Media.TextFormatting
                                 {
                                     currentPosition += lineBreaker.Current.PositionWrap;
 
-                                    if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
+                                    if (lineBreaker.Current.PositionWrap != currentRun.Length)
                                     {
                                         break;
                                     }
@@ -640,7 +640,9 @@ namespace Avalonia.Media.TextFormatting
 
                                     currentRun = textRuns[index];
 
-                                    lineBreaker = new LineBreakEnumerator(currentRun.Text);
+                                    runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
+
+                                    lineBreaker = new LineBreakEnumerator(runText);
                                 }
                             }
                             else
@@ -669,7 +671,7 @@ namespace Avalonia.Media.TextFormatting
 
                 if (!breakFound)
                 {
-                    currentLength += currentRun.TextSourceLength;
+                    currentLength += currentRun.Length;
 
                     continue;
                 }
@@ -723,12 +725,12 @@ namespace Avalonia.Media.TextFormatting
                     return false;
                 }
 
-                if (Current.TextSourceLength == 0)
+                if (Current.Length == 0)
                 {
                     return false;
                 }
 
-                _pos += Current.TextSourceLength;
+                _pos += Current.Length;
 
                 return true;
             }
@@ -754,7 +756,9 @@ namespace Avalonia.Media.TextFormatting
 
             var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
 
-            var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
+            var characterBuffer = textRun.CharacterBufferReference;
+
+            var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
 
             return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
         }

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting
                 CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
                     textDecorations, flowDirection, lineHeight, letterSpacing);
 
-            _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
+            _textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
 
             _textTrimming = textTrimming ?? TextTrimming.None;
 

+ 2 - 3
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -19,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="width">width in which collapsing is constrained to</param>
         /// <param name="textRunProperties">text run properties of ellipsis symbol</param>
         public TextLeadingPrefixCharacterEllipsis(
-            ReadOnlySlice<char> ellipsis,
+            string ellipsis,
             int prefixLength,
             double width,
             TextRunProperties textRunProperties)
@@ -129,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
                                                 if (suffixCount > 0)
                                                 {
                                                     var splitSuffix =
-                                                        endShapedRun.Split(run.TextSourceLength - suffixCount);
+                                                        endShapedRun.Split(run.Length - suffixCount);
 
                                                     collapsedRuns.Add(splitSuffix.Second!);
                                                 }

+ 177 - 181
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -56,7 +56,7 @@ namespace Avalonia.Media.TextFormatting
         public override double Height => _textLineMetrics.Height;
 
         /// <inheritdoc/>
-        public override int NewLineLength => _textLineMetrics.NewLineLength;
+        public override int NewLineLength => _textLineMetrics.NewlineLength;
 
         /// <inheritdoc/>
         public override double OverhangAfter => 0;
@@ -180,7 +180,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 var lastRun = _textRuns[_textRuns.Count - 1];
 
-                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width);
+                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
             }
 
             // process hit that happens within the line
@@ -195,18 +195,18 @@ namespace Avalonia.Media.TextFormatting
                 if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
                 {
                     var rightToLeftIndex = i;
-                    currentPosition += currentRun.TextSourceLength;
+                    currentPosition += currentRun.Length;
 
                     while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
                     {
-                        var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
+                        var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters;
 
                         if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
                         {
                             break;
                         }
 
-                        currentPosition += nextShaped.TextSourceLength;
+                        currentPosition += nextShaped.Length;
 
                         rightToLeftIndex++;
                     }
@@ -223,27 +223,26 @@ namespace Avalonia.Media.TextFormatting
                         if (currentDistance + currentRun.Size.Width <= distance)
                         {
                             currentDistance += currentRun.Size.Width;
-                            currentPosition -= currentRun.TextSourceLength;
+                            currentPosition -= currentRun.Length;
 
                             continue;
                         }
 
-                        characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
-
-                        break;
+                        return GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
                     }
                 }
 
-                if (currentDistance + currentRun.Size.Width < distance)
+                characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
+
+                if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
                 {
                     currentDistance += currentRun.Size.Width;
-                    currentPosition += currentRun.TextSourceLength;
+
+                    currentPosition += currentRun.Length;
 
                     continue;
                 }
 
-                characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
-
                 break;
             }
 
@@ -264,10 +263,10 @@ namespace Avalonia.Media.TextFormatting
 
                         if (shapedRun.GlyphRun.IsLeftToRight)
                         {
-                            offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
+                            offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
                         }
 
-                        characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
+                        characterHit = new CharacterHit(offset + characterHit.FirstCharacterIndex, characterHit.TrailingLength);
 
                         break;
                     }
@@ -279,7 +278,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                         else
                         {
-                            characterHit = new CharacterHit(currentPosition, run.TextSourceLength);
+                            characterHit = new CharacterHit(currentPosition, run.Length);
                         }
                         break;
                     }
@@ -334,14 +333,14 @@ namespace Avalonia.Media.TextFormatting
 
                                 rightToLeftWidth -= currentRun.Size.Width;
 
-                                if (currentPosition + currentRun.TextSourceLength >= characterIndex)
+                                if (currentPosition + currentRun.Length >= characterIndex)
                                 {
                                     break;
                                 }
 
-                                currentPosition += currentRun.TextSourceLength;
+                                currentPosition += currentRun.Length;
 
-                                remainingLength -= currentRun.TextSourceLength;
+                                remainingLength -= currentRun.Length;
 
                                 i--;
                             }
@@ -350,7 +349,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                     }
 
-                    if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
+                    if (currentPosition + currentRun.Length >= characterIndex &&
                         TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
                     {
                         return Math.Max(0, currentDistance + distance);
@@ -358,8 +357,8 @@ namespace Avalonia.Media.TextFormatting
 
                     //No hit hit found so we add the full width
                     currentDistance += currentRun.Size.Width;
-                    currentPosition += currentRun.TextSourceLength;
-                    remainingLength -= currentRun.TextSourceLength;
+                    currentPosition += currentRun.Length;
+                    remainingLength -= currentRun.Length;
                 }
             }
             else
@@ -383,8 +382,8 @@ namespace Avalonia.Media.TextFormatting
 
                     //No hit hit found so we add the full width
                     currentDistance -= currentRun.Size.Width;
-                    currentPosition += currentRun.TextSourceLength;
-                    remainingLength -= currentRun.TextSourceLength;
+                    currentPosition += currentRun.Length;
+                    remainingLength -= currentRun.Length;
                 }
             }
 
@@ -412,16 +411,16 @@ namespace Avalonia.Media.TextFormatting
                     {
                         currentGlyphRun = shapedTextCharacters.GlyphRun;
 
-                        if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length)
+                        if (currentPosition + remainingLength <= currentPosition + currentRun.Length)
                         {
-                            characterHit = new CharacterHit(currentRun.Text.Start + remainingLength);
+                            characterHit = new CharacterHit(currentPosition + remainingLength);
 
                             distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit);
 
                             return true;
                         }
 
-                        if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit)
+                        if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit)
                         {
                             if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
                             {
@@ -440,7 +439,7 @@ namespace Avalonia.Media.TextFormatting
                             return true;
                         }
 
-                        if (characterIndex == currentPosition + currentRun.TextSourceLength)
+                        if (characterIndex == currentPosition + currentRun.Length)
                         {
                             distance = currentRun.Size.Width;
 
@@ -479,17 +478,22 @@ namespace Avalonia.Media.TextFormatting
             {
                 case ShapedTextCharacters shapedRun:
                     {
-                        characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
+                        nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
                         break;
                     }
                 default:
                     {
-                        characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+                        nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
                         break;
                     }
             }
 
-            return characterHit;
+            if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
+            {
+                return characterHit;
+            }
+
+            return nextCharacterHit;
         }
 
         /// <inheritdoc/>
@@ -542,200 +546,182 @@ namespace Avalonia.Media.TextFormatting
                 var characterLength = 0;
                 var endX = startX;
 
-                var currentShapedRun = currentRun as ShapedTextCharacters;
-
                 TextRunBounds currentRunBounds;
 
                 double combinedWidth;
 
-                if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
-                {
-                    startX += currentRun.Size.Width;
-
-                    currentPosition += currentRun.TextSourceLength;
-
-                    continue;
-                }
-
-                if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
+                if (currentRun is ShapedTextCharacters currentShapedRun)
                 {
-                    var rightToLeftIndex = index;
-                    var rightToLeftWidth = currentShapedRun.Size.Width;
+                    var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
 
-                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
+                    if (currentPosition + currentRun.Length <= firstTextSourceIndex)
                     {
-                        if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
-                        {
-                            break;
-                        }
+                        startX += currentRun.Size.Width;
 
-                        rightToLeftIndex++;
+                        currentPosition += currentRun.Length;
 
-                        rightToLeftWidth += nextShapedRun.Size.Width;
-
-                        if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength)
-                        {
-                            break;
-                        }
-
-                        currentShapedRun = nextShapedRun;
+                        continue;
                     }
 
-                    startX = startX + rightToLeftWidth;
+                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+                    {
+                        var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
 
-                    currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+                        double startOffset;
 
-                    remainingLength -= currentRunBounds.Length;
-                    currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
-                    endX = currentRunBounds.Rectangle.Right;
-                    startX = currentRunBounds.Rectangle.Left;
+                        double endOffset;
 
-                    var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
+                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
 
-                    for (int i = rightToLeftIndex - 1; i >= index; i--)
-                    {
-                        currentShapedRun = TextRuns[i] as ShapedTextCharacters;
+                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
 
-                        if(currentShapedRun == null)
-                        {
-                            continue;
-                        }
+                        startX += startOffset;
 
-                        currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+                        endX += endOffset;
 
-                        rightToLeftRunBounds.Insert(0, currentRunBounds);
+                        var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
 
-                        remainingLength -= currentRunBounds.Length;
-                        startX = currentRunBounds.Rectangle.Left;
+                        var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
 
-                        currentPosition += currentRunBounds.Length;
+                        characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+
+                        currentDirection = FlowDirection.LeftToRight;
                     }
+                    else
+                    {
+                        var rightToLeftIndex = index;
+                        var rightToLeftWidth = currentShapedRun.Size.Width;
 
-                    combinedWidth = endX - startX;
+                        while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
+                        {
+                            if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+                            {
+                                break;
+                            }
 
-                    currentRect = new Rect(startX, 0, combinedWidth, Height);
+                            rightToLeftIndex++;
 
-                    currentDirection = FlowDirection.RightToLeft;
+                            rightToLeftWidth += nextShapedRun.Size.Width;
 
-                    if (!MathUtilities.IsZero(combinedWidth))
-                    {
-                        result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
-                    }
+                            if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
+                            {
+                                break;
+                            }
 
-                    startX = endX;
-                }
-                else
-                {
-                    if (currentShapedRun != null)
-                    {
-                        var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+                            currentShapedRun = nextShapedRun;
+                        }
 
-                        currentPosition += offset;
+                        startX += rightToLeftWidth;
 
-                        var startIndex = currentRun.Text.Start + offset;
+                        currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
 
-                        double startOffset;
-                        double endOffset;
+                        remainingLength -= currentRunBounds.Length;
+                        currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
+                        endX = currentRunBounds.Rectangle.Right;
+                        startX = currentRunBounds.Rectangle.Left;
 
-                        if (currentShapedRun.ShapedBuffer.IsLeftToRight)
-                        {
-                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                        var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
 
-                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
-                        }
-                        else
+                        for (int i = rightToLeftIndex - 1; i >= index; i--)
                         {
-                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
-
-                            if (currentPosition < startIndex)
-                            {
-                                startOffset = endOffset;
-                            }
-                            else
+                            if (TextRuns[i] is not ShapedTextCharacters)
                             {
-                                startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                                continue;
                             }
-                        }
 
-                        startX += startOffset;
+                            currentShapedRun = (ShapedTextCharacters)TextRuns[i];
 
-                        endX += endOffset;
+                            currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
 
-                        var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
-                        var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+                            rightToLeftRunBounds.Insert(0, currentRunBounds);
 
-                        characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+                            remainingLength -= currentRunBounds.Length;
+                            startX = currentRunBounds.Rectangle.Left;
 
-                        currentDirection = FlowDirection.LeftToRight;
-                    }
-                    else
-                    {
-                        if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
-                        {
-                            startX += currentRun.Size.Width;
+                            currentPosition += currentRunBounds.Length;
+                        }
 
-                            currentPosition += currentRun.TextSourceLength;
+                        combinedWidth = endX - startX;
 
-                            continue;
-                        }
+                        currentRect = new Rect(startX, 0, combinedWidth, Height);
+
+                        currentDirection = FlowDirection.RightToLeft;
 
-                        if (currentPosition < firstTextSourceIndex)
+                        if (!MathUtilities.IsZero(combinedWidth))
                         {
-                            startX += currentRun.Size.Width;
+                            result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
                         }
 
-                        if (currentPosition + currentRun.TextSourceLength <= characterIndex)
-                        {
-                            endX += currentRun.Size.Width;
+                        startX = endX;
+                    }
+                }
+                else
+                {
+                    if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+                    {
+                        startX += currentRun.Size.Width;
 
-                            characterLength = currentRun.TextSourceLength;
-                        }
+                        currentPosition += currentRun.Length;
+
+                        continue;
                     }
 
-                    if (endX < startX)
+                    if (currentPosition < firstTextSourceIndex)
                     {
-                        (endX, startX) = (startX, endX);
+                        startX += currentRun.Size.Width;
                     }
 
-                    //Lines that only contain a linebreak need to be covered here
-                    if (characterLength == 0)
+                    if (currentPosition + currentRun.Length <= characterIndex)
                     {
-                        characterLength = NewLineLength;
+                        endX += currentRun.Size.Width;
+
+                        characterLength = currentRun.Length;
                     }
+                }
 
-                    combinedWidth = endX - startX;
+                if (endX < startX)
+                {
+                    (endX, startX) = (startX, endX);
+                }
 
-                    currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
+                //Lines that only contain a linebreak need to be covered here
+                if (characterLength == 0)
+                {
+                    characterLength = NewLineLength;
+                }
 
-                    currentPosition += characterLength;
+                combinedWidth = endX - startX;
 
-                    remainingLength -= characterLength;
+                currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
 
-                    startX = endX;
+                currentPosition += characterLength;
 
-                    if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
-                    {
-                        if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
-                        {
-                            currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
+                remainingLength -= characterLength;
 
-                            var textBounds = result[result.Count - 1];
+                startX = endX;
 
-                            textBounds.Rectangle = currentRect;
+                if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
+                {
+                    if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
+                    {
+                        currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
 
-                            textBounds.TextRunBounds.Add(currentRunBounds);
-                        }
-                        else
-                        {
-                            currentRect = currentRunBounds.Rectangle;
+                        var textBounds = result[result.Count - 1];
 
-                            result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
-                        }
+                        textBounds.Rectangle = currentRect;
+
+                        textBounds.TextRunBounds.Add(currentRunBounds);
                     }
+                    else
+                    {
+                        currentRect = currentRunBounds.Rectangle;
 
-                    lastRunBounds = currentRunBounds;
+                        result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
+                    }
                 }
 
+                lastRunBounds = currentRunBounds;
+
                 currentWidth += combinedWidth;
 
                 if (remainingLength <= 0 || currentPosition >= characterIndex)
@@ -771,11 +757,11 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
-                if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex)
+                if (currentPosition + currentRun.Length < firstTextSourceIndex)
                 {
                     startX -= currentRun.Size.Width;
 
-                    currentPosition += currentRun.TextSourceLength;
+                    currentPosition += currentRun.Length;
 
                     continue;
                 }
@@ -789,7 +775,7 @@ namespace Avalonia.Media.TextFormatting
 
                     currentPosition += offset;
 
-                    var startIndex = currentRun.Text.Start + offset;
+                    var startIndex = currentPosition;
                     double startOffset;
                     double endOffset;
 
@@ -827,7 +813,7 @@ namespace Avalonia.Media.TextFormatting
                 }
                 else
                 {
-                    if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+                    if (currentPosition + currentRun.Length <= characterIndex)
                     {
                         endX -= currentRun.Size.Width;
                     }
@@ -836,7 +822,7 @@ namespace Avalonia.Media.TextFormatting
                     {
                         startX -= currentRun.Size.Width;
 
-                        characterLength = currentRun.TextSourceLength;
+                        characterLength = currentRun.Length;
                     }
                 }
 
@@ -905,7 +891,7 @@ namespace Avalonia.Media.TextFormatting
 
             currentPosition += offset;
 
-            var startIndex = currentRun.Text.Start + offset;
+            var startIndex = currentPosition;
 
             double startOffset;
             double endOffset;
@@ -1172,12 +1158,12 @@ namespace Avalonia.Media.TextFormatting
                                 return true;
                             }
 
-                            var characterIndex = codepointIndex - shapedRun.Text.Start;
+                            //var characterIndex = codepointIndex - shapedRun.Text.Start;
 
-                            if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
-                            {
-                                foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
-                            }
+                            //if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
+                            //{
+                            //    foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+                            //}
 
                             nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
                                 foundCharacterHit :
@@ -1196,7 +1182,7 @@ namespace Avalonia.Media.TextFormatting
 
                             if (textPosition == currentPosition)
                             {
-                                nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+                                nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
 
                                 return true;
                             }
@@ -1205,7 +1191,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                 }
 
-                currentPosition += currentRun.TextSourceLength;
+                currentPosition += currentRun.Length;
                 runIndex++;
             }
 
@@ -1271,7 +1257,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                     default:
                         {
-                            if (characterIndex == currentPosition + currentRun.TextSourceLength)
+                            if (characterIndex == currentPosition + currentRun.Length)
                             {
                                 previousCharacterHit = new CharacterHit(currentPosition);
 
@@ -1282,7 +1268,7 @@ namespace Avalonia.Media.TextFormatting
                         }
                 }
 
-                currentPosition -= currentRun.TextSourceLength;
+                currentPosition -= currentRun.Length;
                 runIndex--;
             }
 
@@ -1310,18 +1296,25 @@ namespace Avalonia.Media.TextFormatting
                 {
                     case ShapedTextCharacters shapedRun:
                         {
+                            var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
+
+                            if (firstCluster > codepointIndex)
+                            {
+                                break;
+                            }
+
                             if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
                             {
                                 if (shapedRun.ShapedBuffer.IsLeftToRight)
                                 {
-                                    if (currentRun.Text.Start >= codepointIndex)
+                                    if (firstCluster >= codepointIndex)
                                     {
                                         return --runIndex;
                                     }
                                 }
                                 else
                                 {
-                                    if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
+                                    if (codepointIndex > firstCluster + currentRun.Length)
                                     {
                                         return --runIndex;
                                     }
@@ -1330,15 +1323,15 @@ namespace Avalonia.Media.TextFormatting
 
                             if (direction == LogicalDirection.Forward)
                             {
-                                if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
+                                if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length)
                                 {
                                     return runIndex;
                                 }
                             }
                             else
                             {
-                                if (codepointIndex > currentRun.Text.Start &&
-                                    codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
+                                if (codepointIndex > firstCluster &&
+                                    codepointIndex <= firstCluster + currentRun.Length)
                                 {
                                     return runIndex;
                                 }
@@ -1349,6 +1342,8 @@ namespace Avalonia.Media.TextFormatting
                                 return runIndex;
                             }
 
+                            textPosition += currentRun.Length;
+
                             break;
                         }
 
@@ -1364,13 +1359,14 @@ namespace Avalonia.Media.TextFormatting
                                 return runIndex;
                             }
 
+                            textPosition += currentRun.Length;
+
                             break;
                         }
                 }
 
                 runIndex++;
                 previousRun = currentRun;
-                textPosition += currentRun.TextSourceLength;
             }
 
             return runIndex;
@@ -1401,7 +1397,7 @@ namespace Avalonia.Media.TextFormatting
                     case ShapedTextCharacters textRun:
                         {
                             var textMetrics =
-                                new TextMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
+                                new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);
 
                             if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
                             {
@@ -1432,7 +1428,7 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
                                 trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
-                                newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+                                newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
                             }
 
                             widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;

+ 3 - 3
src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs

@@ -6,13 +6,13 @@
     /// </summary>
     public readonly struct TextLineMetrics
     {
-        public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline,
+        public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
             int trailingWhitespaceLength, double width,
             double widthIncludingTrailingWhitespace)
         {
             HasOverflowed = hasOverflowed;
             Height = height;
-            NewLineLength = newLineLength;
+            NewlineLength = newlineLength;
             Start = start;
             TextBaseline = textBaseline;
             TrailingWhitespaceLength = trailingWhitespaceLength;
@@ -33,7 +33,7 @@
         /// <summary>
         /// Gets the number of newline characters at the end of a line.
         /// </summary>
-        public int NewLineLength { get; }
+        public int NewlineLength { get; }
         
         /// <summary>
         /// Gets the distance from the start of a paragraph to the starting point of a line.

+ 2 - 2
src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs

@@ -5,9 +5,9 @@
     /// </summary>
     public readonly struct TextMetrics
     {
-        public TextMetrics(Typeface typeface, double fontRenderingEmSize)
+        public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
         {
-            var fontMetrics = typeface.GlyphTypeface.Metrics;
+            var fontMetrics = glyphTypeface.Metrics;
 
             var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;
 

+ 6 - 5
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@@ -1,5 +1,4 @@
 using System.Diagnostics;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -14,12 +13,12 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         ///  Gets the text source length.
         /// </summary>
-        public virtual int TextSourceLength => DefaultTextSourceLength;
+        public virtual int Length => DefaultTextSourceLength;
 
         /// <summary>
         /// Gets the text run's text.
         /// </summary>
-        public virtual ReadOnlySlice<char> Text => default;
+        public virtual CharacterBufferReference CharacterBufferReference => default;
 
         /// <summary>
         /// A set of properties shared by every characters in the run
@@ -41,9 +40,11 @@ namespace Avalonia.Media.TextFormatting
                 {
                     unsafe
                     {
-                        fixed (char* charsPtr = _textRun.Text.Span)
+                        var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
+
+                        fixed (char* charsPtr = characterBuffer.Span)
                         {
-                            return new string(charsPtr, 0, _textRun.Text.Length);
+                            return new string(charsPtr, 0, characterBuffer.Span.Length);
                         }
                     }
                 }

+ 7 - 4
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@@ -1,7 +1,5 @@
 using System;
-using System.Globalization;
 using Avalonia.Platform;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -45,9 +43,14 @@ namespace Avalonia.Media.TextFormatting
         }
 
         /// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
-        public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
+        public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default)
         {
-            return _platformImpl.ShapeText(text, options);
+            return _platformImpl.ShapeText(text, length, options);
+        }
+
+        public ShapedBuffer ShapeText(string text, TextShaperOptions options = default)
+        {
+            return ShapeText(new CharacterBufferReference(text), text.Length, options);
         }
     }
 }

+ 1 - 2
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -15,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="ellipsis">Text used as collapsing symbol.</param>
         /// <param name="width">Width in which collapsing is constrained to.</param>
         /// <param name="textRunProperties">Text run properties of ellipsis symbol.</param>
-        public TextTrailingCharacterEllipsis(ReadOnlySlice<char> ellipsis, double width, TextRunProperties textRunProperties)
+        public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
         {
             Width = width;
             Symbol = new TextCharacters(ellipsis, textRunProperties);

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="width">width in which collapsing is constrained to.</param>
         /// <param name="textRunProperties">text run properties of ellipsis symbol.</param>
         public TextTrailingWordEllipsis(
-            ReadOnlySlice<char> ellipsis,
+            string ellipsis,
             double width,
             TextRunProperties textRunProperties
         )

+ 2 - 1
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@@ -2,6 +2,7 @@
 // Licensed under the Apache License, Version 2.0.
 // Ported from: https://github.com/SixLabors/Fonts/
 
+using System;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode
@@ -63,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(ReadOnlySlice<char> text)
+        public void Append(CharacterBufferRange text)
         {
             _classes.Add(text.Length);
             _pairedBracketTypes.Add(text.Length);

+ 4 - 5
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@@ -1,6 +1,5 @@
-using System;
+using System.Collections.Generic;
 using System.Runtime.CompilerServices;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
@@ -166,11 +165,11 @@ 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(ReadOnlySpan<char> text, int index, out int count)
+        public static Codepoint ReadAt(IReadOnlyList<char> text, int index, out int count)
         {
             count = 1;
 
-            if (index >= text.Length)
+            if (index >= text.Count)
             {
                 return ReplacementCodepoint;
             }
@@ -184,7 +183,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
             {
                 hi = code;
 
-                if (index + 1 == text.Length)
+                if (index + 1 == text.Count)
                 {
                     return ReplacementCodepoint;
                 }

+ 4 - 3
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@@ -1,12 +1,13 @@
-using Avalonia.Utilities;
+using System;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
     public ref struct CodepointEnumerator
     {
-        private ReadOnlySlice<char> _text;
+        private CharacterBufferRange _text;
+        private int _pos;
 
-        public CodepointEnumerator(ReadOnlySlice<char> text)
+        public CodepointEnumerator(CharacterBufferRange text)
         {
             _text = text;
             Current = Codepoint.ReplacementCodepoint;

+ 4 - 4
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@@ -1,13 +1,13 @@
-using Avalonia.Utilities;
+using System;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
     /// <summary>
     /// Represents the smallest unit of a writing system of any given language.
     /// </summary>
-    public readonly struct Grapheme
+    public readonly ref struct Grapheme
     {
-        public Grapheme(Codepoint firstCodepoint, ReadOnlySlice<char> text)
+        public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
         {
             FirstCodepoint = firstCodepoint;
             Text = text;
@@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
         /// <summary>
         /// The text that is representing the <see cref="Grapheme"/>.
         /// </summary>
-        public ReadOnlySlice<char> Text { get; }
+        public ReadOnlySpan<char> Text { get; }
     }
 }

+ 6 - 6
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.Runtime.InteropServices;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
     public ref struct GraphemeEnumerator
     {
-        private ReadOnlySlice<char> _text;
+        private CharacterBufferRange _text;
 
-        public GraphemeEnumerator(ReadOnlySlice<char> text)
+        public GraphemeEnumerator(CharacterBufferRange text)
         {
             _text = text;
             Current = default;
@@ -187,7 +187,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
 
             var text = _text.Take(processor.CurrentCodeUnitOffset);
 
-            Current = new Grapheme(firstCodepoint, text);
+            Current = new Grapheme(firstCodepoint, text.Span);
 
             _text = _text.Skip(processor.CurrentCodeUnitOffset);
 
@@ -197,10 +197,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
         [StructLayout(LayoutKind.Auto)]
         private ref struct Processor
         {
-            private readonly ReadOnlySlice<char> _buffer;
+            private readonly CharacterBufferRange _buffer;
             private int _codeUnitLengthOfCurrentScalar;
 
-            internal Processor(ReadOnlySlice<char> buffer)
+            internal Processor(CharacterBufferRange buffer)
             {
                 _buffer = buffer;
                 _codeUnitLengthOfCurrentScalar = 0;

+ 8 - 7
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

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

+ 3 - 8
src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs

@@ -1,21 +1,16 @@
 using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media
 {
     public sealed class TextLeadingPrefixTrimming : TextTrimming
     {
-        private readonly ReadOnlySlice<char> _ellipsis;
+        private readonly string _ellipsis;
         private readonly int _prefixLength;
 
-        public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength)
-        {
-        }
-
-        public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength)
+        public TextLeadingPrefixTrimming(string ellipsis, int prefixLength)
         {
             _prefixLength = prefixLength;
-            _ellipsis = new ReadOnlySlice<char>(ellipsis);
+            _ellipsis = ellipsis;
         }
 
         public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

+ 3 - 8
src/Avalonia.Base/Media/TextTrailingTrimming.cs

@@ -1,21 +1,16 @@
 using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media
 {
     public sealed class TextTrailingTrimming : TextTrimming
     {
-        private readonly ReadOnlySlice<char> _ellipsis;
+        private readonly string _ellipsis;
         private readonly bool _isWordBased;
-
-        public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased)
-        {
-        }
         
-        public TextTrailingTrimming(char[] ellipsis, bool isWordBased)
+        public TextTrailingTrimming(string ellipsis, bool isWordBased)
         {
             _isWordBased = isWordBased;
-            _ellipsis = new ReadOnlySlice<char>(ellipsis);
+            _ellipsis = ellipsis;
         }
 
         public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

+ 1 - 1
src/Avalonia.Base/Media/TextTrimming.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Media
     /// </summary>
     public abstract class TextTrimming
     {
-        internal const char DefaultEllipsisChar = '\u2026';
+        internal const string DefaultEllipsisChar = "\u2026";
 
         /// <summary>
         /// Text is not trimmed.

+ 2 - 3
src/Avalonia.Base/Platform/ITextShaperImpl.cs

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

+ 2 - 1
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics;
 using System.Globalization;
+using System.Linq;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Platform;
@@ -31,7 +32,7 @@ internal class FpsCounter
         {
             var s = new string((char)c, 1);
             var glyph = typeface.GetGlyph((uint)(s[0]));
-            _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice<char>(s.AsMemory()), new ushort[] { glyph });
+            _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph });
         }
     }
 

+ 0 - 8
src/Avalonia.Base/Utilities/ArraySlice.cs

@@ -111,14 +111,6 @@ namespace Avalonia.Utilities
             }
         }
 
-        /// <summary>
-        /// Defines an implicit conversion of a <see cref="ArraySlice{T}"/> to a <see cref="ReadOnlySlice{T}"/>
-        /// </summary>
-        public static implicit operator ReadOnlySlice<T>(ArraySlice<T> slice)
-        {
-            return new ReadOnlySlice<T>(slice._data, 0, slice.Length, slice.Start);
-        }
-
         /// <summary>
         /// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
         /// </summary>

+ 0 - 239
src/Avalonia.Base/Utilities/ReadOnlySlice.cs

@@ -1,239 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Runtime.CompilerServices;
-
-namespace Avalonia.Utilities
-{
-    /// <summary>
-    ///     ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
-    /// </summary>
-    /// <typeparam name="T">The type of elements in the slice.</typeparam>
-    [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))]
-    public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> where T : struct
-    {
-        private readonly int _bufferOffset;
-        
-        /// <summary>
-        /// Gets an empty <see cref="ReadOnlySlice{T}"/>
-        /// </summary>
-        public static ReadOnlySlice<T> Empty => new ReadOnlySlice<T>(Array.Empty<T>());
-        
-        private readonly ReadOnlyMemory<T> _buffer;
-
-        public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
-
-        public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length, int bufferOffset = 0)
-        {
-#if DEBUG
-            if (start.CompareTo(0) < 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof (start));
-            }
-
-            if (length.CompareTo(buffer.Length) > 0)
-            {
-                throw new ArgumentOutOfRangeException(nameof (length));
-            }
-#endif
-            
-            _buffer = buffer;
-            Start = start;
-            Length = length;
-            _bufferOffset = bufferOffset;
-        }
-
-        /// <summary>
-        ///     Gets the start.
-        /// </summary>
-        /// <value>
-        ///     The start.
-        /// </value>
-        public int Start { get; }
-
-        /// <summary>
-        ///     Gets the end.
-        /// </summary>
-        /// <value>
-        ///     The end.
-        /// </value>
-        public int End => Start + Length - 1;
-
-        /// <summary>
-        ///     Gets the length.
-        /// </summary>
-        /// <value>
-        ///     The length.
-        /// </value>
-        public int Length { get; }
-
-        /// <summary>
-        ///     Gets a value that indicates whether this instance of <see cref="ReadOnlySlice{T}"/> is Empty.
-        /// </summary>
-        public bool IsEmpty => Length == 0;
-
-        /// <summary>
-        ///     Get the underlying span.
-        /// </summary>
-        public ReadOnlySpan<T> Span => _buffer.Span.Slice(_bufferOffset, Length);
-
-        /// <summary>
-        ///     Get the buffer offset.
-        /// </summary>
-        public int BufferOffset => _bufferOffset;
-        
-        /// <summary>
-        ///     Get the underlying buffer.
-        /// </summary>
-        public ReadOnlyMemory<T> Buffer => _buffer;
-
-        /// <summary>
-        /// Returns a value to specified element of the slice.
-        /// </summary>
-        /// <param name="index">The index of the element to return.</param>
-        /// <returns>The <typeparamref name="T"/>.</returns>
-        /// <exception cref="IndexOutOfRangeException">
-        /// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
-        /// </exception>
-        public T 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 Span[index];
-            }
-        }
-        
-        /// <summary>
-        ///     Returns a sub slice of elements that start at the specified index and has the specified number of elements.
-        /// </summary>
-        /// <param name="start">The start of the sub slice.</param>
-        /// <param name="length">The length of the sub slice.</param>
-        /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
-        public ReadOnlySlice<T> AsSlice(int start, int length)
-        {
-            if (IsEmpty)
-            {
-                return this;
-            }
-
-            if (length == 0)
-            {
-                return Empty;
-            }
-
-            if (start < 0 || _bufferOffset + start > _buffer.Length - 1)
-            {
-                throw new ArgumentOutOfRangeException(nameof(start));
-            }
-
-            if (_bufferOffset + start + length > _buffer.Length)
-            {
-                throw new ArgumentOutOfRangeException(nameof(length));
-            }
-
-            return new ReadOnlySlice<T>(_buffer, start, length, _bufferOffset);
-        }
-
-        /// <summary>
-        ///     Returns a specified number of contiguous elements from the start of the slice.
-        /// </summary>
-        /// <param name="length">The number of elements to return.</param>
-        /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
-        public ReadOnlySlice<T> Take(int length)
-        {
-            if (IsEmpty)
-            {
-                return this;
-            }
-
-            if (length > Length)
-            {
-                throw new ArgumentOutOfRangeException(nameof(length));
-            }
-
-            return new ReadOnlySlice<T>(_buffer, Start, length, _bufferOffset);
-        }
-
-        /// <summary>
-        ///     Bypasses a specified number of elements in the slice and then returns the remaining elements.
-        /// </summary>
-        /// <param name="length">The number of elements to skip before returning the remaining elements.</param>
-        /// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
-        public ReadOnlySlice<T> Skip(int length)
-        {
-            if (IsEmpty)
-            {
-                return this;
-            }
-
-            if (length > Length)
-            {
-                throw new ArgumentOutOfRangeException(nameof(length));
-            }
-
-            return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _bufferOffset + length);
-        }
-
-        /// <summary>
-        /// Returns an enumerator for the slice.
-        /// </summary>
-        public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator()
-        {
-            return new ImmutableReadOnlyListStructEnumerator<T>(this);
-        }
-
-        IEnumerator<T> IEnumerable<T>.GetEnumerator()
-        {
-            return GetEnumerator();
-        }
-
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            return GetEnumerator();
-        }
-
-        int IReadOnlyCollection<T>.Count => Length;
-
-        T IReadOnlyList<T>.this[int index] => this[index];
-
-        public static implicit operator ReadOnlySlice<T>(T[] array)
-        {
-            return new ReadOnlySlice<T>(array);
-        }
-
-        public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory)
-        {
-            return new ReadOnlySlice<T>(memory);
-        }
-
-        public static implicit operator ReadOnlySpan<T>(ReadOnlySlice<T> slice) => slice.Span;
-
-        internal class ReadOnlySliceDebugView
-        {
-            private readonly ReadOnlySlice<T> _readOnlySlice;
-
-            public ReadOnlySliceDebugView(ReadOnlySlice<T> readOnlySlice)
-            {
-                _readOnlySlice = readOnlySlice;
-            }
-
-            public int Start => _readOnlySlice.Start;
-
-            public int End => _readOnlySlice.End;
-
-            public int Length => _readOnlySlice.Length;
-
-            public bool IsEmpty => _readOnlySlice.IsEmpty;
-
-            public ReadOnlySpan<T> Items => _readOnlySlice.Span;
-        }
-    }
-}

+ 2 - 2
src/Avalonia.Controls/Documents/LineBreak.cs

@@ -4,7 +4,7 @@ using System.Text;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
 
-namespace Avalonia.Controls.Documents 
+namespace Avalonia.Controls.Documents
 {
     /// <summary>
     /// LineBreak element that forces a line breaking. 
@@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
 
         internal override void BuildTextRun(IList<TextRun> textRuns)
         {
-            var text = Environment.NewLine.AsMemory();
+            var text = Environment.NewLine;
 
             var textRunProperties = CreateTextRunProperties();
 

+ 1 - 1
src/Avalonia.Controls/Documents/Run.cs

@@ -52,7 +52,7 @@ namespace Avalonia.Controls.Documents
 
         internal override void BuildTextRun(IList<TextRun> textRuns)
         {
-            var text = (Text ?? "").AsMemory();
+            var text = Text ?? "";
 
             var textRunProperties = CreateTextRunProperties();           
 

+ 17 - 10
src/Avalonia.Controls/TextBlock.cs

@@ -630,7 +630,7 @@ namespace Avalonia.Controls
             }
             else
             {
-                textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
+                textSource = new SimpleTextSource(text ?? "", defaultProperties);
             }
 
             return new TextLayout(
@@ -829,12 +829,12 @@ namespace Avalonia.Controls
 
         protected readonly struct SimpleTextSource : ITextSource
         {
-            private readonly ReadOnlySlice<char> _text;
+            private readonly CharacterBufferRange _text;
             private readonly TextRunProperties _defaultProperties;
 
-            public SimpleTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties)
+            public SimpleTextSource(string text, TextRunProperties defaultProperties)
             {
-                _text = text;
+                _text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length);
                 _defaultProperties = defaultProperties;
             }
 
@@ -852,7 +852,7 @@ namespace Avalonia.Controls
                     return new TextEndOfParagraph();
                 }
 
-                return new TextCharacters(runText, _defaultProperties);
+                return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties);
             }
         }
 
@@ -873,21 +873,28 @@ namespace Avalonia.Controls
 
                 foreach (var textRun in _textRuns)
                 {
-                    if (textRun.TextSourceLength == 0)
+                    if (textRun.Length == 0)
                     {
                         continue;
                     }
 
-                    if (textSourceIndex >= currentPosition + textRun.TextSourceLength)
+                    if (textSourceIndex >= currentPosition + textRun.Length)
                     {
-                        currentPosition += textRun.TextSourceLength;
+                        currentPosition += textRun.Length;
 
                         continue;
                     }
 
-                    if (textRun is TextCharacters)
+                    if (textRun is TextCharacters)                 
                     {
-                        return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!);
+                        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 textRun;

+ 3 - 1
src/Avalonia.Controls/TextBox.cs

@@ -961,7 +961,9 @@ namespace Avalonia.Controls
 
                 var length = 0;
 
-                var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory());
+                var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length);
+
+                var graphemeEnumerator = new GraphemeEnumerator(inputRange);
 
                 while (graphemeEnumerator.MoveNext())
                 {

+ 5 - 3
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

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

+ 4 - 2
src/Avalonia.Headless/HeadlessPlatformStubs.cs

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

+ 12 - 12
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@@ -3,7 +3,6 @@ using System.Globalization;
 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;
@@ -12,8 +11,9 @@ namespace Avalonia.Skia
 {
     internal class TextShaperImpl : ITextShaperImpl
     {
-        public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
+        public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options)
         {
+            var text = new CharacterBufferRange(characterBufferReference, length);
             var typeface = options.Typeface;
             var fontRenderingEmSize = options.FontRenderingEmSize;
             var bidiLevel = options.BidiLevel;
@@ -21,21 +21,21 @@ namespace Avalonia.Skia
 
             using (var buffer = new Buffer())
             {
-                buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
+                buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, 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 = ((GlyphTypefaceImpl)typeface).Font;
 
                 font.Shape(buffer);
 
-                if(buffer.Direction == Direction.RightToLeft)
+                if (buffer.Direction == Direction.RightToLeft)
                 {
                     buffer.Reverse();
                 }
@@ -64,12 +64,12 @@ namespace Avalonia.Skia
 
                     var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
 
-                    if(text.Buffer.Span[glyphCluster] == '\t')
+                    if (text[i] == '\t')
                     {
                         glyphIndex = typeface.GetGlyph(' ');
 
-                        glyphAdvance = options.IncrementalTabWidth > 0 ? 
-                            options.IncrementalTabWidth : 
+                        glyphAdvance = options.IncrementalTabWidth > 0 ?
+                            options.IncrementalTabWidth :
                             4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
                     }
 
@@ -87,7 +87,7 @@ namespace Avalonia.Skia
             var length = buffer.Length;
 
             var glyphInfos = buffer.GetGlyphInfoSpan();
-            
+
             var second = glyphInfos[length - 1];
 
             if (!new Codepoint(second.Codepoint).IsBreakChar)
@@ -98,7 +98,7 @@ namespace Avalonia.Skia
             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;
@@ -109,7 +109,7 @@ namespace Avalonia.Skia
                     {
                         *p = first;
                     }
-                
+
                     fixed (GlyphInfo* p = &glyphInfos[length - 1])
                     {
                         *p = second;

+ 6 - 5
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@@ -3,7 +3,6 @@ using System.Globalization;
 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;
@@ -12,7 +11,7 @@ namespace Avalonia.Direct2D1.Media
 {
     internal class TextShaperImpl : ITextShaperImpl
     {
-        public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
+        public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options)
         {
             var typeface = options.Typeface;
             var fontRenderingEmSize = options.FontRenderingEmSize;
@@ -21,7 +20,7 @@ namespace Avalonia.Direct2D1.Media
 
             using (var buffer = new Buffer())
             {
-                buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
+                buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length);
 
                 MergeBreakPair(buffer);
 
@@ -46,7 +45,9 @@ namespace Avalonia.Direct2D1.Media
 
                 var bufferLength = buffer.Length;
 
-                var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
+                var characterBufferRange = new CharacterBufferRange(characterBufferReference, length);
+
+                var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
 
                 var glyphInfos = buffer.GetGlyphInfoSpan();
 
@@ -64,7 +65,7 @@ namespace Avalonia.Direct2D1.Media
 
                     var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
 
-                    if (text.Buffer.Span[glyphCluster] == '\t')
+                    if (characterBufferRange[i] == '\t')
                     {
                         glyphIndex = typeface.GetGlyph(' ');
 

+ 1 - 3
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@@ -181,9 +181,7 @@ namespace Avalonia.Base.UnitTests.Media
             var count = glyphAdvances.Length;
             var glyphIndices = new ushort[count];
 
-            var start = bidiLevel == 0 ? glyphClusters[0] : glyphClusters[^1];
-
-            var characters = new ReadOnlySlice<char>(Enumerable.Repeat('a', count).ToArray(), start, count);
+            var characters = Enumerable.Repeat('a', count).ToArray();
 
             return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances,
                 glyphClusters: glyphClusters, biDiLevel: bidiLevel);

+ 2 - 1
tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs

@@ -2,6 +2,7 @@
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Text;
+using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Xunit;
 using Xunit.Abstractions;
@@ -36,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting
             var text = Encoding.UTF32.GetString(MemoryMarshal.Cast<int, byte>(t.CodePoints).ToArray());
 
             // Append
-            bidiData.Append(text.AsMemory());
+            bidiData.Append(new CharacterBufferRange(text));
 
             // Act
             for (int i = 0; i < 10; i++)

+ 4 - 5
tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Runtime.InteropServices;
 using System.Text;
+using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Visuals.UnitTests.Media.TextFormatting;
 using Xunit;
@@ -37,11 +38,11 @@ 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(text.AsMemory());
+            var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
 
             enumerator.MoveNext();
 
-            var actual = enumerator.Current.Text.Span;
+            var actual = enumerator.Current.Text;
 
             var pass = true;
 
@@ -86,9 +87,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting
         {
             const string text = "ABCDEFGHIJ";
 
-            var textMemory = text.AsMemory();
-
-            var enumerator = new GraphemeEnumerator(textMemory);
+            var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
 
             var count = 0;
 

+ 5 - 4
tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs

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

+ 0 - 37
tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs

@@ -1,37 +0,0 @@
-using System.Linq;
-using Avalonia.Utilities;
-using Xunit;
-
-namespace Avalonia.Base.UnitTests.Utilities
-{
-    public class ReadOnlySpanTests
-    {
-        [Fact]
-        public void Should_Skip()
-        {
-            var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
-
-            var slice = new ReadOnlySlice<int>(buffer);
-
-            var skipped = slice.Skip(2);
-
-            var expected = buffer.Skip(2);
-
-            Assert.Equal(expected, skipped);
-        }
-
-        [Fact]
-        public void Should_Take()
-        {
-            var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
-
-            var slice = new ReadOnlySlice<int>(buffer);
-
-            var taken = slice.Take(8);
-
-            var expected = buffer.Take(8);
-
-            Assert.Equal(expected, taken);
-        }
-    }
-}

+ 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.Text.Span.ToString()));
+                    target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.CharacterBufferReference.CharacterBuffer.Span.ToString()));
 
                 Assert.Equal("****", actual);
             }

+ 15 - 14
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(text.AsMemory(), options);
+                    TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
 
                 var glyphRun = CreateGlyphRun(shapedBuffer);
 
@@ -39,8 +39,6 @@ namespace Avalonia.Skia.UnitTests.Media
                 }
                 else
                 {
-                    shapedBuffer.GlyphInfos.Span.Reverse();
-
                     foreach (var rect in rects)
                     {
                         characterHit = glyphRun.GetNextCaretCharacterHit(characterHit);
@@ -62,7 +60,7 @@ namespace Avalonia.Skia.UnitTests.Media
             {
                 var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
                 var shapedBuffer =
-                    TextShaper.Current.ShapeText(text.AsMemory(), options);
+                    TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
 
                 var glyphRun = CreateGlyphRun(shapedBuffer);
 
@@ -84,8 +82,6 @@ namespace Avalonia.Skia.UnitTests.Media
                 }
                 else
                 {
-                    shapedBuffer.GlyphInfos.Span.Reverse();
-
                     foreach (var rect in rects)
                     {
                         characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit);
@@ -107,7 +103,7 @@ namespace Avalonia.Skia.UnitTests.Media
             {
                 var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture);
                 var shapedBuffer =
-                   TextShaper.Current.ShapeText(text.AsMemory(), options);
+                   TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options);
 
                 var glyphRun = CreateGlyphRun(shapedBuffer);
 
@@ -116,16 +112,14 @@ namespace Avalonia.Skia.UnitTests.Media
                     var characterHit =
                         glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _);
                     
-                    Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+                    Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
                 }
                 else
                 {
-                    shapedBuffer.GlyphInfos.Span.Reverse();
-
-                    var characterHit =
+                     var characterHit =
                         glyphRun.GetCharacterHitFromDistance(0, out _);
                     
-                    Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+                    Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
                 }
                 
                 var rects = BuildRects(glyphRun);
@@ -218,15 +212,22 @@ namespace Avalonia.Skia.UnitTests.Media
 
         private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer)
         {
-            return new GlyphRun(
+            var glyphRun =  new GlyphRun(
                 shapedBuffer.GlyphTypeface,
                 shapedBuffer.FontRenderingEmSize,
-                shapedBuffer.Text,
+                shapedBuffer.CharacterBufferRange,
                 shapedBuffer.GlyphIndices,
                 shapedBuffer.GlyphAdvances,
                 shapedBuffer.GlyphOffsets,
                 shapedBuffer.GlyphClusters,
                 shapedBuffer.BidiLevel);
+
+            if(shapedBuffer.BidiLevel == 1)
+            {
+                shapedBuffer.GlyphInfos.Span.Reverse();
+            }
+
+            return glyphRun;
         }
 
         private static IDisposable Start()

+ 1 - 2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs

@@ -29,8 +29,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
             var runText = _runTexts[index];
 
-            return new TextCharacters(
-                new ReadOnlySlice<char>(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle);
+            return new TextCharacters(runText, _defaultStyle);
         }
     }
 }

+ 11 - 8
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs

@@ -1,30 +1,33 @@
-using System;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
+using Avalonia.Media.TextFormatting;
 
 namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 {
     internal class SingleBufferTextSource : ITextSource
     {
-        private readonly ReadOnlySlice<char> _text;
+        private readonly CharacterBufferRange _text;
         private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties;
 
         public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties)
         {
-            _text = text.AsMemory();
+            _text = new CharacterBufferRange(text);
             _defaultGenericPropertiesRunProperties = defaultProperties;
         }
 
         public TextRun GetTextRun(int textSourceIndex)
         {
-            if (textSourceIndex > _text.Length)
+            if (textSourceIndex >= _text.Length)
             {
                 return null;
             }
-            
+
             var runText = _text.Skip(textSourceIndex);
 
-            return runText.IsEmpty ? null : new TextCharacters(runText, _defaultGenericPropertiesRunProperties);
+            if (runText.IsEmpty)
+            {
+                return null;
+            }
+
+            return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultGenericPropertiesRunProperties);
         }
     }
 }

+ 10 - 11
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@@ -37,7 +37,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush);
 
-                Assert.Equal(text.Length, textRun.Text.Length);
+                Assert.Equal(text.Length, textRun.Length);
             }
         }
 
@@ -82,7 +82,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     new ValueSpan<TextRunProperties>(9, 1, defaultProperties)
                 };
 
-                var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns);
+                var textSource = new FormattedTextSource(text, defaultProperties, GenericTextRunPropertiesRuns);
 
                 var formatter = new TextFormatterImpl();
 
@@ -97,7 +97,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                     var textRun = textLine.TextRuns[i];
 
-                    Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length);
+                    Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Length);
                 }
             }
         }
@@ -166,7 +166,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var firstRun = textLine.TextRuns[0];
 
-                Assert.Equal(4, firstRun.Text.Length);
+                Assert.Equal(4, firstRun.Length);
             }
         }
 
@@ -216,7 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         {
             using (Start())
             {
-                var lineBreaker = new LineBreakEnumerator(text.AsMemory());
+                var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text));
 
                 var expected = new List<int>();
 
@@ -369,7 +369,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32))
                 };
 
-                var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans);
+                var textSource = new FormattedTextSource(text, defaultProperties, styleSpans);
 
                 var formatter = new TextFormatterImpl();
 
@@ -389,7 +389,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                     if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
                     {
-                        textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice<char>(new[] { TextTrimming.DefaultEllipsisChar }), 300, defaultProperties));
+                        textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties));
                     }
 
                     currentHeight += textLine.Height;
@@ -472,7 +472,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     var textLine =
                         formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
 
-                    Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
+                    Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.Length));
 
                     textPosition += textLine.Length;
 
@@ -534,7 +534,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                                 new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
                         };
 
-                        var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, spans);
+                        var textSource = new FormattedTextSource(text, defaultProperties, spans);
 
                         var textLine =
                             formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
@@ -614,8 +614,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green);
                 }
 
-                return new TextCharacters(_text.AsMemory(),
-                    new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
+                return new TextCharacters(_text, 0, _text.Length, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
             }
         }
 

+ 24 - 22
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -60,9 +60,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textRun = textLine.TextRuns[1];
 
-                Assert.Equal(2, textRun.Text.Length);
+                Assert.Equal(2, textRun.Length);
 
-                var actual = textRun.Text.Span.ToString();
+                var actual = new CharacterBufferRange(textRun).Span.ToString();
 
                 Assert.Equal("1 ", actual);
 
@@ -144,8 +144,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextCharacters>()
                     .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
 
-                var outer = new GraphemeEnumerator(text.AsMemory());
-                var inner = new GraphemeEnumerator(text.AsMemory());
+                var outer = new GraphemeEnumerator(new CharacterBufferRange(text));
+                var inner = new GraphemeEnumerator(new CharacterBufferRange(text));
                 var i = 0;
                 var j = 0;
 
@@ -190,7 +190,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         break;
                     }
 
-                    inner = new GraphemeEnumerator(text.AsMemory());
+                    inner = new GraphemeEnumerator(new CharacterBufferRange(text));
 
                     i += outer.Current.Text.Length;
                 }
@@ -223,10 +223,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textRun = textLine.TextRuns[0];
 
-                Assert.Equal(2, textRun.Text.Length);
+                Assert.Equal(2, textRun.Length);
 
-                var actual = SingleLineText.Substring(textRun.Text.Start,
-                    textRun.Text.Length);
+                var actual = SingleLineText[..textRun.Length];
 
                 Assert.Equal("01", actual);
 
@@ -260,9 +259,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textRun = textLine.TextRuns[1];
 
-                Assert.Equal(2, textRun.Text.Length);
+                Assert.Equal(2, textRun.Length);
 
-                var actual = textRun.Text.Span.ToString();
+                var actual = new CharacterBufferRange(textRun).Span.ToString();
 
                 Assert.Equal("89", actual);
 
@@ -296,7 +295,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textRun = textLine.TextRuns[0];
 
-                Assert.Equal(1, textRun.Text.Length);
+                Assert.Equal(1, textRun.Length);
 
                 Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
             }
@@ -330,9 +329,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textRun = textLine.TextRuns[1];
 
-                Assert.Equal(2, textRun.Text.Length);
+                Assert.Equal(2, textRun.Length);
 
-                var actual = textRun.Text.Span.ToString();
+                var actual = new CharacterBufferRange(textRun).Span.ToString();
 
                 Assert.Equal("😄", actual);
 
@@ -369,7 +368,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.Equal(
                     MultiLineText.Length,
                     layout.TextLines.Select(textLine =>
-                            textLine.TextRuns.Sum(textRun => textRun.Text.Length))
+                            textLine.TextRuns.Sum(textRun => textRun.Length))
                         .Sum());
             }
         }
@@ -402,7 +401,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.Equal(
                     text.Length,
                     layout.TextLines.Select(textLine =>
-                            textLine.TextRuns.Sum(textRun => textRun.Text.Length))
+                            textLine.TextRuns.Sum(textRun => textRun.Length))
                         .Sum());
             }
         }
@@ -558,7 +557,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textRun = (ShapedTextCharacters)textLine.TextRuns[0];
 
-                Assert.Equal(7, textRun.Text.Length);
+                Assert.Equal(7, textRun.Length);
 
                 var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint);
 
@@ -668,10 +667,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(5, layout.TextLines.Count);
 
-                Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text);
-                Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text);
-                Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text);
-                Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text);
+                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]));
             }
         }
 
@@ -815,7 +814,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     {
                         Assert.True(textLine.Width <= maxWidth);
 
-                        var actual = new string(textLine.TextRuns.Cast<ShapedTextCharacters>().OrderBy(x => x.Text.Start).SelectMany(x => x.Text).ToArray());
+                        var actual = new string(textLine.TextRuns.Cast<ShapedTextCharacters>()
+                            .OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)
+                            .SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray());
+
                         var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
 
                         Assert.Equal(expected, actual);
@@ -966,7 +968,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var i = 0;
 
-                var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory());
+                var graphemeEnumerator = new GraphemeEnumerator(new CharacterBufferRange(text));
 
                 while (graphemeEnumerator.MoveNext())
                 {

+ 46 - 44
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var clusters = new List<int>();
 
-                foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
+                foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar))
                 {
                     var shapedRun = (ShapedTextCharacters)textRun;
 
@@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var clusters = new List<int>();
 
-                foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
+                foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar))
                 {
                     var shapedRun = (ShapedTextCharacters)textRun;
 
@@ -187,14 +187,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     formatter.FormatLine(textSource, 0, double.PositiveInfinity,
                         new GenericTextParagraphProperties(defaultProperties));
 
-                var clusters = textLine.TextRuns.Cast<ShapedTextCharacters>().SelectMany(x => x.ShapedBuffer.GlyphClusters)
-                    .ToArray();
+                var clusters = BuildGlyphClusters(textLine);
 
                 var nextCharacterHit = new CharacterHit(0);
 
-                for (var i = 0; i < clusters.Length; i++)
+                for (var i = 0; i < clusters.Count; i++)
                 {
-                    Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex);
+                    var expectedCluster = clusters[i];
+                    var actualCluster = nextCharacterHit.FirstCharacterIndex;
+
+                    Assert.Equal(expectedCluster, actualCluster);
 
                     nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
                 }
@@ -406,7 +408,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.True(collapsedLine.HasCollapsed);
 
-                var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text).ToArray();
+                var trimmedText = collapsedLine.TextRuns.SelectMany(x => new CharacterBufferRange(x)).ToArray();
 
                 Assert.Equal(expected.Length, trimmedText.Length);
 
@@ -450,8 +452,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 currentHit = textLine.GetNextCaretCharacterHit(currentHit);
 
-                Assert.Equal(3, currentHit.FirstCharacterIndex);
-                Assert.Equal(1, currentHit.TrailingLength);
+                Assert.Equal(4, currentHit.FirstCharacterIndex);
+                Assert.Equal(0, currentHit.TrailingLength);
             }
         }
 
@@ -473,18 +475,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
 
-                Assert.Equal(3, currentHit.FirstCharacterIndex);
-                Assert.Equal(0, currentHit.TrailingLength);
+                Assert.Equal(2, currentHit.FirstCharacterIndex);
+                Assert.Equal(1, currentHit.TrailingLength);
 
                 currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
 
-                Assert.Equal(2, currentHit.FirstCharacterIndex);
-                Assert.Equal(0, currentHit.TrailingLength);
+                Assert.Equal(1, currentHit.FirstCharacterIndex);
+                Assert.Equal(1, currentHit.TrailingLength);
 
                 currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
 
-                Assert.Equal(1, currentHit.FirstCharacterIndex);
-                Assert.Equal(0, currentHit.TrailingLength);
+                Assert.Equal(0, currentHit.FirstCharacterIndex);
+                Assert.Equal(1, currentHit.TrailingLength);
 
                 currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
 
@@ -509,13 +511,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var characterHit = textLine.GetCharacterHitFromDistance(50);
 
-                Assert.Equal(3, characterHit.FirstCharacterIndex);
+                Assert.Equal(5, characterHit.FirstCharacterIndex);
                 Assert.Equal(1, characterHit.TrailingLength);
 
                 characterHit = textLine.GetCharacterHitFromDistance(32);
 
-                Assert.Equal(2, characterHit.FirstCharacterIndex);
-                Assert.Equal(1, characterHit.TrailingLength);
+                Assert.Equal(3, characterHit.FirstCharacterIndex);
+                Assert.Equal(0, characterHit.TrailingLength);
             }
         }
 
@@ -649,7 +651,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     var run = textRuns[i];
                     var bounds = runBounds[i];
 
-                    Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex);
+                    Assert.Equal(run.CharacterBufferReference.OffsetToFirstChar, bounds.TextSourceCharacterIndex);
                     Assert.Equal(run, bounds.TextRun);
                     Assert.Equal(run.Size.Width, bounds.Rectangle.Width);
                 }
@@ -683,13 +685,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 switch (textSourceIndex)
                 {
                     case 0:
-                        return new TextCharacters(new ReadOnlySlice<char>("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default));
+                        return new TextCharacters("aaaaaaaaaa", new GenericTextRunProperties(Typeface.Default));
                     case 10:
-                        return new TextCharacters(new ReadOnlySlice<char>("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default));
+                        return new TextCharacters("bbbbbbbbbb", new GenericTextRunProperties(Typeface.Default));
                     case 20:
-                        return new TextCharacters(new ReadOnlySlice<char>("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default));
+                        return new TextCharacters("cccccccccc", new GenericTextRunProperties(Typeface.Default));
                     case 30:
-                        return new TextCharacters(new ReadOnlySlice<char>("dddddddddd".AsMemory()), new GenericTextRunProperties(Typeface.Default));
+                        return new TextCharacters("dddddddddd", new GenericTextRunProperties(Typeface.Default));
                     default:
                         return null;
                 }
@@ -698,7 +700,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
         private class DrawableRunTextSource : ITextSource
         {
-            const string Text = "_A_A";
+            private const string Text = "_A_A";
 
             public TextRun GetTextRun(int textSourceIndex)
             {
@@ -707,11 +709,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     case 0:
                         return new CustomDrawableRun();
                     case 1:
-                        return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default));
-                    case 2:
+                        return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default));
+                    case 5:
                         return new CustomDrawableRun();
-                    case 3:
-                        return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default));
+                    case 6:
+                        return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default));
                     default:
                         return null;
                 }
@@ -815,19 +817,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             using (Start())
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
-                var text = "0123".AsMemory();
+                var text = "0123";
                 var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
 
-                var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, 1, text.Length), shaperOption), defaultProperties);
+                var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties);
 
                 var textRuns = new List<TextRun>
                 {
                     new CustomDrawableRun(),
                     firstRun,
                     new CustomDrawableRun(),
-                    new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length + 2, text.Length), shaperOption), defaultProperties),
+                    new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties),
                     new CustomDrawableRun(),
-                    new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties)
+                    new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties)
                 };
 
                 var textSource = new FixedRunsTextSource(textRuns);
@@ -838,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     formatter.FormatLine(textSource, 0, double.PositiveInfinity,
                         new GenericTextParagraphProperties(defaultProperties));
 
-                var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
+                var textBounds = textLine.GetTextBounds(0, textLine.Length);
 
                 Assert.Equal(6, textBounds.Count);
                 Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
@@ -848,17 +850,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.Equal(1, textBounds.Count);
                 Assert.Equal(14, textBounds[0].Rectangle.Width);
 
-                textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1);
+                textBounds = textLine.GetTextBounds(0, firstRun.Length + 1);
 
                 Assert.Equal(2, textBounds.Count);
                 Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
 
-                textBounds = textLine.GetTextBounds(1, firstRun.Text.Length);
+                textBounds = textLine.GetTextBounds(1, firstRun.Length);
 
                 Assert.Equal(1, textBounds.Count);
                 Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
 
-                textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1);
+                textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length);
 
                 Assert.Equal(2, textBounds.Count);
                 Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
@@ -878,7 +880,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textLine =
                     formatter.FormatLine(textSource, 0, 200,
-                        new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, 
+                        new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
                         true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
 
                 var textBounds = textLine.GetTextBounds(0, 3);
@@ -899,11 +901,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(2, textBounds.Count);
 
-                Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);           
+                Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
 
                 Assert.Equal(7.201171875, textBounds[1].Rectangle.Width);
 
-                Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left);             
+                Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left);
 
                 textBounds = textLine.GetTextBounds(0, text.Length);
 
@@ -925,7 +927,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var textLine =
                     formatter.FormatLine(textSource, 0, 200,
-                        new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, 
+                        new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left,
                         true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
 
                 var textBounds = textLine.GetTextBounds(0, 4);
@@ -941,13 +943,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(1, textBounds.Count);
 
-                Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length));
+                Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x => x.Length));
                 Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
 
                 textBounds = textLine.GetTextBounds(0, 5);
 
                 Assert.Equal(2, textBounds.Count);
-                Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length)));
+                Assert.Equal(5, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
 
                 Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width);
                 Assert.Equal(7.201171875, textBounds[0].Rectangle.Width);
@@ -960,7 +962,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
                 Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
             }
-        }       
+        }
 
         private class FixedRunsTextSource : ITextSource
         {
@@ -982,7 +984,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         return textRun;
                     }
 
-                    currentPosition += textRun.TextSourceLength;
+                    currentPosition += textRun.Length;
                 }
 
                 return null;

+ 3 - 3
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs

@@ -14,11 +14,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         {
             using (Start())
             {
-                var text = "\n\r\n".AsMemory();
+                var text = "\n\r\n";
                 var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture);
                 var shapedBuffer = TextShaper.Current.ShapeText(text, options);
                 
-                Assert.Equal(shapedBuffer.Text.Length, text.Length);
+                Assert.Equal(shapedBuffer.CharacterBufferRange.Length, text.Length);
                 Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length);
                 Assert.Equal(0, shapedBuffer.GlyphClusters[0]);
                 Assert.Equal(1, shapedBuffer.GlyphClusters[1]);
@@ -31,7 +31,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         {
             using (Start())
             {
-                var text = "\t".AsMemory();
+                var text = "\t";
                 var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100);
                 var shapedBuffer = TextShaper.Current.ShapeText(text, options);
 

+ 5 - 3
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@@ -11,7 +11,7 @@ namespace Avalonia.UnitTests
 {
     public class HarfBuzzTextShaperImpl : ITextShaperImpl
     {
-        public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
+        public ShapedBuffer ShapeText(CharacterBufferReference text, int textLength, TextShaperOptions options)
         {
             var typeface = options.Typeface;
             var fontRenderingEmSize = options.FontRenderingEmSize;
@@ -20,7 +20,7 @@ namespace Avalonia.UnitTests
 
             using (var buffer = new Buffer())
             {
-                buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
+                buffer.AddUtf16(text.CharacterBuffer.Span, text.OffsetToFirstChar, textLength);
 
                 MergeBreakPair(buffer);
                 
@@ -45,7 +45,9 @@ namespace Avalonia.UnitTests
 
                 var bufferLength = buffer.Length;
 
-                var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
+                var characterBufferRange = new CharacterBufferRange(text, textLength);
+
+                var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
 
                 var glyphInfos = buffer.GetGlyphInfoSpan();
 

+ 6 - 6
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@@ -1,24 +1,24 @@
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Platform;
-using Avalonia.Utilities;
 
 namespace Avalonia.UnitTests
 {
     public class MockTextShaperImpl : ITextShaperImpl
     {
-        public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
+        public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options)
         {
             var typeface = options.Typeface;
             var fontRenderingEmSize = options.FontRenderingEmSize;
             var bidiLevel = options.BidiLevel;
-
-            var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
+            var characterBufferRange = new CharacterBufferRange(text, length);
+            var shapedBuffer = new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel);
 
             for (var i = 0; i < shapedBuffer.Length;)
             {
-                var glyphCluster = i + text.Start;
-                var codepoint = Codepoint.ReadAt(text, i, out var count);
+                var glyphCluster = i + text.OffsetToFirstChar;
+
+                var codepoint = Codepoint.ReadAt(characterBufferRange, i, out var count);
 
                 var glyphIndex = typeface.GetGlyph(codepoint);