Browse Source

Merge pull request #9765 from Gillibald/characterBufferReference

Allow TextFormatter to process non text runs
Max Katz 2 years ago
parent
commit
b6eafdf660

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

@@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
                     throw new ArgumentOutOfRangeException(nameof(index));
                 }
 #endif
-                return CharacterBufferReference.CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
+                return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
             }
         }
 
@@ -157,8 +157,7 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Gets a span from the character buffer range
         /// </summary>
-        public ReadOnlySpan<char> Span =>
-            CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
+        public ReadOnlySpan<char> Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length);
 
         /// <summary>
         /// Gets the character memory buffer
@@ -174,7 +173,7 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Indicate whether the character buffer range is empty
         /// </summary>
-        internal bool IsEmpty => CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0;
+        internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0;
 
         internal CharacterBufferRange Take(int length)
         {
@@ -208,9 +207,7 @@ namespace Avalonia.Media.TextFormatting
                 return new CharacterBufferRange(new CharacterBufferReference(), 0);
             }
 
-            var characterBufferReference = new CharacterBufferReference(
-                CharacterBufferReference.CharacterBuffer,
-                CharacterBufferReference.OffsetToFirstChar + length);
+            var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length);
 
             return new CharacterBufferRange(characterBufferReference, Length - length);
         }

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

@@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting
         /// Collapses given text line.
         /// </summary>
         /// <param name="textLine">Text line to collapse.</param>
-        public abstract List<DrawableTextRun>? Collapse(TextLine textLine);
+        public abstract List<TextRun>? Collapse(TextLine textLine);
     }
 }

+ 13 - 10
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -5,9 +5,11 @@ namespace Avalonia.Media.TextFormatting
 {
     internal static class TextEllipsisHelper
     {
-        public static List<DrawableTextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
+        public static List<TextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
         {
-            if (textLine.TextRuns is not List<DrawableTextRun> textRuns || textRuns.Count == 0)
+            var textRuns = textLine.TextRuns;
+
+            if (textRuns == null || textRuns.Count == 0)
             {
                 return null;
             }
@@ -20,7 +22,7 @@ namespace Avalonia.Media.TextFormatting
             if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
             {
                 //Not enough space to fit in the symbol
-                return new List<DrawableTextRun>(0);
+                return new List<TextRun>(0);
             }
 
             var availableWidth = properties.Width - shapedSymbol.Size.Width;
@@ -70,11 +72,11 @@ namespace Avalonia.Media.TextFormatting
 
                                 collapsedLength += measuredLength;
 
-                                var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
+                                var collapsedRuns = new List<TextRun>(textRuns.Count);
 
                                 if (collapsedLength > 0)
                                 {
-                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+                                    var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
 
                                     collapsedRuns.AddRange(splitResult.First);
                                 }
@@ -84,22 +86,21 @@ namespace Avalonia.Media.TextFormatting
                                 return collapsedRuns;
                             }
 
-                            availableWidth -= currentRun.Size.Width;
-
+                            availableWidth -= shapedRun.Size.Width;
 
                             break;
                         }
 
-                    case { } drawableRun:
+                    case DrawableTextRun drawableRun:
                         {
                             //The whole run needs to fit into available space
                             if (currentWidth + drawableRun.Size.Width > availableWidth)
                             {
-                                var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
+                                var collapsedRuns = new List<TextRun>(textRuns.Count);
 
                                 if (collapsedLength > 0)
                                 {
-                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+                                    var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
 
                                     collapsedRuns.AddRange(splitResult.First);
                                 }
@@ -109,6 +110,8 @@ namespace Avalonia.Media.TextFormatting
                                 return collapsedRuns;
                             }
 
+                            availableWidth -= drawableRun.Size.Width;
+
                             break;
                         }
                 }

+ 117 - 101
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Utilities;
 
@@ -17,20 +16,20 @@ namespace Avalonia.Media.TextFormatting
             var textWrapping = paragraphProperties.TextWrapping;
             FlowDirection resolvedFlowDirection;
             TextLineBreak? nextLineBreak = null;
-            List<DrawableTextRun> drawableTextRuns;
+            IReadOnlyList<TextRun> textRuns;
 
-            var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
+            var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex,
                 out var textEndOfLine, out var textSourceLength);
 
             if (previousLineBreak?.RemainingRuns != null)
             {
                 resolvedFlowDirection = previousLineBreak.FlowDirection;
-                drawableTextRuns = previousLineBreak.RemainingRuns.ToList();
+                textRuns = previousLineBreak.RemainingRuns;
                 nextLineBreak = previousLineBreak;
             }
             else
             {
-                drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out resolvedFlowDirection);
+                textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection);
 
                 if (nextLineBreak == null && textEndOfLine != null)
                 {
@@ -44,7 +43,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 case TextWrapping.NoWrap:
                     {
-                        textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
+                        textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength,
                             paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
 
                         textLine.FinalizeLine();
@@ -54,7 +53,7 @@ namespace Avalonia.Media.TextFormatting
                 case TextWrapping.WrapWithOverflow:
                 case TextWrapping.Wrap:
                     {
-                        textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
+                        textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
                             resolvedFlowDirection, nextLineBreak);
                         break;
                     }
@@ -71,7 +70,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textRuns">The text run's.</param>
         /// <param name="length">The length to split at.</param>
         /// <returns>The split text runs.</returns>
-        internal static SplitResult<List<DrawableTextRun>> SplitDrawableRuns(List<DrawableTextRun> textRuns, int length)
+        internal static SplitResult<IReadOnlyList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length)
         {
             var currentLength = 0;
 
@@ -88,7 +87,7 @@ namespace Avalonia.Media.TextFormatting
 
                 var firstCount = currentRun.Length >= 1 ? i + 1 : i;
 
-                var first = new List<DrawableTextRun>(firstCount);
+                var first = new List<TextRun>(firstCount);
 
                 if (firstCount > 1)
                 {
@@ -102,7 +101,7 @@ namespace Avalonia.Media.TextFormatting
 
                 if (currentLength + currentRun.Length == length)
                 {
-                    var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
+                    var second = secondCount > 0 ? new List<TextRun>(secondCount) : null;
 
                     if (second != null)
                     {
@@ -116,13 +115,13 @@ namespace Avalonia.Media.TextFormatting
 
                     first.Add(currentRun);
 
-                    return new SplitResult<List<DrawableTextRun>>(first, second);
+                    return new SplitResult<IReadOnlyList<TextRun>>(first, second);
                 }
                 else
                 {
                     secondCount++;
 
-                    var second = new List<DrawableTextRun>(secondCount);
+                    var second = new List<TextRun>(secondCount);
 
                     if (currentRun is ShapedTextRun shapedTextCharacters)
                     {
@@ -131,18 +130,18 @@ namespace Avalonia.Media.TextFormatting
                         first.Add(split.First);
 
                         second.Add(split.Second!);
-                    }                
+                    }
 
                     for (var j = 1; j < secondCount; j++)
                     {
                         second.Add(textRuns[i + j]);
                     }
 
-                    return new SplitResult<List<DrawableTextRun>>(first, second);
+                    return new SplitResult<IReadOnlyList<TextRun>>(first, second);
                 }
             }
 
-            return new SplitResult<List<DrawableTextRun>>(textRuns, null);
+            return new SplitResult<IReadOnlyList<TextRun>>(textRuns, null);
         }
 
         /// <summary>
@@ -154,11 +153,11 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>
         /// A list of shaped text characters.
         /// </returns>
-        private static List<DrawableTextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
+        private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
             out FlowDirection resolvedFlowDirection)
         {
             var flowDirection = paragraphProperties.FlowDirection;
-            var drawableTextRuns = new List<DrawableTextRun>();
+            var shapedRuns = new List<TextRun>();
             var biDiData = new BidiData((sbyte)flowDirection);
 
             foreach (var textRun in textRuns)
@@ -199,13 +198,6 @@ namespace Avalonia.Media.TextFormatting
 
                 switch (currentRun)
                 {
-                    case DrawableTextRun drawableRun:
-                        {
-                            drawableTextRuns.Add(drawableRun);
-
-                            break;
-                        }
-
                     case UnshapedTextRun shapeableRun:
                         {
                             var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
@@ -245,17 +237,23 @@ namespace Avalonia.Media.TextFormatting
 
                             var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
                                         currentRun.Properties.FontRenderingEmSize,
-                                         shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, 
+                                         shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
                                          paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
 
-                            drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
+                            shapedRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
+
+                            break;
+                        }
+                    default:
+                        {
+                            shapedRuns.Add(currentRun);
 
                             break;
                         }
                 }
             }
 
-            return drawableTextRuns;
+            return shapedRuns;
         }
 
         private static IReadOnlyList<ShapedTextRun> ShapeTogether(
@@ -390,6 +388,10 @@ namespace Avalonia.Media.TextFormatting
 
                 if (textRun == null)
                 {
+                    textRuns.Add(new TextEndOfParagraph());
+
+                    textSourceLength += TextRun.DefaultTextSourceLength;
+
                     break;
                 }
 
@@ -465,7 +467,7 @@ namespace Avalonia.Media.TextFormatting
             return false;
         }
 
-        private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, double paragraphWidth, out int measuredLength)
+        private static bool TryMeasureLength(IReadOnlyList<TextRun> textRuns, double paragraphWidth, out int measuredLength)
         {
             measuredLength = 0;
             var currentWidth = 0.0;
@@ -476,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     case ShapedTextRun shapedTextCharacters:
                         {
-                            if(shapedTextCharacters.ShapedBuffer.Length > 0)
+                            if (shapedTextCharacters.ShapedBuffer.Length > 0)
                             {
                                 var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
                                 var lastCluster = firstCluster;
@@ -497,12 +499,12 @@ namespace Avalonia.Media.TextFormatting
                                 }
 
                                 measuredLength += currentRun.Length;
-                            }                         
+                            }
 
                             break;
                         }
 
-                    case { } drawableTextRun:
+                    case DrawableTextRun drawableTextRun:
                         {
                             if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
                             {
@@ -510,14 +512,20 @@ namespace Avalonia.Media.TextFormatting
                             }
 
                             measuredLength += currentRun.Length;
-                            currentWidth += currentRun.Size.Width;
+                            currentWidth += drawableTextRun.Size.Width;
+
+                            break;
+                        }
+                    default:
+                        {
+                            measuredLength += currentRun.Length;
 
                             break;
                         }
                 }
             }
 
-            found:
+        found:
 
             return measuredLength != 0;
         }
@@ -553,13 +561,13 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="resolvedFlowDirection"></param>
         /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
         /// <returns>The wrapped text line.</returns>
-        private static TextLineImpl PerformTextWrapping(List<DrawableTextRun> textRuns, int firstTextSourceIndex,
+        private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex,
             double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
             TextLineBreak? currentLineBreak)
         {
-            if(textRuns.Count == 0)
+            if (textRuns.Count == 0)
             {
-                return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties);
+                return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
             }
 
             if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@@ -575,46 +583,24 @@ namespace Avalonia.Media.TextFormatting
 
             for (var index = 0; index < textRuns.Count; index++)
             {
-                var currentRun = textRuns[index];
-
-                var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
-
-                var lineBreaker = new LineBreakEnumerator(runText);
-
                 var breakFound = false;
 
-                while (lineBreaker.MoveNext())
-                {
-                    if (lineBreaker.Current.Required &&
-                        currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
-                    {
-                        //Explicit break found
-                        breakFound = true;
-
-                        currentPosition = currentLength + lineBreaker.Current.PositionWrap;
-
-                        break;
-                    }
+                var currentRun = textRuns[index];
 
-                    if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
-                    {
-                        if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
+                switch (currentRun)
+                {
+                    case ShapedTextRun:
                         {
-                            if (lastWrapPosition > 0)
-                            {
-                                currentPosition = lastWrapPosition;
+                            var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
 
-                                breakFound = true;
+                            var lineBreaker = new LineBreakEnumerator(runText);
 
-                                break;
-                            }
-
-                            //Find next possible wrap position (overflow)
-                            if (index < textRuns.Count - 1)
+                            while (lineBreaker.MoveNext())
                             {
-                                if (lineBreaker.Current.PositionWrap != currentRun.Length)
+                                if (lineBreaker.Current.Required &&
+                                    currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
                                 {
-                                    //We already found the next possible wrap position.
+                                    //Explicit break found
                                     breakFound = true;
 
                                     currentPosition = currentLength + lineBreaker.Current.PositionWrap;
@@ -622,51 +608,81 @@ namespace Avalonia.Media.TextFormatting
                                     break;
                                 }
 
-                                while (lineBreaker.MoveNext() && index < textRuns.Count)
+                                if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
                                 {
-                                    currentPosition += lineBreaker.Current.PositionWrap;
-
-                                    if (lineBreaker.Current.PositionWrap != currentRun.Length)
+                                    if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
                                     {
-                                        break;
-                                    }
+                                        if (lastWrapPosition > 0)
+                                        {
+                                            currentPosition = lastWrapPosition;
 
-                                    index++;
+                                            breakFound = true;
+
+                                            break;
+                                        }
+
+                                        //Find next possible wrap position (overflow)
+                                        if (index < textRuns.Count - 1)
+                                        {
+                                            if (lineBreaker.Current.PositionWrap != currentRun.Length)
+                                            {
+                                                //We already found the next possible wrap position.
+                                                breakFound = true;
+
+                                                currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+
+                                                break;
+                                            }
+
+                                            while (lineBreaker.MoveNext() && index < textRuns.Count)
+                                            {
+                                                currentPosition += lineBreaker.Current.PositionWrap;
+
+                                                if (lineBreaker.Current.PositionWrap != currentRun.Length)
+                                                {
+                                                    break;
+                                                }
+
+                                                index++;
+
+                                                if (index >= textRuns.Count)
+                                                {
+                                                    break;
+                                                }
+
+                                                currentRun = textRuns[index];
+
+                                                runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
+
+                                                lineBreaker = new LineBreakEnumerator(runText);
+                                            }
+                                        }
+                                        else
+                                        {
+                                            currentPosition = currentLength + lineBreaker.Current.PositionWrap;
+                                        }
+
+                                        breakFound = true;
 
-                                    if (index >= textRuns.Count)
-                                    {
                                         break;
                                     }
 
-                                    currentRun = textRuns[index];
+                                    //We overflowed so we use the last available wrap position.
+                                    currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition;
 
-                                    runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
+                                    breakFound = true;
 
-                                    lineBreaker = new LineBreakEnumerator(runText);
+                                    break;
                                 }
-                            }
-                            else
-                            {
-                                currentPosition = currentLength + lineBreaker.Current.PositionWrap;
-                            }
 
-                            breakFound = true;
+                                if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
+                                {
+                                    lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
+                                }
+                            }
 
                             break;
                         }
-
-                        //We overflowed so we use the last available wrap position.
-                        currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition;
-
-                        breakFound = true;
-
-                        break;
-                    }
-
-                    if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
-                    {
-                        lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
-                    }
                 }
 
                 if (!breakFound)
@@ -681,12 +697,12 @@ namespace Avalonia.Media.TextFormatting
                 break;
             }
 
-            var splitResult = SplitDrawableRuns(textRuns, measuredLength);
+            var splitResult = SplitTextRuns(textRuns, measuredLength);
 
             var remainingCharacters = splitResult.Second;
 
             var lineBreak = remainingCharacters?.Count > 0 ?
-                new TextLineBreak(currentLineBreak?.TextEndOfLine, resolvedFlowDirection, remainingCharacters) :
+                new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) :
                 null;
 
             if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)

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

@@ -448,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
                 var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
                     _paragraphProperties, previousLine?.TextLineBreak);
 
-                if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+                if(textLine == null || textLine.Length == 0)
                 {
                     if (previousLine != null && previousLine.NewLineLength > 0)
                     {
@@ -501,6 +501,11 @@ namespace Avalonia.Media.TextFormatting
 
                     break;
                 }
+
+                if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+                {
+                    break;
+                }
             }
 
             //Make sure the TextLayout always contains at least on empty line

+ 72 - 61
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -39,9 +39,11 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<DrawableTextRun>? Collapse(TextLine textLine)
+        public override List<TextRun>? Collapse(TextLine textLine)
         {
-            if (textLine.TextRuns is not List<DrawableTextRun> textRuns || textRuns.Count == 0)
+            var textRuns = textLine.TextRuns;
+
+            if (textRuns == null || textRuns.Count == 0)
             {
                 return null;
             }
@@ -52,7 +54,7 @@ namespace Avalonia.Media.TextFormatting
 
             if (Width < shapedSymbol.GlyphRun.Size.Width)
             {
-                return new List<DrawableTextRun>(0);
+                return new List<TextRun>(0);
             }
 
             // Overview of ellipsis structure
@@ -66,92 +68,101 @@ namespace Avalonia.Media.TextFormatting
                 switch (currentRun)
                 {
                     case ShapedTextRun shapedRun:
-                    {
-                        currentWidth += currentRun.Size.Width;
-
-                        if (currentWidth > availableWidth)
                         {
-                            shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
-
-                            var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
+                            currentWidth += shapedRun.Size.Width;
 
-                            if (measuredLength > 0)
+                            if (currentWidth > availableWidth)
                             {
-                                List<DrawableTextRun>? preSplitRuns = null;
-                                List<DrawableTextRun>? postSplitRuns;
-
-                                if (_prefixLength > 0)
-                                {
-                                    var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns,
-                                        Math.Min(_prefixLength, measuredLength));
+                                shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
 
-                                    collapsedRuns.AddRange(splitResult.First);
+                                var collapsedRuns = new List<TextRun>(textRuns.Count);
 
-                                    preSplitRuns = splitResult.First;
-                                    postSplitRuns = splitResult.Second;
-                                }
-                                else
+                                if (measuredLength > 0)
                                 {
-                                    postSplitRuns = textRuns;
-                                }
+                                    IReadOnlyList<TextRun>? preSplitRuns = null;
+                                    IReadOnlyList<TextRun>? postSplitRuns;
 
-                                collapsedRuns.Add(shapedSymbol);
+                                    if (_prefixLength > 0)
+                                    {
+                                        var splitResult = TextFormatterImpl.SplitTextRuns(textRuns,
+                                            Math.Min(_prefixLength, measuredLength));
 
-                                if (measuredLength <= _prefixLength || postSplitRuns is null)
-                                {
-                                    return collapsedRuns;
-                                }
+                                        collapsedRuns.AddRange(splitResult.First);
 
-                                var availableSuffixWidth = availableWidth;
+                                        preSplitRuns = splitResult.First;
+                                        postSplitRuns = splitResult.Second;
+                                    }
+                                    else
+                                    {
+                                        postSplitRuns = textRuns;
+                                    }
 
-                                if (preSplitRuns is not null)
-                                {
-                                    foreach (var run in preSplitRuns)
+                                    collapsedRuns.Add(shapedSymbol);
+
+                                    if (measuredLength <= _prefixLength || postSplitRuns is null)
                                     {
-                                        availableSuffixWidth -= run.Size.Width;
+                                        return collapsedRuns;
                                     }
-                                }
 
-                                for (var i = postSplitRuns.Count - 1; i >= 0; i--)
-                                {
-                                    var run = postSplitRuns[i];
+                                    var availableSuffixWidth = availableWidth;
 
-                                    switch (run)
+                                    if (preSplitRuns is not null)
                                     {
-                                        case ShapedTextRun endShapedRun:
+                                        foreach (var run in preSplitRuns)
                                         {
-                                            if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
-                                                    out var suffixCount, out var suffixWidth))
+                                            if (run is DrawableTextRun drawableTextRun)
                                             {
-                                                availableSuffixWidth -= suffixWidth;
+                                                availableSuffixWidth -= drawableTextRun.Size.Width;
+                                            }
+                                        }
+                                    }
 
-                                                if (suffixCount > 0)
+                                    for (var i = postSplitRuns.Count - 1; i >= 0; i--)
+                                    {
+                                        var run = postSplitRuns[i];
+
+                                        switch (run)
+                                        {
+                                            case ShapedTextRun endShapedRun:
                                                 {
-                                                    var splitSuffix =
-                                                        endShapedRun.Split(run.Length - suffixCount);
+                                                    if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
+                                                            out var suffixCount, out var suffixWidth))
+                                                    {
+                                                        availableSuffixWidth -= suffixWidth;
 
-                                                    collapsedRuns.Add(splitSuffix.Second!);
-                                                }
-                                            }
+                                                        if (suffixCount > 0)
+                                                        {
+                                                            var splitSuffix =
+                                                                endShapedRun.Split(run.Length - suffixCount);
+
+                                                            collapsedRuns.Add(splitSuffix.Second!);
+                                                        }
+                                                    }
 
-                                            break;
+                                                    break;
+                                                }
                                         }
                                     }
                                 }
-                            }
-                            else
-                            {
-                                collapsedRuns.Add(shapedSymbol);
+                                else
+                                {
+                                    collapsedRuns.Add(shapedSymbol);
+                                }
+
+                                return collapsedRuns;
                             }
 
-                            return collapsedRuns;
-                        }
+                            availableWidth -= shapedRun.Size.Width;
 
-                        break;
-                    }
-                }
+                            break;
+                        }
+                    case DrawableTextRun drawableTextRun:
+                        {
+                            availableWidth -= drawableTextRun.Size.Width;
 
-                availableWidth -= currentRun.Size.Width;
+                            break;
+                        }
+                }            
 
                 runIndex++;
             }

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

@@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting
     public class TextLineBreak
     {
         public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, 
-            IReadOnlyList<DrawableTextRun>? remainingRuns = null)
+            IReadOnlyList<TextRun>? remainingRuns = null)
         {
             TextEndOfLine = textEndOfLine;
             FlowDirection = flowDirection;
@@ -25,6 +25,6 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Get the remaining runs that were split up by the <see cref="TextFormatter"/> during the formatting process.
         /// </summary>
-        public IReadOnlyList<DrawableTextRun>? RemainingRuns { get; }
+        public IReadOnlyList<TextRun>? RemainingRuns { get; }
     }
 }

+ 76 - 30
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -6,13 +6,13 @@ namespace Avalonia.Media.TextFormatting
 {
     internal class TextLineImpl : TextLine
     {
-        private readonly List<DrawableTextRun> _textRuns;
+        private IReadOnlyList<TextRun> _textRuns;
         private readonly double _paragraphWidth;
         private readonly TextParagraphProperties _paragraphProperties;
         private TextLineMetrics _textLineMetrics;
         private readonly FlowDirection _resolvedFlowDirection;
 
-        public TextLineImpl(List<DrawableTextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
+        public TextLineImpl(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
             TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
             TextLineBreak? lineBreak = null, bool hasCollapsed = false)
         {
@@ -86,11 +86,14 @@ namespace Avalonia.Media.TextFormatting
 
             foreach (var textRun in _textRuns)
             {
-                var offsetY = GetBaselineOffset(this, textRun);
+                if (textRun is DrawableTextRun drawable)
+                {
+                    var offsetY = GetBaselineOffset(this, drawable);
 
-                textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
+                    drawable.Draw(drawingContext, new Point(currentX, currentY + offsetY));
 
-                currentX += textRun.Size.Width;
+                    currentX += drawable.Size.Width;
+                }
             }
         }
 
@@ -180,7 +183,14 @@ namespace Avalonia.Media.TextFormatting
             {
                 var lastRun = _textRuns[_textRuns.Count - 1];
 
-                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
+                var size = 0.0;
+
+                if (lastRun is DrawableTextRun drawableTextRun)
+                {
+                    size = drawableTextRun.Size.Width;
+                }
+
+                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size);
             }
 
             // process hit that happens within the line
@@ -220,9 +230,16 @@ namespace Avalonia.Media.TextFormatting
 
                         currentRun = _textRuns[j];
 
-                        if (currentDistance + currentRun.Size.Width <= distance)
+                        if(currentRun is not ShapedTextRun)
+                        {
+                            continue;
+                        }
+
+                        shapedRun = (ShapedTextRun)currentRun;
+
+                        if (currentDistance + shapedRun.Size.Width <= distance)
                         {
-                            currentDistance += currentRun.Size.Width;
+                            currentDistance += shapedRun.Size.Width;
                             currentPosition -= currentRun.Length;
 
                             continue;
@@ -234,12 +251,19 @@ namespace Avalonia.Media.TextFormatting
 
                 characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
 
-                if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
+                if (currentRun is DrawableTextRun drawableTextRun)
                 {
-                    currentDistance += currentRun.Size.Width;
+                    if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance)
+                    {
+                        currentDistance += drawableTextRun.Size.Width;
 
-                    currentPosition += currentRun.Length;
+                        currentPosition += currentRun.Length;
 
+                        continue;
+                    }
+                }
+                else
+                {
                     continue;
                 }
 
@@ -249,7 +273,7 @@ namespace Avalonia.Media.TextFormatting
             return characterHit;
         }
 
-        private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance)
+        private static CharacterHit GetRunCharacterHit(TextRun run, int currentPosition, double distance)
         {
             CharacterHit characterHit;
 
@@ -270,9 +294,9 @@ namespace Avalonia.Media.TextFormatting
 
                         break;
                     }
-                default:
+                case DrawableTextRun drawableTextRun:
                     {
-                        if (distance < run.Size.Width / 2)
+                        if (distance < drawableTextRun.Size.Width / 2)
                         {
                             characterHit = new CharacterHit(currentPosition);
                         }
@@ -282,6 +306,10 @@ namespace Avalonia.Media.TextFormatting
                         }
                         break;
                     }
+                default:
+                    characterHit = new CharacterHit(currentPosition, run.Length);
+
+                    break;
             }
 
             return characterHit;
@@ -307,7 +335,7 @@ namespace Avalonia.Media.TextFormatting
                     {
                         var i = index;
 
-                        var rightToLeftWidth = currentRun.Size.Width;
+                        var rightToLeftWidth = shapedRun.Size.Width;
 
                         while (i + 1 <= _textRuns.Count - 1)
                         {
@@ -317,7 +345,7 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 i++;
 
-                                rightToLeftWidth += nextRun.Size.Width;
+                                rightToLeftWidth += nextShapedRun.Size.Width;
 
                                 continue;
                             }
@@ -331,7 +359,10 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 currentRun = _textRuns[i];
 
-                                rightToLeftWidth -= currentRun.Size.Width;
+                                if (currentRun is DrawableTextRun drawable)
+                                {
+                                    rightToLeftWidth -= drawable.Size.Width;
+                                }
 
                                 if (currentPosition + currentRun.Length >= characterIndex)
                                 {
@@ -355,8 +386,13 @@ namespace Avalonia.Media.TextFormatting
                         return Math.Max(0, currentDistance + distance);
                     }
 
+                    if (currentRun is DrawableTextRun drawableTextRun)
+                    {
+                        currentDistance += drawableTextRun.Size.Width;
+                    }
+
                     //No hit hit found so we add the full width
-                    currentDistance += currentRun.Size.Width;
+
                     currentPosition += currentRun.Length;
                     remainingLength -= currentRun.Length;
                 }
@@ -380,8 +416,12 @@ namespace Avalonia.Media.TextFormatting
                         return Math.Max(0, currentDistance - distance);
                     }
 
+                    if (currentRun is DrawableTextRun drawableTextRun)
+                    {
+                        currentDistance -= drawableTextRun.Size.Width;
+                    }
+
                     //No hit hit found so we add the full width
-                    currentDistance -= currentRun.Size.Width;
                     currentPosition += currentRun.Length;
                     remainingLength -= currentRun.Length;
                 }
@@ -391,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
         }
 
         private static bool TryGetDistanceFromCharacterHit(
-            DrawableTextRun currentRun,
+            TextRun currentRun,
             CharacterHit characterHit,
             int currentPosition,
             int remainingLength,
@@ -432,7 +472,7 @@ namespace Avalonia.Media.TextFormatting
 
                         break;
                     }
-                default:
+                case DrawableTextRun drawableTextRun:
                     {
                         if (characterIndex == currentPosition)
                         {
@@ -441,7 +481,7 @@ namespace Avalonia.Media.TextFormatting
 
                         if (characterIndex == currentPosition + currentRun.Length)
                         {
-                            distance = currentRun.Size.Width;
+                            distance = drawableTextRun.Size.Width;
 
                             return true;
 
@@ -449,6 +489,10 @@ namespace Avalonia.Media.TextFormatting
 
                         break;
                     }
+                default:
+                    {
+                        return false;
+                    }
             }
 
             return false;
@@ -943,7 +987,7 @@ namespace Avalonia.Media.TextFormatting
             return this;
         }
 
-        private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
+        private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
         {
             if (run is ShapedTextRun shapedTextCharacters)
             {
@@ -1039,16 +1083,18 @@ namespace Avalonia.Media.TextFormatting
                 minLevelToReverse--;
             }
 
-            _textRuns.Clear();
+            var textRuns = new List<TextRun>(_textRuns.Count);
 
             current = orderedRun;
 
             while (current != null)
             {
-                _textRuns.Add(current.Run);
+                textRuns.Add(current.Run);
 
                 current = current.Next;
             }
+
+            _textRuns = textRuns;
         }
 
         /// <summary>
@@ -1286,7 +1332,7 @@ namespace Avalonia.Media.TextFormatting
         {
             var runIndex = 0;
             textPosition = FirstTextSourceIndex;
-            DrawableTextRun? previousRun = null;
+            TextRun? previousRun = null;
 
             while (runIndex < _textRuns.Count)
             {
@@ -1346,7 +1392,6 @@ namespace Avalonia.Media.TextFormatting
 
                             break;
                         }
-
                     default:
                         {
                             if (codepointIndex == textPosition)
@@ -1363,6 +1408,7 @@ namespace Avalonia.Media.TextFormatting
 
                             break;
                         }
+
                 }
 
                 runIndex++;
@@ -1436,7 +1482,7 @@ namespace Avalonia.Media.TextFormatting
                             break;
                         }
 
-                    case { } drawableTextRun:
+                    case DrawableTextRun drawableTextRun:
                         {
                             widthIncludingWhitespace += drawableTextRun.Size.Width;
 
@@ -1558,7 +1604,7 @@ namespace Avalonia.Media.TextFormatting
 
         private sealed class OrderedBidiRun
         {
-            public OrderedBidiRun(DrawableTextRun run, sbyte level)
+            public OrderedBidiRun(TextRun run, sbyte level)
             {
                 Run = run;
                 Level = level;
@@ -1566,7 +1612,7 @@ namespace Avalonia.Media.TextFormatting
 
             public sbyte Level { get; }
 
-            public DrawableTextRun Run { get; }
+            public TextRun Run { get; }
 
             public OrderedBidiRun? Next { get; set; }
         }

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

@@ -40,11 +40,11 @@ namespace Avalonia.Media.TextFormatting
                 {
                     unsafe
                     {
-                        var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
+                        var characterBuffer = new CharacterBufferRange(_textRun.CharacterBufferReference, _textRun.Length);
 
                         fixed (char* charsPtr = characterBuffer.Span)
                         {
-                            return new string(charsPtr, _textRun.CharacterBufferReference.OffsetToFirstChar, _textRun.Length);
+                            return new string(charsPtr, 0, _textRun.Length);
                         }
                     }
                 }

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

@@ -26,7 +26,7 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<DrawableTextRun>? Collapse(TextLine textLine)
+        public override List<TextRun>? Collapse(TextLine textLine)
         {
             return TextEllipsisHelper.Collapse(textLine, this, false);
         }

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

@@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextRun Symbol { get; }
 
-        public override List<DrawableTextRun>? Collapse(TextLine textLine)
+        public override List<TextRun>? Collapse(TextLine textLine)
         {
             return TextEllipsisHelper.Collapse(textLine, this, true);
         }

+ 5 - 0
src/Avalonia.Controls/Documents/Run.cs

@@ -54,6 +54,11 @@ namespace Avalonia.Controls.Documents
         {
             var text = Text ?? "";
 
+            if (string.IsNullOrEmpty(text))
+            {
+                return;
+            }
+
             var textRunProperties = CreateTextRunProperties();           
 
             var textCharacters = new TextCharacters(text, textRunProperties);

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

@@ -1,6 +1,4 @@
-using System;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
+using Avalonia.Media.TextFormatting;
 
 namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 {

+ 63 - 0
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@@ -62,6 +62,69 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             }
         }
 
+        private class TextSourceWithDummyRuns : ITextSource
+        {
+            private readonly TextRunProperties _properties;
+            private readonly List<ValueSpan<TextRun>> _textRuns;
+
+            public TextSourceWithDummyRuns(TextRunProperties properties)
+            {
+                _properties = properties;
+
+                _textRuns = new List<ValueSpan<TextRun>>
+                {
+                    new ValueSpan<TextRun>(0, 5, new TextCharacters("Hello", _properties)),
+                    new ValueSpan<TextRun>(5, 1, new DummyRun()),
+                    new ValueSpan<TextRun>(6, 1, new DummyRun()),
+                    new ValueSpan<TextRun>(7, 6, new TextCharacters(" World", _properties))
+                };
+            }
+
+            public TextRun GetTextRun(int textSourceIndex)
+            {
+                foreach (var run in _textRuns)
+                {
+                    if (textSourceIndex < run.Start + run.Length)
+                    {
+                        return run.Value;
+                    }
+                }
+
+                return new TextEndOfParagraph();
+            }
+
+            private class DummyRun : TextRun
+            {
+                public DummyRun()
+                {
+                    Length = DefaultTextSourceLength;
+                }
+
+                public override int Length { get; }
+            }
+        }
+
+        [Fact]
+        public void Should_Format_TextLine_With_Non_Text_TextRuns()
+        {
+            using (Start())
+            {
+                var defaultProperties =
+                    new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
+
+                var textSource = new TextSourceWithDummyRuns(defaultProperties);
+
+                var formatter = new TextFormatterImpl();
+
+                var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+                    new GenericTextParagraphProperties(defaultProperties));
+
+                Assert.Equal(5, textLine.TextRuns.Count);
+
+                Assert.Equal(14, textLine.Length);
+            }
+        }
+
         [Fact]
         public void Should_Format_TextRuns_With_TextRunStyles()
         {

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

@@ -326,7 +326,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     }
                 }
 
-                Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
+                var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length));
+
+                Assert.Equal(currentDistance, actualDistance);
             }
         }