浏览代码

Better WPF compat
Disable run sort

Benedikt Stebner 3 年之前
父节点
当前提交
59d8f86509

+ 2 - 2
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -660,7 +660,7 @@ namespace Avalonia.Controls.Presenters
 
                     caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
 
-                    if (textLine.NewLineLength > 0 && caretIndex == textLine.TextRange.Start + textLine.TextRange.Length)
+                    if (textLine.NewLineLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length)
                     {
                         characterHit = new CharacterHit(caretIndex);
                     }
@@ -672,7 +672,7 @@ namespace Avalonia.Controls.Presenters
                         break;
                     }
 
-                    if (caretIndex - textLine.NewLineLength == textLine.TextRange.Start + textLine.TextRange.Length)
+                    if (caretIndex - textLine.NewLineLength == textLine.FirstTextSourceIndex + textLine.Length)
                     {
                         break;
                     }

+ 2 - 2
src/Avalonia.Controls/TextBox.cs

@@ -1390,7 +1390,7 @@ namespace Avalonia.Controls
                 var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
                 var textLine = textLines[lineIndex];
 
-                _presenter.MoveCaretToTextPosition(textLine.TextRange.Start);
+                _presenter.MoveCaretToTextPosition(textLine.FirstTextSourceIndex);
             }
         }
 
@@ -1414,7 +1414,7 @@ namespace Avalonia.Controls
                 var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
                 var textLine = textLines[lineIndex];
 
-                var textPosition = textLine.TextRange.Start + textLine.TextRange.Length;
+                var textPosition = textLine.FirstTextSourceIndex + textLine.Length;
 
                 _presenter.MoveCaretToTextPosition(textPosition, true);
             }

+ 6 - 1
src/Avalonia.Visuals/ApiCompatBaseline.txt

@@ -96,8 +96,10 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.
 MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLayout.Size.get()' does not exist in the implementation but it does exist in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.FirstTextSourceIndex' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.Length' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading' is abstract in the implementation but is missing in the contract.
@@ -110,15 +112,18 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
 MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
 CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.FirstTextSourceIndex.get()' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList<Avalonia.Media.TextFormatting.TextBounds> Avalonia.Media.TextFormatting.TextLine.GetTextBounds(System.Int32, System.Int32)' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed.get()' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.Length.get()' is abstract in the implementation but is missing in the contract.
 MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLine.LineMetrics.get()' does not exist in the implementation but it does exist in the contract.
 CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength.get()' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter.get()' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading.get()' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing.get()' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextRange Avalonia.Media.TextFormatting.TextLine.TextRange.get()' does not exist in the implementation but it does exist in the contract.
 CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength.get()' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width.get()' is abstract in the implementation but is missing in the contract.
 CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace.get()' is abstract in the implementation but is missing in the contract.
@@ -178,4 +183,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphR
 MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable<System.Int32>)' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice<T>..ctor(System.ReadOnlyMemory<T>, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 179
+Total Issues: 184

+ 6 - 6
src/Avalonia.Visuals/Media/FormattedText.cs

@@ -768,7 +768,7 @@ namespace Avalonia.Media
                 // as a result of the next line measurement
 
                 // maybe there is no next line at all
-                if (Position + Current.TextRange.Length < _that._text.Length)
+                if (Position + Current.Length < _that._text.Length)
                 {
                     bool nextLineFits;
 
@@ -780,7 +780,7 @@ namespace Avalonia.Media
                     {
                         _nextLine = FormatLine(
                             _textSource,
-                            Position + Current.TextRange.Length,
+                            Position + Current.Length,
                             MaxLineLength(_lineCount + 1),
                             _that._defaultParaProps,
                             currentLineBreak
@@ -819,7 +819,7 @@ namespace Avalonia.Media
 
                 _previousHeight = Current.Height;
 
-                Length = Current.TextRange.Length;
+                Length = Current.Length;
 
                 _previousLineBreak = currentLineBreak;
 
@@ -839,17 +839,17 @@ namespace Avalonia.Media
                     lineBreak
                     );
 
-                if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.TextRange.Length > 0)
+                if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
                 {
                     // what I really need here is the last displayed text run of the line
                     // textSourcePosition + line.Length - 1 works except the end of paragraph case,
                     // where line length includes the fake paragraph break run
-                    Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.TextRange.Length <= _that._text.Length + 1);
+                    Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.Length <= _that._text.Length + 1);
 
                     var thatFormatRider = new SpanRider(
                         _that._formatRuns,
                         _that._latestPosition,
-                        Math.Min(textSourcePosition + line.TextRange.Length - 1, _that._text.Length - 1)
+                        Math.Min(textSourcePosition + line.Length - 1, _that._text.Length - 1)
                         );
 
                     var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;

+ 1 - 6
src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs

@@ -15,7 +15,6 @@ namespace Avalonia.Media.TextFormatting
             var runIndex = 0;
             var currentWidth = 0.0;
             var collapsedLength = 0;
-            var textRange = textLine.TextRange;
             var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);
 
             if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
@@ -40,7 +39,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
                             {
-                                if (isWordEllipsis && measuredLength < textRange.Length)
+                                if (isWordEllipsis && measuredLength < textLine.Length)
                                 {
                                     var currentBreakPosition = 0;
 
@@ -76,8 +75,6 @@ namespace Avalonia.Media.TextFormatting
                                 var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
 
                                 collapsedRuns.AddRange(splitResult.First);
-
-                                TextLineImpl.SortRuns(collapsedRuns);
                             }
 
                             collapsedRuns.Add(shapedSymbol);
@@ -103,8 +100,6 @@ namespace Avalonia.Media.TextFormatting
                                 var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
 
                                 collapsedRuns.AddRange(splitResult.First);
-
-                                TextLineImpl.SortRuns(collapsedRuns);
                             }
 
                             collapsedRuns.Add(shapedSymbol);

+ 98 - 104
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
             List<DrawableTextRun> drawableTextRuns;
 
             var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
-                out var textEndOfLine, out var textRange);
+                out var textEndOfLine, out var textSourceLength);
 
             if (previousLineBreak?.RemainingRuns != null)
             {
@@ -30,7 +30,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out flowDirection);
 
-                if(nextLineBreak == null && textEndOfLine != null)
+                if (nextLineBreak == null && textEndOfLine != null)
                 {
                     nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection);
                 }
@@ -42,10 +42,8 @@ namespace Avalonia.Media.TextFormatting
             {
                 case TextWrapping.NoWrap:
                     {
-                        TextLineImpl.SortRuns(drawableTextRuns);
-
-                        textLine = new TextLineImpl(drawableTextRuns, textRange, paragraphWidth, paragraphProperties,
-                        flowDirection, nextLineBreak);
+                        textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
+                            paragraphWidth, paragraphProperties, flowDirection, nextLineBreak);
 
                         textLine.FinalizeLine();
 
@@ -54,7 +52,7 @@ namespace Avalonia.Media.TextFormatting
                 case TextWrapping.WrapWithOverflow:
                 case TextWrapping.Wrap:
                     {
-                        textLine = PerformTextWrapping(drawableTextRuns, textRange, paragraphWidth, paragraphProperties,
+                        textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
                             flowDirection, nextLineBreak);
                         break;
                     }
@@ -82,7 +80,7 @@ namespace Avalonia.Media.TextFormatting
                 if (currentLength + currentRun.Text.Length < length)
                 {
                     currentLength += currentRun.TextSourceLength;
-                    
+
                     continue;
                 }
 
@@ -156,7 +154,7 @@ 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<DrawableTextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
             out FlowDirection resolvedFlowDirection)
         {
             var flowDirection = paragraphProperties.FlowDirection;
@@ -176,7 +174,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     biDiData.Append(textRun.Text);
                 }
-                
+
             }
 
             var biDi = BidiAlgorithm.Instance.Value!;
@@ -202,63 +200,63 @@ namespace Avalonia.Media.TextFormatting
                 switch (currentRun)
                 {
                     case DrawableTextRun drawableRun:
-                    {
-                        drawableTextRuns.Add(drawableRun);
+                        {
+                            drawableTextRuns.Add(drawableRun);
 
-                        break;
-                    }
+                            break;
+                        }
 
                     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;
-
-                        while (index + 1 < processedRuns.Count)
                         {
-                            if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun)
-                            {
-                                break;
-                            }
+                            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;
 
-                            if (shapeableRun.CanShapeTogether(nextRun))
+                            while (index + 1 < processedRuns.Count)
                             {
-                                groupedRuns.Add(nextRun);
-
-                                length += nextRun.Text.Length;
-
-                                if (start > nextRun.Text.Start)
+                                if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun)
                                 {
-                                    start = nextRun.Text.Start;
+                                    break;
                                 }
 
-                                if (bufferOffset > nextRun.Text.BufferOffset)
+                                if (shapeableRun.CanShapeTogether(nextRun))
                                 {
-                                    bufferOffset = nextRun.Text.BufferOffset;
-                                }
+                                    groupedRuns.Add(nextRun);
 
-                                text = new ReadOnlySlice<char>(text.Buffer, start, length, bufferOffset);
+                                    length += nextRun.Text.Length;
 
-                                index++;
+                                    if (start > nextRun.Text.Start)
+                                    {
+                                        start = nextRun.Text.Start;
+                                    }
 
-                                shapeableRun = nextRun;
+                                    if (bufferOffset > nextRun.Text.BufferOffset)
+                                    {
+                                        bufferOffset = nextRun.Text.BufferOffset;
+                                    }
 
-                                continue;
-                            }
+                                    text = new ReadOnlySlice<char>(text.Buffer, start, length, bufferOffset);
 
-                            break;
-                        }
+                                    index++;
+
+                                    shapeableRun = nextRun;
 
-                        var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
-                                    currentRun.Properties.FontRenderingEmSize,
-                                     shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
+                                    continue;
+                                }
 
-                        drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
+                                break;
+                            }
 
-                        break;
-                    }
+                            var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
+                                        currentRun.Properties.FontRenderingEmSize,
+                                         shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
+
+                            drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
+
+                            break;
+                        }
                 }
             }
 
@@ -317,14 +315,14 @@ namespace Avalonia.Media.TextFormatting
                 if (currentRun == null)
                 {
                     var drawableRun = textCharacters[i];
-                    
+
                     yield return new[] { drawableRun };
 
                     levelIndex += drawableRun.TextSourceLength;
-                    
+
                     continue;
                 }
-                
+
                 runText = currentRun.Text;
 
                 for (; j < runText.Length;)
@@ -379,14 +377,14 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textSource">The text source.</param>
         /// <param name="firstTextSourceIndex">The first text source index.</param>
         /// <param name="endOfLine"></param>
-        /// <param name="textRange"></param>
+        /// <param name="textSourceLength"></param>
         /// <returns>
         /// The formatted text runs.
         /// </returns>
         private static List<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
-            out TextEndOfLine? endOfLine, out TextRange textRange)
+            out TextEndOfLine? endOfLine, out int textSourceLength)
         {
-            var length = 0;
+            textSourceLength = 0;
 
             endOfLine = null;
 
@@ -398,7 +396,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 var textRun = textRunEnumerator.Current;
 
-                if(textRun == null)
+                if (textRun == null)
                 {
                     break;
                 }
@@ -408,9 +406,9 @@ namespace Avalonia.Media.TextFormatting
                     endOfLine = textEndOfLine;
 
                     textRuns.Add(textRun);
-                    
-                    length += textRun.TextSourceLength;
-                    
+
+                    textSourceLength += textRun.TextSourceLength;
+
                     break;
                 }
 
@@ -425,9 +423,7 @@ namespace Avalonia.Media.TextFormatting
 
                                 textRuns.Add(splitResult);
 
-                                length += runLineBreak.PositionWrap;
-
-                                textRange = new TextRange(firstTextSourceIndex, length);
+                                textSourceLength += runLineBreak.PositionWrap;
 
                                 return textRuns;
                             }
@@ -437,17 +433,15 @@ namespace Avalonia.Media.TextFormatting
                             break;
                         }
                     case DrawableTextRun drawableTextRun:
-                    {
-                        textRuns.Add(drawableTextRun);
-                        break;
-                    }
+                        {
+                            textRuns.Add(drawableTextRun);
+                            break;
+                        }
                 }
 
-                length += textRun.TextSourceLength;
+                textSourceLength += textRun.TextSourceLength;
             }
 
-            textRange = new TextRange(firstTextSourceIndex, length);
-
             return textRuns;
         }
 
@@ -477,71 +471,74 @@ namespace Avalonia.Media.TextFormatting
             return false;
         }
 
-        private static int MeasureLength(IReadOnlyList<DrawableTextRun> textRuns, TextRange textRange,
-            double paragraphWidth)
+        private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength)
         {
+            measuredLength = 0;
             var currentWidth = 0.0;
-            var lastCluster = textRange.Start;
+            var lastCluster = firstTextSourceIndex;
 
             foreach (var currentRun in textRuns)
             {
                 switch (currentRun)
                 {
                     case ShapedTextCharacters shapedTextCharacters:
-                    {
-                        for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
                         {
-                            var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
-
-                            if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
+                            for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
                             {
-                                var measuredLength = lastCluster - textRange.Start;
+                                var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
+
+                                if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
+                                {
+                                    goto found;
+                                }
 
-                                return measuredLength == 0 ? 1 : measuredLength;
+                                lastCluster = glyphInfo.GlyphCluster;
+                                currentWidth += glyphInfo.GlyphAdvance;
                             }
 
-                            lastCluster = glyphInfo.GlyphCluster;
-                            currentWidth += glyphInfo.GlyphAdvance;
+                            break;
                         }
-                        
-                        break;
-                    }
 
                     case { } drawableTextRun:
-                    {
-                        if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
                         {
-                            var measuredLength = lastCluster - textRange.Start;
-                            
-                            return measuredLength == 0 ? 1 : measuredLength; 
+                            if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
+                            {
+                                goto found;
+                            }
+
+                            lastCluster += currentRun.TextSourceLength;
+                            currentWidth += currentRun.Size.Width;
+
+                            break;
                         }
-                        
-                        lastCluster += currentRun.TextSourceLength;
-                        currentWidth += currentRun.Size.Width;
-                        
-                        break;
-                    }
                 }
             }
 
-            return textRange.Length;
+            found:
+
+            measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1);
+
+            return measuredLength != 0;
         }
 
         /// <summary>
         /// Performs text wrapping returns a list of text lines.
         /// </summary>
         /// <param name="textRuns"></param>
-        /// <param name="textRange">The text range that is covered by the text runs.</param>
+        /// <param name="firstTextSourceIndex">The first text source index.</param>
         /// <param name="paragraphWidth">The paragraph width.</param>
         /// <param name="paragraphProperties">The text paragraph properties.</param>
         /// <param name="flowDirection"></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, TextRange textRange,
+        private static TextLineImpl PerformTextWrapping(List<DrawableTextRun> textRuns, int firstTextSourceIndex,
             double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
             TextLineBreak? currentLineBreak)
         {
-            var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
+            if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength))
+            {
+                measuredLength = 1;
+            }
 
             var currentLength = 0;
 
@@ -655,8 +652,6 @@ namespace Avalonia.Media.TextFormatting
 
             var splitResult = SplitDrawableRuns(textRuns, measuredLength);
 
-            textRange = new TextRange(textRange.Start, measuredLength);
-
             var remainingCharacters = splitResult.Second;
 
             var lineBreak = remainingCharacters?.Count > 0 ?
@@ -668,9 +663,8 @@ namespace Avalonia.Media.TextFormatting
                 lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection);
             }
 
-            TextLineImpl.SortRuns(splitResult.First);
-
-            var textLine = new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, flowDirection,
+            var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength,
+                paragraphWidth, paragraphProperties, flowDirection,
                 lineBreak);
 
             return textLine.FinalizeLine();

+ 17 - 16
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -162,7 +162,9 @@ namespace Avalonia.Media.TextFormatting
 
             foreach (var textLine in TextLines)
             {
-                if (textLine.TextRange.End < textPosition)
+                var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
+
+                if (end < textPosition)
                 {
                     currentY += textLine.Height;
 
@@ -197,7 +199,7 @@ namespace Avalonia.Media.TextFormatting
             foreach (var textLine in TextLines)
             {
                 //Current line isn't covered.
-                if (textLine.TextRange.Start + textLine.TextRange.Length <= start)
+                if (textLine.FirstTextSourceIndex + textLine.Length <= start)
                 {
                     currentY += textLine.Height;
 
@@ -220,7 +222,7 @@ namespace Avalonia.Media.TextFormatting
                     }                  
                 }
 
-                if(textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
+                if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)
                 {
                     break;
                 }
@@ -280,12 +282,13 @@ namespace Avalonia.Media.TextFormatting
             {
                 var textLine = TextLines[index];
 
-                if (textLine.TextRange.Start + textLine.TextRange.Length < charIndex)
+                if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
                 {
                     continue;
                 }
 
-                if (charIndex >= textLine.TextRange.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0))
+                if (charIndex >= textLine.FirstTextSourceIndex && 
+                    charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
                 {
                     return index;
                 }
@@ -298,11 +301,11 @@ namespace Avalonia.Media.TextFormatting
         {
             var (x, y) = point;
 
-            var lastTrailingIndex = textLine.TextRange.Start + textLine.TextRange.Length;
+            var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
 
             var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
 
-            if (x >= textLine.Width && textLine.TextRange.Length > 0 && textLine.NewLineLength > 0)
+            if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
             {
                 lastTrailingIndex -= textLine.NewLineLength;
             }
@@ -312,7 +315,7 @@ namespace Avalonia.Media.TextFormatting
             var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
                              y > Bounds.Bottom;
 
-            if (textPosition == textLine.TextRange.Start + textLine.TextRange.Length)
+            if (textPosition == textLine.FirstTextSourceIndex + textLine.Length)
             {
                 textPosition -= textLine.NewLineLength;
             }
@@ -376,23 +379,21 @@ namespace Avalonia.Media.TextFormatting
         /// Creates an empty text line.
         /// </summary>
         /// <returns>The empty text line.</returns>
-        private TextLine CreateEmptyTextLine(int startingIndex)
+        private TextLine CreateEmptyTextLine(int firstTextSourceIndex)
         {
             var flowDirection = _paragraphProperties.FlowDirection;
             var properties = _paragraphProperties.DefaultTextRunProperties;
             var glyphTypeface = properties.Typeface.GlyphTypeface;
-            var text = new ReadOnlySlice<char>(s_empty, startingIndex, 1);
+            var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
             var glyph = glyphTypeface.GetGlyph(s_empty[0]);
-            var glyphInfos = new[] { new GlyphInfo(glyph, startingIndex) };
+            var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
 
             var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
                 (sbyte)flowDirection);
 
             var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
 
-            var textRange = new TextRange(startingIndex, 1);
-
-            return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
+            return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
         }
 
         private IReadOnlyList<TextLine> CreateTextLines()
@@ -423,13 +424,13 @@ namespace Avalonia.Media.TextFormatting
                     _paragraphProperties, previousLine?.TextLineBreak);
 
 #if DEBUG
-                if (textLine.TextRange.Length == 0)
+                if (textLine.Length == 0)
                 {
                     throw new InvalidOperationException($"{nameof(textLine)} should not be empty.");
                 }
 #endif
 
-                currentPosition += textLine.TextRange.Length;
+                currentPosition += textLine.Length;
                 
                 //Fulfill max height constraint
                 if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)

+ 0 - 2
src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -88,8 +88,6 @@ namespace Avalonia.Media.TextFormatting
 
                                     collapsedRuns.AddRange(splitResult.First);
 
-                                    TextLineImpl.SortRuns(collapsedRuns);
-
                                     preSplitRuns = splitResult.First;
                                     postSplitRuns = splitResult.Second;
                                 }

+ 3 - 7
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@@ -16,13 +16,9 @@ namespace Avalonia.Media.TextFormatting
         /// </value>
         public abstract IReadOnlyList<TextRun> TextRuns { get; }
         
-        /// <summary>
-        /// Gets the text range that is covered by the line.
-        /// </summary>
-        /// <value>
-        /// The text range that is covered by the line.
-        /// </value>
-        public abstract TextRange TextRange { get; }
+        public abstract int FirstTextSourceIndex { get; }
+
+        public abstract int Length { get; }
 
         /// <summary>
         /// Gets the state of the line when broken by line breaking process.

+ 43 - 58
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@@ -6,30 +6,18 @@ namespace Avalonia.Media.TextFormatting
 {
     internal class TextLineImpl : TextLine
     {
-        private static readonly Comparer<int> s_compareStart = Comparer<int>.Default;
-
-        private static readonly Comparison<DrawableTextRun> s_compareLogicalOrder =
-            (a, b) =>
-            {
-                if (a is ShapedTextCharacters && b is ShapedTextCharacters)
-                {
-                    return s_compareStart.Compare(a.Text.Start, b.Text.Start);
-                }
-
-                return 0;
-            };
-
         private readonly List<DrawableTextRun> _textRuns;
         private readonly double _paragraphWidth;
         private readonly TextParagraphProperties _paragraphProperties;
         private TextLineMetrics _textLineMetrics;
         private readonly FlowDirection _flowDirection;
 
-        public TextLineImpl(List<DrawableTextRun> textRuns, TextRange textRange, double paragraphWidth,
+        public TextLineImpl(List<DrawableTextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
             TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
             TextLineBreak? lineBreak = null, bool hasCollapsed = false)
         {
-            TextRange = textRange;
+            FirstTextSourceIndex = firstTextSourceIndex;
+            Length = length;
             TextLineBreak = lineBreak;
             HasCollapsed = hasCollapsed;
 
@@ -44,7 +32,10 @@ namespace Avalonia.Media.TextFormatting
         public override IReadOnlyList<TextRun> TextRuns => _textRuns;
 
         /// <inheritdoc/>
-        public override TextRange TextRange { get; }
+        public override int FirstTextSourceIndex { get; }
+
+        /// <inheritdoc/>
+        public override int Length { get; }
 
         /// <inheritdoc/>
         public override TextLineBreak? TextLineBreak { get; }
@@ -144,7 +135,7 @@ namespace Avalonia.Media.TextFormatting
                 return this;
             }
 
-            var collapsedLine = new TextLineImpl(collapsedRuns, TextRange, _paragraphWidth, _paragraphProperties,
+            var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
                 _flowDirection, TextLineBreak, true);
 
             if (collapsedRuns.Count > 0)
@@ -159,17 +150,15 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override CharacterHit GetCharacterHitFromDistance(double distance)
         {
+            if (_textRuns.Count == 0)
+            {
+                return new CharacterHit();
+            }
+
             distance -= Start;
 
             if (distance <= 0)
-            {
-                if (_textRuns.Count == 0)
-                {
-                    return _flowDirection == FlowDirection.LeftToRight ?
-                        new CharacterHit(TextRange.Start) :
-                        new CharacterHit(TextRange.Start, TextRange.Length);
-                }
-
+            {              
                 // hit happens before the line, return the first position
                 var firstRun = _textRuns[0];
 
@@ -179,13 +168,13 @@ namespace Avalonia.Media.TextFormatting
                 }
 
                 return _flowDirection == FlowDirection.LeftToRight ?
-                    new CharacterHit(TextRange.Start) :
-                    new CharacterHit(TextRange.Start + TextRange.Length);
+                    new CharacterHit(FirstTextSourceIndex) :
+                    new CharacterHit(FirstTextSourceIndex + Length);
             }
 
             // process hit that happens within the line
             var characterHit = new CharacterHit();
-            var currentPosition = TextRange.Start;
+            var currentPosition = FirstTextSourceIndex;
 
             foreach (var currentRun in _textRuns)
             {
@@ -227,7 +216,7 @@ namespace Avalonia.Media.TextFormatting
         {
             var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
             var currentDistance = Start;
-            var currentPosition = TextRange.Start;
+            var currentPosition = FirstTextSourceIndex;
 
             GlyphRun? lastRun = null;
 
@@ -346,14 +335,10 @@ namespace Avalonia.Media.TextFormatting
 
         /// <inheritdoc/>
         public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
-        {
+        {         
             if (_textRuns.Count == 0)
             {
-                var textPosition = TextRange.Start + TextRange.Length;
-
-                return characterHit.FirstCharacterIndex + characterHit.TrailingLength == textPosition ?
-                    characterHit :
-                    new CharacterHit(textPosition);
+                return new CharacterHit();
             }
 
             if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit))
@@ -361,8 +346,10 @@ namespace Avalonia.Media.TextFormatting
                 return nextCharacterHit;
             }
 
+            var lastTextPosition = FirstTextSourceIndex + Length;
+
             // Can't move, we're after the last character
-            var runIndex = GetRunIndexAtCharacterIndex(TextRange.End, LogicalDirection.Forward, out var currentPosition);
+            var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition);
 
             var currentRun = _textRuns[runIndex];
 
@@ -391,9 +378,9 @@ namespace Avalonia.Media.TextFormatting
                 return previousCharacterHit;
             }
 
-            if (characterHit.FirstCharacterIndex <= TextRange.Start)
+            if (characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
             {
-                characterHit = new CharacterHit(TextRange.Start);
+                characterHit = new CharacterHit(FirstTextSourceIndex);
             }
 
             return characterHit; // Can't move, we're before the first character
@@ -408,7 +395,7 @@ namespace Avalonia.Media.TextFormatting
 
         public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceCharacterIndex, int textLength)
         {
-            if (firstTextSourceCharacterIndex + textLength <= TextRange.Start)
+            if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex)
             {
                 return Array.Empty<TextBounds>();
             }
@@ -601,11 +588,6 @@ namespace Avalonia.Media.TextFormatting
             return result;
         }
 
-        public static void SortRuns(List<DrawableTextRun> textRuns)
-        {
-            textRuns.Sort(s_compareLogicalOrder);
-        }
-
         public TextLineImpl FinalizeLine()
         {
             BidiReorder();
@@ -797,15 +779,16 @@ namespace Avalonia.Media.TextFormatting
             nextCharacterHit = characterHit;
 
             var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+            var lastCodepointIndex = FirstTextSourceIndex + Length;
 
-            if (codepointIndex >= TextRange.End)
+            if (codepointIndex >= lastCodepointIndex)
             {
                 return false; // Cannot go forward anymore
             }
 
-            if (codepointIndex < TextRange.Start)
+            if (codepointIndex < FirstTextSourceIndex)
             {
-                codepointIndex = TextRange.Start;
+                codepointIndex = FirstTextSourceIndex;
             }
 
             var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
@@ -820,8 +803,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
 
-                            var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
-                                          TextRange.Start + TextRange.Length;
+                            var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length;
 
                             if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight)
                             {
@@ -880,16 +862,16 @@ namespace Avalonia.Media.TextFormatting
         {
             var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
 
-            if (characterIndex == TextRange.Start)
+            if (characterIndex == FirstTextSourceIndex)
             {
-                previousCharacterHit = new CharacterHit(TextRange.Start);
+                previousCharacterHit = new CharacterHit(FirstTextSourceIndex);
 
                 return true;
             }
 
             previousCharacterHit = characterHit;
 
-            if (characterIndex < TextRange.Start)
+            if (characterIndex < FirstTextSourceIndex)
             {
                 return false; // Cannot go backward anymore.
             }
@@ -957,7 +939,7 @@ namespace Avalonia.Media.TextFormatting
         private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
         {
             var runIndex = 0;
-            textPosition = TextRange.Start;
+            textPosition = FirstTextSourceIndex;
             DrawableTextRun? previousRun = null;
 
             while (runIndex < _textRuns.Count)
@@ -1089,6 +1071,11 @@ namespace Avalonia.Media.TextFormatting
                                 {
                                     lineGap = fontMetrics.LineGap;
                                 }
+
+                                if(descent - ascent + lineGap > height)
+                                {
+                                    height = descent - ascent + lineGap;
+                                }
                             }
 
                             switch (_paragraphProperties.FlowDirection)
@@ -1164,9 +1151,9 @@ namespace Avalonia.Media.TextFormatting
                                     }
                             }
 
-                            if (descent - ascent + lineGap < drawableTextRun.Size.Height || lineHeight < drawableTextRun.Size.Height)
+                            if (drawableTextRun.Size.Height > height)
                             {
-                                lineHeight = drawableTextRun.Size.Height;
+                                height = drawableTextRun.Size.Height;
                             }
 
                             break;
@@ -1176,10 +1163,8 @@ namespace Avalonia.Media.TextFormatting
 
             start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
                 _paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
-
-            height = descent - ascent + lineGap;
            
-            if(!double.IsNaN(lineHeight) && lineHeight > height)
+            if(!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
             {
                 height = lineHeight;
             }

+ 17 - 15
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@@ -58,7 +58,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(5, textLine.TextRuns.Count);
 
-                Assert.Equal(50, textLine.TextRange.Length);
+                Assert.Equal(50, textLine.Length);
             }
         }
 
@@ -89,7 +89,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
                     new GenericTextParagraphProperties(defaultProperties));
 
-                Assert.Equal(text.Length, textLine.TextRange.Length);
+                Assert.Equal(text.Length, textLine.Length);
 
                 for (var i = 0; i < GenericTextRunPropertiesRuns.Length; i++)
                 {
@@ -195,10 +195,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                     if (text.Length - currentPosition > expectedCharactersPerLine)
                     {
-                        Assert.Equal(expectedCharactersPerLine, textLine.TextRange.Length);
+                        Assert.Equal(expectedCharactersPerLine, textLine.Length);
                     }
 
-                    currentPosition += textLine.TextRange.Length;
+                    currentPosition += textLine.Length;
 
                     numberOfLines++;
                 }
@@ -249,16 +249,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         formatter.FormatLine(textSource, currentPosition, paragraphWidth,
                             new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
 
-                    Assert.True(expected.Contains(textLine.TextRange.End));
+                    var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
 
-                    var index = expected.IndexOf(textLine.TextRange.End);
+                    Assert.True(expected.Contains(end));
+
+                    var index = expected.IndexOf(end);
 
                     for (var i = 0; i <= index; i++)
                     {
                         expected.RemoveAt(0);
                     }
 
-                    currentPosition += textLine.TextRange.Length;
+                    currentPosition += textLine.Length;
                 }
             }
         }
@@ -312,7 +314,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                     Assert.True(textLine.Width <= 200);
 
-                    textSourceIndex += textLine.TextRange.Length;
+                    textSourceIndex += textLine.Length;
                 }
             }
         }
@@ -336,9 +338,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     var textLine =
                         formatter.FormatLine(textSource, textSourceIndex, 3, paragraphProperties);
 
-                    Assert.NotEqual(0, textLine.TextRange.Length);
+                    Assert.NotEqual(0, textLine.Length);
 
-                    textSourceIndex += textLine.TextRange.Length;
+                    textSourceIndex += textLine.Length;
                 }
 
                 Assert.Equal(text.Length, textSourceIndex);
@@ -383,7 +385,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         formatter.FormatLine(textSource, currentPosition, 300,
                             new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
 
-                    currentPosition += textLine.TextRange.Length;
+                    currentPosition += textLine.Length;
 
                     if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
                     {
@@ -392,7 +394,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     
                     currentHeight += textLine.Height;
 
-                    var currentText = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
+                    var currentText = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
                     
                     Assert.Equal(expectedLines[currentLineIndex], currentText);
 
@@ -485,9 +487,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     var textLine =
                         formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
 
-                    Assert.Equal(textLine.TextRange.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
+                    Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength));
                     
-                    textPosition += textLine.TextRange.Length;
+                    textPosition += textLine.Length;
 
                     lastBreak = textLine.TextLineBreak;
                 }
@@ -594,7 +596,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 
                 Assert.NotNull(textLine.TextLineBreak);
                 
-                Assert.Equal(TextRun.DefaultTextSourceLength, textLine.TextRange.Length);
+                Assert.Equal(TextRun.DefaultTextSourceLength, textLine.Length);
             }
         }
 

+ 10 - 8
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -88,8 +88,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     textWrapping: TextWrapping.Wrap,
                     maxWidth: 200);
 
-                var expectedLines = expected.TextLines.Select(x => text.Substring(x.TextRange.Start,
-                    x.TextRange.Length)).ToList();
+                var expectedLines = expected.TextLines.Select(x => text.Substring(x.FirstTextSourceIndex,
+                    x.Length)).ToList();
 
                 var spans = new[]
                 {
@@ -106,8 +106,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     maxWidth: 200,
                     textStyleOverrides: spans);
 
-                var actualLines = actual.TextLines.Select(x => text.Substring(x.TextRange.Start,
-                    x.TextRange.Length)).ToList();
+                var actualLines = actual.TextLines.Select(x => text.Substring(x.FirstTextSourceIndex,
+                    x.Length)).ToList();
 
                 Assert.Equal(expectedLines.Count, actualLines.Count);
 
@@ -140,7 +140,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     black,
                     textWrapping: TextWrapping.Wrap);
 
-                var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextCharacters>().SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
+                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());
@@ -172,7 +173,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                             textWrapping: TextWrapping.Wrap,
                             textStyleOverrides: spans);
 
-                        var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextCharacters>().SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
+                        var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast<ShapedTextCharacters>()
+                            .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();
 
                         Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count);
 
@@ -348,7 +350,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     12.0f,
                     Brushes.Black.ToImmutable());
 
-                Assert.Equal(MultiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
+                Assert.Equal(MultiLineText.Length, layout.TextLines.Sum(x => x.Length));
             }
         }
 
@@ -813,7 +815,7 @@ 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 expected = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
+                        var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
 
                         Assert.Equal(expected, actual);
                     }                  

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

@@ -35,9 +35,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                     var firstCharacterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(int.MinValue));
 
-                    Assert.Equal(textLine.TextRange.Start, firstCharacterHit.FirstCharacterIndex);
+                    Assert.Equal(textLine.FirstTextSourceIndex, firstCharacterHit.FirstCharacterIndex);
 
-                    currentIndex += textLine.TextRange.Length;
+                    currentIndex += textLine.Length;
                 }
             }
         }
@@ -63,10 +63,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                     var lastCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(int.MaxValue));
 
-                    Assert.Equal(textLine.TextRange.Start + textLine.TextRange.Length,
+                    Assert.Equal(textLine.FirstTextSourceIndex + textLine.Length,
                         lastCharacterHit.FirstCharacterIndex + lastCharacterHit.TrailingLength);
 
-                    currentIndex += textLine.TextRange.Length;
+                    currentIndex += textLine.Length;
                 }
             }
         }