Forráskód Böngészése

Merge pull request #8626 from Gillibald/fixes/textProcessingFixes

Text processing fixes
Max Katz 3 éve
szülő
commit
ed05285ad3

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

@@ -265,7 +265,7 @@ namespace Avalonia.Media
                 //RightToLeft
                 var glyphIndex = FindGlyphIndex(characterIndex);
 
-                if (GlyphClusters != null)
+                if (GlyphClusters != null && GlyphClusters.Count > 0)
                 {
                     if (characterIndex > GlyphClusters[0])
                     {

+ 24 - 0
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
@@ -116,7 +117,30 @@ namespace Avalonia.Media.TextFormatting
                 length = text.Length;
             }
 
+            length = CoerceLength(text, length);
+
             return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
         }
+
+        private static int CoerceLength(ReadOnlySlice<char> text, int length)
+        {
+            var finalLength = 0;
+
+            var graphemeEnumerator = new GraphemeEnumerator(text);
+
+            while (graphemeEnumerator.MoveNext())
+            {
+                var grapheme = graphemeEnumerator.Current;
+
+                finalLength += grapheme.Text.Length;
+
+                if (finalLength >= length)
+                {
+                    return finalLength;
+                }
+            }
+
+            return length;
+        }
     }
 }

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

@@ -15,6 +15,13 @@ namespace Avalonia.Media.TextFormatting
 
         public override void Justify(TextLine textLine)
         {
+            var lineImpl = textLine as TextLineImpl;
+
+            if(lineImpl is null)
+            {
+                return;
+            }
+
             var paragraphWidth = Width;
 
             if (double.IsInfinity(paragraphWidth))
@@ -22,12 +29,12 @@ namespace Avalonia.Media.TextFormatting
                 return;
             }
 
-            if (textLine.NewLineLength > 0)
+            if (lineImpl.NewLineLength > 0)
             {
                 return;
             }
 
-            var textLineBreak = textLine.TextLineBreak;
+            var textLineBreak = lineImpl.TextLineBreak;
 
             if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null)
             {
@@ -39,7 +46,7 @@ namespace Avalonia.Media.TextFormatting
 
             var breakOportunities = new Queue<int>();
 
-            foreach (var textRun in textLine.TextRuns)
+            foreach (var textRun in lineImpl.TextRuns)
             {
                 var text = textRun.Text;
 
@@ -68,10 +75,10 @@ namespace Avalonia.Media.TextFormatting
                 return;
             }
 
-            var remainingSpace = Math.Max(0, paragraphWidth - textLine.WidthIncludingTrailingWhitespace);
+            var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace);
             var spacing = remainingSpace / breakOportunities.Count;
 
-            foreach (var textRun in textLine.TextRuns)
+            foreach (var textRun in lineImpl.TextRuns)
             {
                 var text = textRun.Text;
 

+ 13 - 13
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting
         /// Gets a list of <see cref="ShapeableTextCharacters"/>.
         /// </summary>
         /// <returns>The shapeable text characters.</returns>
-        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel, 
+        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
             ref TextRunProperties? previousProperties)
         {
             var shapeableCharacters = new List<ShapeableTextCharacters>(2);
@@ -65,7 +65,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="biDiLevel">The bidi level of the run.</param>
         /// <param name="previousProperties"></param>
         /// <returns>A list of shapeable text runs.</returns>
-        private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text, 
+        private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
             TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
             var defaultTypeface = defaultProperties.Typeface;
@@ -76,7 +76,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 if (script == Script.Common && previousTypeface is not null)
                 {
-                    if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _))
+                    if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _))
                     {
                         return new ShapeableTextCharacters(text.Take(fallbackCount),
                             defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -86,10 +86,10 @@ namespace Avalonia.Media.TextFormatting
                 return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
-            
+
             if (previousTypeface is not null)
             {
-                if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
+                if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
                 {
                     return new ShapeableTextCharacters(text.Take(count),
                         defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -106,12 +106,12 @@ namespace Avalonia.Media.TextFormatting
                 {
                     continue;
                 }
-                
+
                 codepoint = codepointEnumerator.Current;
-                    
+
                 break;
             }
-            
+
             //ToDo: Fix FontFamily fallback
             var matchFound =
                 FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
@@ -157,14 +157,14 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="script"></param>
         /// <returns></returns>
         protected static bool TryGetShapeableLength(
-            ReadOnlySlice<char> text, 
-            Typeface typeface, 
+            ReadOnlySlice<char> text,
+            Typeface typeface,
             Typeface? defaultTypeface,
             out int length,
             out Script script)
         {
             length = 0;
-            script = Script.Unknown;         
+            script = Script.Unknown;
 
             if (text.Length == 0)
             {
@@ -182,7 +182,7 @@ namespace Avalonia.Media.TextFormatting
 
                 var currentScript = currentGrapheme.FirstCodepoint.Script;
 
-                if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+                if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
                 {
                     break;
                 }
@@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     break;
                 }
-                
+
                 if (currentScript != script)
                 {
                     if (script is Script.Unknown || currentScript != Script.Common &&

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

@@ -537,8 +537,13 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         /// <param name="width">The collapsing width.</param>
         /// <returns>The <see cref="TextCollapsingProperties"/>.</returns>
-        private TextCollapsingProperties GetCollapsingProperties(double width)
+        private TextCollapsingProperties? GetCollapsingProperties(double width)
         {
+            if(_textTrimming == TextTrimming.None)
+            {
+                return null;
+            }
+
             return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
         }
     }

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

@@ -153,7 +153,7 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>
         /// A <see cref="TextLine"/> value that represents a collapsed line that can be displayed.
         /// </returns>
-        public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList);
+        public abstract TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList);
 
         /// <summary>
         /// Create a justified line based on justification text properties.

+ 324 - 78
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -119,7 +119,7 @@ namespace Avalonia.Media.TextFormatting
         }
 
         /// <inheritdoc/>
-        public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
+        public override TextLine Collapse(params TextCollapsingProperties?[] collapsingPropertiesList)
         {
             if (collapsingPropertiesList.Length == 0)
             {
@@ -128,6 +128,11 @@ namespace Avalonia.Media.TextFormatting
 
             var collapsingProperties = collapsingPropertiesList[0];
 
+            if(collapsingProperties is null)
+            {
+                return this;
+            }
+
             var collapsedRuns = collapsingProperties.Collapse(this);
 
             if (collapsedRuns is null)
@@ -171,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
                 return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0);
             }
 
-            if (distance > WidthIncludingTrailingWhitespace)
+            if (distance >= WidthIncludingTrailingWhitespace)
             {
                 var lastRun = _textRuns[_textRuns.Count - 1];
 
@@ -183,8 +188,52 @@ namespace Avalonia.Media.TextFormatting
             var currentPosition = FirstTextSourceIndex;
             var currentDistance = 0.0;
 
-            foreach (var currentRun in _textRuns)
+            for (var i = 0; i < _textRuns.Count; i++)
             {
+                var currentRun = _textRuns[i];
+
+                if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+                {
+                    var rightToLeftIndex = i;
+                    currentPosition += currentRun.TextSourceLength;
+
+                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
+                    {
+                        var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
+
+                        if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
+                        {
+                            break;
+                        }
+
+                        currentPosition += nextShaped.TextSourceLength;
+
+                        rightToLeftIndex++;
+                    }
+
+                    for (var j = i; i <= rightToLeftIndex; j++)
+                    {
+                        if(j > _textRuns.Count - 1)
+                        {
+                            break;
+                        }
+
+                        currentRun = _textRuns[j];
+
+                        if(currentDistance + currentRun.Size.Width <= distance)
+                        {
+                            currentDistance += currentRun.Size.Width;
+                            currentPosition -= currentRun.TextSourceLength;
+
+                            continue;
+                        }
+
+                        characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
+
+                        break;
+                    }
+                }
+
                 if (currentDistance + currentRun.Size.Width < distance)
                 {
                     currentDistance += currentRun.Size.Width;
@@ -211,12 +260,16 @@ namespace Avalonia.Media.TextFormatting
                     {
                         characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
 
-                        var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
+                        var offset = 0;
 
-                        if (!shapedRun.GlyphRun.IsLeftToRight)
+                        if (shapedRun.GlyphRun.IsLeftToRight)
                         {
-                            offset = Math.Max(0, offset - shapedRun.Text.End);
+                            offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
                         }
+                        //else
+                        //{
+                        //    offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length);
+                        //}
 
                         characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
 
@@ -255,10 +308,56 @@ namespace Avalonia.Media.TextFormatting
                 {
                     var currentRun = _textRuns[index];
 
-                    if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
-                        flowDirection, out var distance, out _))
+                    if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+                    {
+                        var i = index;
+
+                        var rightToLeftWidth = currentRun.Size.Width;
+
+                        while (i + 1 <= _textRuns.Count - 1)
+                        {
+                            var nextRun = _textRuns[i + 1];
+
+                            if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight)
+                            {
+                                i++;
+
+                                rightToLeftWidth += nextRun.Size.Width;
+
+                                continue;
+                            }
+                            
+                            break;
+                        }
+
+                        if(i > index)
+                        {
+                            while (i >= index)
+                            {
+                                currentRun = _textRuns[i];
+
+                                rightToLeftWidth -= currentRun.Size.Width;
+
+                                if (currentPosition + currentRun.TextSourceLength >= characterIndex)
+                                {
+                                    break;
+                                }
+
+                                currentPosition += currentRun.TextSourceLength;
+
+                                remainingLength -= currentRun.TextSourceLength;
+
+                                i--;
+                            }
+
+                            currentDistance += rightToLeftWidth;
+                        }
+                    }
+
+                    if (currentPosition + currentRun.TextSourceLength >= characterIndex && 
+                        TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
                     {
-                        return currentDistance + distance;
+                        return Math.Max(0, currentDistance + distance);
                     }
 
                     //No hit hit found so we add the full width
@@ -283,7 +382,7 @@ namespace Avalonia.Media.TextFormatting
                             distance = currentGlyphRun.Size.Width - distance;
                         }
 
-                        return currentDistance - distance;
+                        return Math.Max(0, currentDistance - distance);
                     }
 
                     //No hit hit found so we add the full width
@@ -293,7 +392,7 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            return currentDistance;
+            return Math.Max(0, currentDistance);
         }
 
         private static bool TryGetDistanceFromCharacterHit(
@@ -442,92 +541,139 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
-                if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
-                {
-                    startX += currentRun.Size.Width;
-
-                    currentPosition += currentRun.TextSourceLength;
-
-                    continue;
-                }
-
                 var characterLength = 0;
                 var endX = startX;
+                var runWidth = 0.0;
+                TextRunBounds? currentRunBounds = null;
 
-                if (currentRun is ShapedTextCharacters currentShapedRun)
+                var currentShapedRun = currentRun as ShapedTextCharacters;
+
+                if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
                 {
-                    var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+                    var rightToLeftIndex = index;
+                    startX += currentShapedRun.Size.Width;
 
-                    currentPosition += offset;
+                    while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
+                    {
+                        var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
 
-                    var startIndex = currentRun.Text.Start + offset;
+                        if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+                        {
+                            break;
+                        }
 
-                    double startOffset;
-                    double endOffset;
+                        startX += nextShapedRun.Size.Width;
 
-                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+                        rightToLeftIndex++;
+                    }
+
+                    if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds))
                     {
-                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                        startX = currentRunBounds!.Rectangle.Left;
+                        endX = currentRunBounds.Rectangle.Right;
 
-                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                        runWidth = currentRunBounds.Rectangle.Width;
                     }
-                    else
+
+                    currentDirection = FlowDirection.RightToLeft;
+                }
+                else
+                {
+                    if (currentShapedRun != null)
                     {
-                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                        if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+                        {
+                            startX += currentRun.Size.Width;
 
-                        if (currentPosition < startIndex)
+                            currentPosition += currentRun.TextSourceLength;
+
+                            continue;
+                        }
+
+                        var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+
+                        currentPosition += offset;
+
+                        var startIndex = currentRun.Text.Start + offset;
+
+                        double startOffset;
+                        double endOffset;
+
+                        if (currentShapedRun.ShapedBuffer.IsLeftToRight)
                         {
-                            startOffset = endOffset;
+                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
                         }
                         else
                         {
-                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+                            if (currentPosition < startIndex)
+                            {
+                                startOffset = endOffset;
+                            }
+                            else
+                            {
+                                startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                            }
                         }
-                    }
 
-                    startX += startOffset;
+                        startX += startOffset;
 
-                    endX += endOffset;
+                        endX += endOffset;
 
-                    var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
-                    var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+                        var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+                        var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
 
-                    characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+                        characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
 
-                    currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
-                        FlowDirection.LeftToRight :
-                        FlowDirection.RightToLeft;
-                }
-                else
-                {
-                    if (currentPosition < firstTextSourceIndex)
+                        currentDirection = FlowDirection.LeftToRight;
+                    }
+                    else
                     {
-                        startX += currentRun.Size.Width;
+                        if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+                        {
+                            startX += currentRun.Size.Width;
+
+                            currentPosition += currentRun.TextSourceLength;
+
+                            continue;
+                        }
+
+                        if (currentPosition < firstTextSourceIndex)
+                        {
+                            startX += currentRun.Size.Width;
+                        }
+
+                        if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+                        {
+                            endX += currentRun.Size.Width;
+
+                            characterLength = currentRun.TextSourceLength;
+                        }
                     }
 
-                    if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+                    if (endX < startX)
                     {
-                        endX += currentRun.Size.Width;
+                        (endX, startX) = (startX, endX);
+                    }
 
-                        characterLength = currentRun.TextSourceLength;
+                    //Lines that only contain a linebreak need to be covered here
+                    if (characterLength == 0)
+                    {
+                        characterLength = NewLineLength;
                     }
-                }
 
-                if (endX < startX)
-                {
-                    (endX, startX) = (startX, endX);
-                }
+                    runWidth = endX - startX;
+                    currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
 
-                //Lines that only contain a linebreak need to be covered here
-                if (characterLength == 0)
-                {
-                    characterLength = NewLineLength;
-                }
+                    currentPosition += characterLength;
 
-                var runWidth = endX - startX;
-                var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+                    remainingLength -= characterLength;
+                }                 
 
-                if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
+                if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0)
                 {
                     if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
                     {
@@ -537,32 +683,26 @@ namespace Avalonia.Media.TextFormatting
 
                         textBounds.Rectangle = currentRect;
 
-                        textBounds.TextRunBounds.Add(currentRunBounds);
+                        textBounds.TextRunBounds.Add(currentRunBounds!);
                     }
                     else
                     {
-                        currentRect = currentRunBounds.Rectangle;
+                        currentRect = currentRunBounds!.Rectangle;
 
                         result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
                     }
                 }
 
                 currentWidth += runWidth;
-                currentPosition += characterLength;
+              
 
-                if (currentPosition > characterIndex)
+                if (remainingLength <= 0 || currentPosition >= characterIndex)
                 {
                     break;
                 }
 
                 startX = endX;
                 lastDirection = currentDirection;
-                remainingLength -= characterLength;
-
-                if (remainingLength <= 0)
-                {
-                    break;
-                }
             }
 
             return result;
@@ -674,7 +814,7 @@ namespace Avalonia.Media.TextFormatting
 
                 var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
 
-                if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
+                if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
                 {
                     if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
                     {
@@ -692,7 +832,7 @@ namespace Avalonia.Media.TextFormatting
 
                         result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
                     }
-                }               
+                }
 
                 currentWidth += runWidth;
                 currentPosition += characterLength;
@@ -716,6 +856,107 @@ namespace Avalonia.Media.TextFormatting
             return result;
         }
 
+        private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds)
+        {
+            textRunBounds = null;
+
+            for (var index = runIndex; index >= 0; index--)
+            {
+                if (TextRuns[index] is not DrawableTextRun currentRun)
+                {
+                    continue;
+                }
+
+                if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
+                {
+                    startX -= currentRun.Size.Width;
+
+                    currentPosition += currentRun.TextSourceLength;
+
+                    continue;
+                }
+
+                var characterLength = 0;
+                var endX = startX;
+
+                if (currentRun is ShapedTextCharacters currentShapedRun)
+                {
+                    var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+
+                    currentPosition += offset;
+
+                    var startIndex = currentRun.Text.Start + offset;
+                    double startOffset;
+                    double endOffset;
+
+                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+                    {
+                        if (currentPosition < startIndex)
+                        {
+                            startOffset = endOffset = 0;
+                        }
+                        else
+                        {
+                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+
+                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                        }
+                    }
+                    else
+                    {
+                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+
+                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                    }
+
+                    startX -= currentRun.Size.Width - startOffset;
+                    endX -= currentRun.Size.Width - endOffset;
+
+                    var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+                    var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+
+                    characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+                }
+                else
+                {
+                    if (currentPosition + currentRun.TextSourceLength <= characterIndex)
+                    {
+                        endX -= currentRun.Size.Width;
+                    }
+
+                    if (currentPosition < firstTextSourceIndex)
+                    {
+                        startX -= currentRun.Size.Width;
+
+                        characterLength = currentRun.TextSourceLength;
+                    }
+                }
+
+                if (endX < startX)
+                {
+                    (endX, startX) = (startX, endX);
+                }
+
+                //Lines that only contain a linebreak need to be covered here
+                if (characterLength == 0)
+                {
+                    characterLength = NewLineLength;
+                }
+
+                var runWidth = endX - startX;
+
+                remainingLength -= characterLength;
+
+                currentPosition += characterLength;
+
+                textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+
+                return true;
+            }
+
+            return false;
+        }
+
         public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
         {
             if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
@@ -1295,6 +1536,11 @@ namespace Avalonia.Media.TextFormatting
             var textAlignment = _paragraphProperties.TextAlignment;
             var paragraphFlowDirection = _paragraphProperties.FlowDirection;
 
+            if(textAlignment == TextAlignment.Justify)
+            {
+                textAlignment = TextAlignment.Start;
+            }
+
             switch (textAlignment)
             {
                 case TextAlignment.Start:
@@ -1319,12 +1565,12 @@ namespace Avalonia.Media.TextFormatting
                 case TextAlignment.Center:
                     var start = (_paragraphWidth - width) / 2;
 
-                    if(paragraphFlowDirection == FlowDirection.RightToLeft)
+                    if (paragraphFlowDirection == FlowDirection.RightToLeft)
                     {
                         start -= (widthIncludingTrailingWhitespace - width);
                     }
 
-                    return Math.Max(0,  start);                         
+                    return Math.Max(0, start);
                 case TextAlignment.Right:
                     return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
 

+ 4 - 3
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -9,6 +9,7 @@ using Avalonia.VisualTree;
 using Avalonia.Layout;
 using Avalonia.Media.Immutable;
 using Avalonia.Controls.Documents;
+using Avalonia.Media.TextFormatting.Unicode;
 
 namespace Avalonia.Controls.Presenters
 {
@@ -496,14 +497,14 @@ namespace Avalonia.Controls.Presenters
             var length = Math.Max(selectionStart, selectionEnd) - start;
 
             IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
-            
-            if (length > 0)
+
+            if (length > 0 && SelectionForegroundBrush != null)
             {
                 textStyleOverrides = new[]
                 {
                     new ValueSpan<TextRunProperties>(start, length,
                         new GenericTextRunProperties(typeface, FontSize,
-                            foregroundBrush: SelectionForegroundBrush ?? Brushes.White))
+                            foregroundBrush: SelectionForegroundBrush))
                 };
             }
 

+ 12 - 12
src/Avalonia.Controls/RichTextBlock.cs

@@ -44,8 +44,8 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="Inlines"/> property.
         /// </summary>
-        public static readonly StyledProperty<InlineCollection> InlinesProperty =
-            AvaloniaProperty.Register<RichTextBlock, InlineCollection>(
+        public static readonly StyledProperty<InlineCollection?> InlinesProperty =
+            AvaloniaProperty.Register<RichTextBlock, InlineCollection?>(
                 nameof(Inlines));
 
         public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
@@ -138,7 +138,7 @@ namespace Avalonia.Controls
         /// Gets or sets the inlines.
         /// </summary>
         [Content]
-        public InlineCollection Inlines
+        public InlineCollection? Inlines
         {
             get => GetValue(InlinesProperty);
             set => SetValue(InlinesProperty, value);
@@ -159,7 +159,7 @@ namespace Avalonia.Controls
             remove => RemoveHandler(CopyingToClipboardEvent, value);
         }
 
-        internal bool HasComplexContent => Inlines.Count > 0;
+        internal bool HasComplexContent => Inlines != null && Inlines.Count > 0;
 
         /// <summary>
         /// Copies the current selection to the Clipboard.
@@ -260,23 +260,23 @@ namespace Avalonia.Controls
             {
                 if (!string.IsNullOrEmpty(_text))
                 {
-                    Inlines.Add(_text);
+                    Inlines?.Add(_text);
 
                     _text = null;
                 }
 
-                Inlines.Add(text);
+                Inlines?.Add(text);
             }
         }
 
         protected override string? GetText()
         {
-            return _text ?? Inlines.Text;
+            return _text ?? Inlines?.Text;
         }
 
         protected override void SetText(string? text)
         {
-            var oldValue = _text ?? Inlines?.Text;
+            var oldValue = GetText();
       
             AddText(text);        
 
@@ -301,10 +301,10 @@ namespace Avalonia.Controls
 
             ITextSource textSource;
 
-            var inlines = Inlines;
-
             if (HasComplexContent)
             {
+                var inlines = Inlines!;
+
                 var textRuns = new List<TextRun>();
 
                 foreach (var inline in inlines)
@@ -537,7 +537,7 @@ namespace Avalonia.Controls
 
             switch (change.Property.Name)
             {
-                case nameof(InlinesProperty):
+                case nameof(Inlines):
                     {
                         OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
                         InvalidateTextLayout();
@@ -553,7 +553,7 @@ namespace Avalonia.Controls
                 return "";
             }
 
-            var text = Inlines.Text ?? Text;
+            var text = GetText();
 
             if (string.IsNullOrEmpty(text))
             {

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

@@ -17,6 +17,7 @@ using Avalonia.Controls.Metadata;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Automation.Peers;
+using System.Diagnostics;
 
 namespace Avalonia.Controls
 {
@@ -1240,9 +1241,10 @@ namespace Avalonia.Controls
                     MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
                     MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
 
-                _presenter.MoveCaretToPoint(point);
+                _presenter.MoveCaretToPoint(point);  
 
                 var caretIndex = _presenter.CaretIndex;
+
                 var text = Text;
 
                 if (text != null && _wordSelectionStart >= 0)
@@ -1266,7 +1268,7 @@ namespace Avalonia.Controls
                 }
                 else
                 {
-                    SelectionEnd = _presenter.CaretIndex;
+                    SelectionEnd = caretIndex;
                 }
             }
         }

+ 44 - 0
tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs

@@ -48,5 +48,49 @@ namespace Avalonia.Controls.UnitTests
                 Assert.False(target.IsMeasureValid);
             }
         }
+
+        [Fact]
+        public void Changing_Inlines_Should_Invalidate_Measure()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var target = new RichTextBlock();
+
+                var inlines = new InlineCollection { new Run("Hello") };
+
+                target.Measure(Size.Infinity);
+
+                Assert.True(target.IsMeasureValid);
+
+                target.Inlines = inlines;
+
+                Assert.False(target.IsMeasureValid);
+            }
+        }
+
+        [Fact]
+        public void Changing_Inlines_Should_Reset_Inlines_Parent()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var target = new RichTextBlock();
+
+                var run = new Run("Hello");
+
+                target.Inlines.Add(run);
+
+                target.Measure(Size.Infinity);
+
+                Assert.True(target.IsMeasureValid);
+
+                target.Inlines = null;
+
+                Assert.Null(run.Parent);
+
+                target.Inlines = new InlineCollection { run };
+
+                Assert.Equal(target, run.Parent);
+            }
+        }
     }
 }

BIN
tests/Avalonia.RenderTests/Assets/NotoKufiArabic-Regular.ttf


BIN
tests/Avalonia.RenderTests/Assets/NotoSansArabic-Regular.ttf


+ 7 - 1
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Skia.UnitTests.Media
         private readonly Typeface _defaultTypeface =
             new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
         private readonly Typeface _arabicTypeface =
-           new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Kufi Arabic");
+           new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic");
         private readonly Typeface _italicTypeface =
             new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic);
         private readonly Typeface _emojiTypeface =
@@ -82,6 +82,12 @@ namespace Avalonia.Skia.UnitTests.Media
                         skTypeface = typefaceCollection.Get(typeface);
                         break;
                     }
+                case "Noto Sans Arabic":
+                    {
+                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily);
+                        skTypeface = typefaceCollection.Get(typeface);
+                        break;
+                    }
                 case FontFamily.DefaultFontFamilyName:
                 case "Noto Mono":
                     {

+ 65 - 5
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using Avalonia.Media;
@@ -914,14 +915,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         public void Should_Get_CharacterHit_From_Distance_RTL()
         {
             using (Start())
-            { 
+            {
                 var text = "أَبْجَدِيَّة عَرَبِيَّة";
 
                 var layout = new TextLayout(
-                  text,
-                  Typeface.Default,
-                  12,
-                  Brushes.Black);
+                    text,
+                    Typeface.Default,
+                    12,
+                    Brushes.Black);
 
                 var textLine = layout.TextLines[0];
 
@@ -952,6 +953,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 rect = layout.HitTestTextPosition(23);
 
                 Assert.Equal(0, rect.Left, 5);
+
+            }
+        }
+
+        [Fact]
+        public void Should_Get_CharacterHit_From_Distance_RTL_With_TextStyles()
+        {
+            using (Start())
+            {
+                var text = "أَبْجَدِيَّة عَرَبِيَّة";
+
+                var i = 0;
+
+                var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory());
+
+                while (graphemeEnumerator.MoveNext())
+                {
+                    var grapheme = graphemeEnumerator.Current;
+
+                    var textStyleOverrides = new[] { new ValueSpan<TextRunProperties>(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
+
+                    i += grapheme.Text.Length;
+
+                    var layout = new TextLayout(
+                        text,
+                        Typeface.Default,
+                        12,
+                        Brushes.Black,
+                        textStyleOverrides: textStyleOverrides);
+
+                    var textLine = layout.TextLines[0];
+
+                    var shapedRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
+
+                    var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList();
+
+                    var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList();
+
+                    var currentX = 0.0;
+
+                    var cluster = text.Length;
+
+                    for (int j = 0; j < clusters.Count - 1; j++)
+                    {                     
+                        var glyphAdvance = glyphAdvances[j];
+
+                        var characterHit = textLine.GetCharacterHitFromDistance(currentX);
+
+                        Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
+
+                        var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
+
+                        Assert.Equal(currentX, distance, 5);
+
+                        currentX += glyphAdvance;
+
+                        cluster = clusters[j];
+                    }
+                }
             }
         }