Browse Source

Merge pull request #7946 from Gillibald/feature/InlineUIContainer

InlineUIContainer
Max Katz 3 years ago
parent
commit
cba8054304

+ 47 - 32
src/Avalonia.Base/Media/GlyphRun.cs

@@ -28,6 +28,8 @@ namespace Avalonia.Media
         private IReadOnlyList<Vector>? _glyphOffsets;
         private IReadOnlyList<int>? _glyphClusters;
 
+        private int _offsetToFirstCharacter;
+
         /// <summary>
         ///     Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
         /// </summary>
@@ -49,7 +51,7 @@ namespace Avalonia.Media
             IReadOnlyList<int>? glyphClusters = null,
             int biDiLevel = 0)
         {
-            _glyphTypeface = glyphTypeface;  
+            _glyphTypeface = glyphTypeface;
 
             FontRenderingEmSize = fontRenderingEmSize;
 
@@ -203,8 +205,8 @@ namespace Avalonia.Media
         /// </returns>
         public double GetDistanceFromCharacterHit(CharacterHit characterHit)
         {
-            var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
-           
+            var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
+
             var distance = 0.0;
 
             if (IsLeftToRight)
@@ -223,7 +225,7 @@ namespace Avalonia.Media
                 }
 
                 var glyphIndex = FindGlyphIndex(characterIndex);
-                
+
                 if (GlyphClusters != null)
                 {
                     var currentCluster = GlyphClusters[glyphIndex];
@@ -249,7 +251,7 @@ namespace Avalonia.Media
             {
                 //RightToLeft
                 var glyphIndex = FindGlyphIndex(characterIndex);
-                
+
                 if (GlyphClusters != null)
                 {
                     if (characterIndex > GlyphClusters[0])
@@ -284,13 +286,13 @@ namespace Avalonia.Media
         public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
         {
             var characterIndex = 0;
-            
+
             // Before
             if (distance <= 0)
             {
                 isInside = false;
 
-                if(GlyphClusters != null)
+                if (GlyphClusters != null)
                 {
                     characterIndex = GlyphClusters[characterIndex];
                 }
@@ -307,11 +309,11 @@ namespace Avalonia.Media
 
                 characterIndex = GlyphIndices.Count - 1;
 
-                if(GlyphClusters != null)
+                if (GlyphClusters != null)
                 {
                     characterIndex = GlyphClusters[characterIndex];
                 }
-                
+
                 var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
 
                 return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
@@ -327,7 +329,7 @@ namespace Avalonia.Media
                     var advance = GetGlyphAdvance(index, out var cluster);
 
                     characterIndex = cluster;
-                    
+
                     if (distance > currentX && distance <= currentX + advance)
                     {
                         break;
@@ -345,7 +347,7 @@ namespace Avalonia.Media
                     var advance = GetGlyphAdvance(index, out var cluster);
 
                     characterIndex = cluster;
-                    
+
                     if (currentX - advance < distance)
                     {
                         break;
@@ -552,20 +554,20 @@ namespace Avalonia.Media
                 }
 
                 nextCluster = GlyphClusters[currentIndex];
-            }
+            }           
 
             int trailingLength;
 
             if (nextCluster == cluster)
             {
-                trailingLength = Characters.Start + Characters.Length - cluster;
+                trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
             }
             else
             {
                 trailingLength = nextCluster - cluster;
             }
 
-            return new CharacterHit(cluster, trailingLength);
+            return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
         }
 
         /// <summary>
@@ -577,7 +579,7 @@ namespace Avalonia.Media
         private double GetGlyphAdvance(int index, out int cluster)
         {
             cluster = GlyphClusters != null ? GlyphClusters[index] : index;
-            
+
             if (GlyphAdvances != null)
             {
                 return GlyphAdvances[index];
@@ -599,11 +601,18 @@ namespace Avalonia.Media
 
         private GlyphRunMetrics CreateGlyphRunMetrics()
         {
+            if (GlyphClusters != null && GlyphClusters.Count > 0)
+            {
+                var firstCluster = GlyphClusters[0];
+
+                _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
+            }
+
             var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
             var widthIncludingTrailingWhitespace = 0d;
 
             var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
-            
+
             for (var index = 0; index < GlyphIndices.Count; index++)
             {
                 var advance = GetGlyphAdvance(index, out _);
@@ -615,7 +624,7 @@ namespace Avalonia.Media
 
             if (IsLeftToRight)
             {
-                for (var index = GlyphIndices.Count - glyphCount; index <GlyphIndices.Count; index++)
+                for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
                 {
                     width -= GetGlyphAdvance(index, out _);
                 }
@@ -670,34 +679,40 @@ namespace Avalonia.Media
             {
                 for (var i = GlyphClusters.Count - 1; i >= 0; i--)
                 {
-                    var cluster = GlyphClusters[i];
+                    var currentCluster = GlyphClusters[i];
+                    var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
+                    var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
 
-                    var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
-
-                    if (codepointIndex < 0)
+                    if (!codepoint.IsWhiteSpace)
                     {
-                        trailingWhitespaceLength = _characters.Length;
-                        
-                        glyphCount = GlyphClusters.Count;
-                        
                         break;
                     }
-                    
-                    var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
 
-                    if (!codepoint.IsWhiteSpace)
+                    var clusterLength = 1;
+
+                    while(i - 1 >= 0)
                     {
+                        var nextCluster = GlyphClusters[i - 1];
+
+                        if(currentCluster == nextCluster)
+                        {
+                            clusterLength++;
+                            i--;
+
+                            continue;
+                        }
+
                         break;
                     }
 
                     if (codepoint.IsBreakChar)
                     {
-                        newLineLength++;
+                        newLineLength += clusterLength;
                     }
 
-                    trailingWhitespaceLength++;
-                    
-                    glyphCount++;
+                    trailingWhitespaceLength += clusterLength;
+                   
+                    glyphCount++;                   
                 }
             }
 

+ 1 - 3
src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs

@@ -1,6 +1,4 @@
-using Avalonia.Media.TextFormatting.Unicode;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
 {
     /// <summary>
     /// Represents a base class for text formatting.

+ 47 - 18
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -8,6 +8,8 @@ namespace Avalonia.Media.TextFormatting
 {
     internal class TextFormatterImpl : TextFormatter
     {
+        private static readonly char[] s_empty = { ' ' };
+
         /// <inheritdoc cref="TextFormatter.FormatLine"/>
         public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
             TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
@@ -77,14 +79,14 @@ namespace Avalonia.Media.TextFormatting
             {
                 var currentRun = textRuns[i];
 
-                if (currentLength + currentRun.Text.Length < length)
+                if (currentLength + currentRun.TextSourceLength < length)
                 {
                     currentLength += currentRun.TextSourceLength;
 
                     continue;
                 }
 
-                var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
+                var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
 
                 var first = new List<DrawableTextRun>(firstCount);
 
@@ -98,13 +100,13 @@ namespace Avalonia.Media.TextFormatting
 
                 var secondCount = textRuns.Count - firstCount;
 
-                if (currentLength + currentRun.Text.Length == length)
+                if (currentLength + currentRun.TextSourceLength == length)
                 {
                     var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
 
                     if (second != null)
                     {
-                        var offset = currentRun.Text.Length >= 1 ? 1 : 0;
+                        var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
 
                         for (var j = 0; j < secondCount; j++)
                         {
@@ -122,16 +124,14 @@ namespace Avalonia.Media.TextFormatting
 
                     var second = new List<DrawableTextRun>(secondCount);
 
-                    if (currentRun is not ShapedTextCharacters shapedTextCharacters)
+                    if (currentRun is ShapedTextCharacters shapedTextCharacters)
                     {
-                        throw new NotSupportedException("Only shaped runs can be split in between.");
-                    }
-
-                    var split = shapedTextCharacters.Split(length - currentLength);
+                        var split = shapedTextCharacters.Split(length - currentLength);
 
-                    first.Add(split.First);
+                        first.Add(split.First);
 
-                    second.Add(split.Second!);
+                        second.Add(split.Second!);
+                    }                
 
                     for (var j = 1; j < secondCount; j++)
                     {
@@ -267,7 +267,6 @@ namespace Avalonia.Media.TextFormatting
             IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text, TextShaperOptions options)
         {
             var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count);
-            var firstRun = textRuns[0];
 
             var shapedBuffer = TextShaper.Current.ShapeText(text, options);
 
@@ -471,11 +470,10 @@ namespace Avalonia.Media.TextFormatting
             return false;
         }
 
-        private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength)
+        private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, double paragraphWidth, out int measuredLength)
         {
             measuredLength = 0;
             var currentWidth = 0.0;
-            var lastCluster = firstTextSourceIndex;
 
             foreach (var currentRun in textRuns)
             {
@@ -483,12 +481,17 @@ namespace Avalonia.Media.TextFormatting
                 {
                     case ShapedTextCharacters shapedTextCharacters:
                         {
+                            var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
+                            var lastCluster = firstCluster;
+
                             for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
                             {
                                 var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
 
                                 if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
                                 {
+                                    measuredLength += Math.Max(0, lastCluster - firstCluster);
+
                                     goto found;
                                 }
 
@@ -496,6 +499,8 @@ namespace Avalonia.Media.TextFormatting
                                 currentWidth += glyphInfo.GlyphAdvance;
                             }
 
+                            measuredLength += currentRun.TextSourceLength;
+
                             break;
                         }
 
@@ -506,7 +511,7 @@ namespace Avalonia.Media.TextFormatting
                                 goto found;
                             }
 
-                            lastCluster += currentRun.TextSourceLength;
+                            measuredLength += currentRun.TextSourceLength;
                             currentWidth += currentRun.Size.Width;
 
                             break;
@@ -516,11 +521,30 @@ namespace Avalonia.Media.TextFormatting
 
             found:
 
-            measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1);
-
             return measuredLength != 0;
         }
 
+        /// <summary>
+        /// Creates an empty text line.
+        /// </summary>
+        /// <returns>The empty text line.</returns>
+        public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, TextParagraphProperties paragraphProperties)
+        {
+            var flowDirection = paragraphProperties.FlowDirection;
+            var properties = paragraphProperties.DefaultTextRunProperties;
+            var glyphTypeface = properties.Typeface.GlyphTypeface;
+            var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
+            var glyph = glyphTypeface.GetGlyph(s_empty[0]);
+            var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
+
+            var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
+                (sbyte)flowDirection);
+
+            var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
+
+            return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine();
+        }
+
         /// <summary>
         /// Performs text wrapping returns a list of text lines.
         /// </summary>
@@ -535,7 +559,12 @@ namespace Avalonia.Media.TextFormatting
             double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
             TextLineBreak? currentLineBreak)
         {
-            if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength))
+            if(textRuns.Count == 0)
+            {
+                return CreateEmptyTextLine(firstTextSourceIndex, paragraphProperties);
+            }
+
+            if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
             {
                 measuredLength = 1;
             }

+ 66 - 53
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -10,13 +10,12 @@ namespace Avalonia.Media.TextFormatting
     /// </summary>
     public class TextLayout
     {
-        private static readonly char[] s_empty = { ' ' };
-
-        private readonly ReadOnlySlice<char> _text;
+        private readonly ITextSource _textSource;
         private readonly TextParagraphProperties _paragraphProperties;
-        private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textStyleOverrides;
         private readonly TextTrimming _textTrimming;
 
+        private int _textSourceLength;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="TextLayout" /> class.
         /// </summary>
@@ -50,17 +49,49 @@ namespace Avalonia.Media.TextFormatting
             int maxLines = 0,
             IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null)
         {
-            _text = string.IsNullOrEmpty(text) ?
-                new ReadOnlySlice<char>() :
-                new ReadOnlySlice<char>(text.AsMemory());
-
             _paragraphProperties =
                 CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
                     textDecorations, flowDirection, lineHeight);
 
+            _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
+
             _textTrimming = textTrimming ?? TextTrimming.None;
 
-            _textStyleOverrides = textStyleOverrides;
+            LineHeight = lineHeight;
+
+            MaxWidth = maxWidth;
+
+            MaxHeight = maxHeight;
+
+            MaxLines = maxLines;      
+
+            TextLines = CreateTextLines();
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TextLayout" /> class.
+        /// </summary>
+        /// <param name="textSource">The text source.</param>
+        /// <param name="paragraphProperties">The default text paragraph properties.</param>
+        /// <param name="textTrimming">The text trimming.</param>
+        /// <param name="maxWidth">The maximum width.</param>
+        /// <param name="maxHeight">The maximum height.</param>
+        /// <param name="lineHeight">The height of each line of text.</param>
+        /// <param name="maxLines">The maximum number of text lines.</param>
+        public TextLayout(
+            ITextSource textSource,
+            TextParagraphProperties paragraphProperties, 
+            TextTrimming? textTrimming = null,
+            double maxWidth = double.PositiveInfinity,
+            double maxHeight = double.PositiveInfinity,
+            double lineHeight = double.NaN,
+            int maxLines = 0)
+        {
+            _textSource = textSource;
+
+            _paragraphProperties = paragraphProperties;
+
+            _textTrimming = textTrimming ?? TextTrimming.None;
 
             LineHeight = lineHeight;
 
@@ -147,7 +178,7 @@ namespace Avalonia.Media.TextFormatting
                 return new Rect();
             }
 
-            if (textPosition < 0 || textPosition >= _text.Length)
+            if (textPosition < 0 || textPosition >= _textSourceLength)
             {
                 var lastLine = TextLines[TextLines.Count - 1];
 
@@ -273,7 +304,7 @@ namespace Avalonia.Media.TextFormatting
                 return 0;
             }
 
-            if (charIndex > _text.Length)
+            if (charIndex > _textSourceLength)
             {
                 return TextLines.Count - 1;
             }
@@ -375,32 +406,11 @@ namespace Avalonia.Media.TextFormatting
             height += textLine.Height;
         }
 
-        /// <summary>
-        /// Creates an empty text line.
-        /// </summary>
-        /// <returns>The empty text line.</returns>
-        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, firstTextSourceIndex, 1);
-            var glyph = glyphTypeface.GetGlyph(s_empty[0]);
-            var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
-
-            var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
-                (sbyte)flowDirection);
-
-            var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
-
-            return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
-        }
-
         private IReadOnlyList<TextLine> CreateTextLines()
         {
-            if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
+            if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
             {
-                var textLine = CreateEmptyTextLine(0);
+                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties);
 
                 Bounds = new Rect(0,0,0, textLine.Height);
 
@@ -411,26 +421,30 @@ namespace Avalonia.Media.TextFormatting
 
             double left = double.PositiveInfinity, width = 0.0, height = 0.0;
 
-            var currentPosition = 0;
-
-            var textSource = new FormattedTextSource(_text,
-                _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
+            _textSourceLength = 0;
 
             TextLine? previousLine = null;
 
-            while (currentPosition < _text.Length)
+            while (true)
             {
-                var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
+                var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
                     _paragraphProperties, previousLine?.TextLineBreak);
 
-#if DEBUG
-                if (textLine.Length == 0)
+                if(textLine == null || textLine.Length == 0)
                 {
-                    throw new InvalidOperationException($"{nameof(textLine)} should not be empty.");
+                    if(previousLine != null && previousLine.NewLineLength  > 0)
+                    {
+                        var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, _paragraphProperties);
+
+                        textLines.Add(emptyTextLine);
+
+                        UpdateBounds(emptyTextLine, ref left, ref width, ref height);
+                    }
+
+                    break;
                 }
-#endif
 
-                currentPosition += textLine.Length;
+                _textSourceLength += textLine.Length;
                 
                 //Fulfill max height constraint
                 if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
@@ -464,17 +478,16 @@ namespace Avalonia.Media.TextFormatting
                 {
                     break;
                 }
-                
-                if (currentPosition != _text.Length || textLine.NewLineLength <= 0)
-                {
-                    continue;
-                }
+            }
 
-                var emptyTextLine = CreateEmptyTextLine(currentPosition);
+            //Make sure the TextLayout always contains at least on empty line
+            if(textLines.Count == 0)
+            {
+                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties);
 
-                textLines.Add(emptyTextLine);
+                textLines.Add(textLine);
 
-                UpdateBounds(emptyTextLine,ref left, ref width, ref height);
+                UpdateBounds(textLine, ref left, ref width, ref height);
             }
 
             Bounds = new Rect(left, 0, width, height);

+ 58 - 60
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -183,6 +183,7 @@ namespace Avalonia.Media.TextFormatting
                     case ShapedTextCharacters shapedRun:
                         {
                             characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+
                             break;
                         }
                     default:
@@ -403,7 +404,7 @@ namespace Avalonia.Media.TextFormatting
             var result = new List<TextBounds>(TextRuns.Count);
             var lastDirection = _flowDirection;
             var currentDirection = lastDirection;
-            var currentPosition = 0;
+            var currentPosition = FirstTextSourceIndex;
             var currentRect = Rect.Empty;
             var startX = Start;
 
@@ -417,6 +418,11 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
+                if(currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex)
+                {
+                    continue;
+                }
+
                 TextRun? nextRun = null;
 
                 if (index + 1 < TextRuns.Count)
@@ -426,31 +432,42 @@ namespace Avalonia.Media.TextFormatting
 
                 if (nextRun != null)
                 {
-                    if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
+                    switch (nextRun)
                     {
-                        goto skip;
-                    }
+                        case ShapedTextCharacters when currentRun is ShapedTextCharacters:
+                            {
+                                if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
+                                {
+                                    goto skip;
+                                }
 
-                    if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
-                    {
-                        goto skip;
-                    }
+                                if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
+                                {
+                                    goto skip;
+                                }
 
-                    if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
-                    {
-                        goto skip;
-                    }
+                                if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
+                                {
+                                    goto skip;
+                                }
 
-                    if (currentRun.Text.End < firstTextSourceCharacterIndex)
-                    {
-                        goto skip;
-                    }
+                                if (currentRun.Text.End < firstTextSourceCharacterIndex)
+                                {
+                                    goto skip;
+                                }
 
-                    goto noop;
+                                goto noop;
+                            }
+                        default:
+                            {
+                               goto noop;
+                            }
+                    }
 
                 skip:
                     {
                         startX += currentRun.Size.Width;
+                        currentPosition += currentRun.TextSourceLength;
                     }
 
                     continue;
@@ -460,7 +477,6 @@ namespace Avalonia.Media.TextFormatting
                     }
                 }
 
-
                 var endX = startX;
                 var endOffset = 0d;
 
@@ -520,11 +536,13 @@ namespace Avalonia.Media.TextFormatting
                         }
                     default:
                         {
-                            if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length)
+                            if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength)
                             {
                                 endX += currentRun.Size.Width;
                             }
 
+                            currentPosition += currentRun.TextSourceLength;
+
                             break;
                         }
                 }
@@ -538,7 +556,9 @@ namespace Avalonia.Media.TextFormatting
 
                 if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
                 {
-                    var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection);
+                    currentRect = currentRect.WithWidth(currentRect.Width + width);
+
+                    var textBounds = new TextBounds(currentRect, currentDirection);
 
                     result[result.Count - 1] = textBounds;
                 }
@@ -551,21 +571,9 @@ namespace Avalonia.Media.TextFormatting
 
                 if (currentDirection == FlowDirection.LeftToRight)
                 {
-                    if (nextRun != null)
-                    {
-                        if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
-                        {
-                            break;
-                        }
-
-                        currentPosition = nextRun.Text.End;
-                    }
-                    else
+                    if (currentPosition >= firstTextSourceCharacterIndex + textLength)
                     {
-                        if (currentPosition >= firstTextSourceCharacterIndex + textLength)
-                        {
-                            break;
-                        }
+                        break;
                     }
                 }
                 else
@@ -575,10 +583,7 @@ namespace Avalonia.Media.TextFormatting
                         break;
                     }
 
-                    if (currentPosition != currentRun.Text.Start)
-                    {
-                        endX += currentRun.Size.Width - endOffset;
-                    }
+                    endX += currentRun.Size.Width - endOffset;
                 }
 
                 lastDirection = currentDirection;
@@ -1018,31 +1023,21 @@ namespace Avalonia.Media.TextFormatting
 
         private TextLineMetrics CreateLineMetrics()
         {
-            var start = 0d;
-            var height = 0d;
+            var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
+            var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
+            var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
+          
             var width = 0d;
             var widthIncludingWhitespace = 0d;
             var trailingWhitespaceLength = 0;
             var newLineLength = 0;
-            var ascent = 0d;
-            var descent = 0d;
-            var lineGap = 0d;
-            var fontRenderingEmSize = 0d;
+            var ascent = glyphTypeface.Ascent * scale;
+            var descent = glyphTypeface.Descent * scale;
+            var lineGap = glyphTypeface.LineGap * scale;
 
-            var lineHeight = _paragraphProperties.LineHeight;
-
-            if (_textRuns.Count == 0)
-            {
-                var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
-                fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
-                var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
-                ascent = glyphTypeface.Ascent * scale;
-                height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
-                descent - ascent + lineGap :
-                lineHeight;
-
-                return new TextLineMetrics(false, height, 0, start, -ascent, 0, 0, 0);
-            }
+            var height = descent - ascent + lineGap;
+         
+            var lineHeight = _paragraphProperties.LineHeight;            
 
             for (var index = 0; index < _textRuns.Count; index++)
             {
@@ -1166,12 +1161,15 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
+            var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
                 _paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
 
             if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
             {
-                height = lineHeight;
+                if(lineHeight > height)
+                {
+                    height = lineHeight;
+                }              
             }
 
             return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,

+ 5 - 0
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@@ -238,6 +238,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
             _levelRuns.Clear();
             _resolvedLevelsBuffer.Clear();
 
+            if (types.IsEmpty)
+            {
+                return;
+            }
+
             // Setup original types and working types
             _originalClasses = types;
             _workingClasses = _workingClassesBuffer.Add(types);

+ 11 - 0
src/Avalonia.Controls/Documents/IInlineHost.cs

@@ -0,0 +1,11 @@
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Controls.Documents
+{
+    internal interface IInlineHost : ILogical
+    {
+        void AddVisualChild(IControl child);
+
+        void Invalidate();
+    }
+}

+ 4 - 5
src/Avalonia.Controls/Documents/Inline.cs

@@ -2,9 +2,8 @@ using System.Collections.Generic;
 using System.Text;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
 
-namespace Avalonia.Controls.Documents 
+namespace Avalonia.Controls.Documents
 {
     /// <summary>
     /// Inline element.
@@ -45,9 +44,9 @@ namespace Avalonia.Controls.Documents
             set { SetValue(BaselineAlignmentProperty, value); }
         }
 
-        internal abstract int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex);
+        internal abstract void BuildTextRun(IList<TextRun> textRuns);
 
-        internal abstract int AppendText(StringBuilder stringBuilder);
+        internal abstract void AppendText(StringBuilder stringBuilder);
 
         protected TextRunProperties CreateTextRunProperties()
         {
@@ -63,7 +62,7 @@ namespace Avalonia.Controls.Documents
             {
                 case nameof(TextDecorations):
                 case nameof(BaselineAlignment):
-                    Invalidate();
+                    InlineHost?.Invalidate();
                     break;
             }
         }

+ 36 - 10
src/Avalonia.Controls/Documents/InlineCollection.cs

@@ -12,29 +12,37 @@ namespace Avalonia.Controls.Documents
     [WhitespaceSignificantCollection]
     public class InlineCollection : AvaloniaList<Inline>
     {
+        private readonly IInlineHost? _host;
         private string? _text = string.Empty;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="InlineCollection"/> class.
         /// </summary>
-        public InlineCollection(ILogical parent) : base(0)
+        public InlineCollection(ILogical parent) : this(parent, null) { }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="InlineCollection"/> class.
+        /// </summary>
+        internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0)
         {
+            _host = host;
+
             ResetBehavior = ResetBehavior.Remove;
             
             this.ForEachItem(
                 x =>
                 {
                     ((ISetLogicalParent)x).SetParent(parent);
-                    x.Invalidated += Invalidate;
-                    Invalidate();
+                    x.InlineHost = host;
+                    host?.Invalidate();
                 },
                 x =>
                 {
                     ((ISetLogicalParent)x).SetParent(null);
-                    x.Invalidated -= Invalidate;
-                    Invalidate();
+                    x.InlineHost = host;
+                    host?.Invalidate();
                 },
-                () => throw new NotSupportedException());
+                () => throw new NotSupportedException());         
         }
 
         public bool HasComplexContent => Count > 0;
@@ -96,12 +104,22 @@ namespace Avalonia.Controls.Documents
             }
         }
 
+        public void Add(IControl child)
+        {
+            var implicitRun = new InlineUIContainer(child);
+
+            Add(implicitRun);
+        }
+
         public override void Add(Inline item)
         {
             if (!HasComplexContent)
             {
-                base.Add(new Run(_text));
-                
+                if (!string.IsNullOrEmpty(_text))
+                {
+                    base.Add(new Run(_text));
+                }
+                             
                 _text = string.Empty;
             }
             
@@ -112,11 +130,19 @@ namespace Avalonia.Controls.Documents
         /// Raised when an inline in the collection changes.
         /// </summary>
         public event EventHandler? Invalidated;
-        
+
         /// <summary>
         /// Raises the <see cref="Invalidated"/> event.
         /// </summary>
-        protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
+        protected void Invalidate()
+        {
+            if(_host != null)
+            {
+                _host.Invalidate();
+            }
+
+            Invalidated?.Invoke(this, EventArgs.Empty);
+        }
 
         private void Invalidate(object? sender, EventArgs e) => Invalidate();
     }

+ 125 - 0
src/Avalonia.Controls/Documents/InlineUIContainer.cs

@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Metadata;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Documents
+{
+    /// <summary>
+    /// InlineUIContainer - a wrapper for embedded UIElements in text 
+    /// flow content inline collections
+    /// </summary>
+    public class InlineUIContainer : Inline
+    {
+        /// <summary>
+        /// Defines the <see cref="Child"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IControl> ChildProperty =
+            AvaloniaProperty.Register<InlineUIContainer, IControl>(nameof(Child));
+
+        static InlineUIContainer()
+        {
+            BaselineAlignmentProperty.OverrideDefaultValue<InlineUIContainer>(BaselineAlignment.Top);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of InlineUIContainer element.
+        /// </summary>
+        /// <remarks>
+        /// The purpose of this element is to be a wrapper for UIElements
+        /// when they are embedded into text flow - as items of
+        /// InlineCollections.
+        /// </remarks>
+        public InlineUIContainer()
+        {
+        }
+
+        /// <summary>
+        /// Initializes an InlineBox specifying its child UIElement
+        /// </summary>
+        /// <param name="child">
+        /// UIElement set as a child of this inline item
+        /// </param>
+        public InlineUIContainer(IControl child)
+        {
+            Child = child;
+        }
+
+        /// <summary>
+        /// The content spanned by this TextElement.
+        /// </summary>
+        [Content]
+        public IControl Child
+        {
+            get => GetValue(ChildProperty);
+            set => SetValue(ChildProperty, value);
+        }
+
+        internal override void BuildTextRun(IList<TextRun> textRuns)
+        {
+            if(InlineHost == null)
+            {
+                return;
+            }
+
+            ((ISetLogicalParent)Child).SetParent(InlineHost);
+
+            InlineHost.AddVisualChild(Child);
+
+            textRuns.Add(new InlineRun(Child, CreateTextRunProperties()));
+        }
+
+        internal override void AppendText(StringBuilder stringBuilder)
+        {
+        }
+
+        private class InlineRun : DrawableTextRun
+        {
+            public InlineRun(IControl control, TextRunProperties properties)
+            {
+                Control = control;
+                Properties = properties;
+            }
+
+            public IControl Control { get; }
+
+            public override TextRunProperties? Properties { get; }
+
+            public override Size Size
+            {
+                get
+                {
+                    if (!Control.IsMeasureValid)
+                    {
+                        Control.Measure(Size.Infinity);
+                    }
+
+                    return Control.DesiredSize;
+                }
+            }
+
+            public override double Baseline
+            {
+                get
+                {
+                    double baseline = Size.Height;
+                    double baselineOffsetValue = Control.GetValue<double>(TextBlock.BaselineOffsetProperty);
+
+                    if (!MathUtilities.IsZero(baselineOffsetValue))
+                    {
+                        baseline = baselineOffsetValue;
+                    }
+
+                    return -baseline;
+                }
+            }
+
+            public override void Draw(DrawingContext drawingContext, Point origin)
+            {             
+                Control.Arrange(new Rect(origin, Size));                
+            }
+        }
+    }
+}

+ 5 - 15
src/Avalonia.Controls/Documents/LineBreak.cs

@@ -1,9 +1,9 @@
 using System;
 using System.Collections.Generic;
 using System.Text;
+using Avalonia.LogicalTree;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
-using Avalonia.Utilities;
 
 namespace Avalonia.Controls.Documents 
 {
@@ -20,24 +20,14 @@ namespace Avalonia.Controls.Documents
         {
         }
 
-        internal override int BuildRun(StringBuilder stringBuilder,
-            IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
+        internal override void BuildTextRun(IList<TextRun> textRuns)
         {
-            var length = AppendText(stringBuilder);
-
-            textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
-                CreateTextRunProperties()));
-
-            return length;
+            textRuns.Add(new TextEndOfLine());
         }
 
-        internal override int AppendText(StringBuilder stringBuilder)
+        internal override void AppendText(StringBuilder stringBuilder)
         {
-            var text = Environment.NewLine;
-
-            stringBuilder.Append(text);
-
-            return text.Length;
+            stringBuilder.Append(Environment.NewLine);
         }
     }
 }

+ 8 - 11
src/Avalonia.Controls/Documents/Run.cs

@@ -4,7 +4,6 @@ using System.Text;
 using Avalonia.Data;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
-using Avalonia.Utilities;
 
 namespace Avalonia.Controls.Documents
 {
@@ -51,24 +50,22 @@ namespace Avalonia.Controls.Documents
             set { SetValue (TextProperty, value); }
         }
 
-        internal override int BuildRun(StringBuilder stringBuilder,
-            IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
+        internal override void BuildTextRun(IList<TextRun> textRuns)
         {
-            var length = AppendText(stringBuilder);
+            var text = (Text ?? "").AsMemory();
 
-            textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
-                CreateTextRunProperties()));
+            var textRunProperties = CreateTextRunProperties();           
 
-            return length;
+            var textCharacters = new TextCharacters(text, textRunProperties);
+
+            textRuns.Add(textCharacters);
         }
 
-        internal override int AppendText(StringBuilder stringBuilder)
+        internal override void AppendText(StringBuilder stringBuilder)
         {
             var text = Text ?? "";
 
             stringBuilder.Append(text);
-
-            return text.Length;
         }
 
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@@ -78,7 +75,7 @@ namespace Avalonia.Controls.Documents
             switch (change.Property.Name)
             {
                 case nameof(Text):
-                    Invalidate();
+                    InlineHost?.Invalidate();
                     break;
             }
         }

+ 13 - 33
src/Avalonia.Controls/Documents/Span.cs

@@ -1,8 +1,8 @@
+using System;
 using System.Collections.Generic;
 using System.Text;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
-using Avalonia.Utilities;
 
 namespace Avalonia.Controls.Documents
 {
@@ -25,8 +25,7 @@ namespace Avalonia.Controls.Documents
         public Span()
         {
             Inlines = new InlineCollection(this);
-
-            Inlines.Invalidated += (s, e) => Invalidate();
+            Inlines.Invalidated += (s, e) => InlineHost?.Invalidate();
         }
 
         /// <summary>
@@ -35,61 +34,42 @@ namespace Avalonia.Controls.Documents
         [Content]
         public InlineCollection Inlines { get; }
 
-        internal override int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
+        internal override void BuildTextRun(IList<TextRun> textRuns)
         {
-            var length = 0;
-
             if (Inlines.HasComplexContent)
             {
                 foreach (var inline in Inlines)
                 {
-                    var inlineLength = inline.BuildRun(stringBuilder, textStyleOverrides, firstCharacterIndex);
-
-                    firstCharacterIndex += inlineLength;
-
-                    length += inlineLength;
+                    inline.BuildTextRun(textRuns);
                 }
             }
             else
             {
-                if (Inlines.Text == null)
+                if (Inlines.Text is string text)
                 {
-                    return length;
-                }
-                
-                stringBuilder.Append(Inlines.Text);
+                    var textRunProperties = CreateTextRunProperties();
 
-                length = Inlines.Text.Length;
+                    var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties);
 
-                textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
-                    CreateTextRunProperties()));
+                    textRuns.Add(textCharacters);
+                }          
             }
-
-            return length;
         }
 
-        internal override int AppendText(StringBuilder stringBuilder)
+        internal override void AppendText(StringBuilder stringBuilder)
         {
             if (Inlines.HasComplexContent)
             {
-                var length = 0;
-
                 foreach (var inline in Inlines)
                 {
-                    length += inline.AppendText(stringBuilder);
+                    inline.AppendText(stringBuilder);
                 }
-
-                return length;
             }
 
-            if (Inlines.Text == null)
+            if (Inlines.Text is string text)
             {
-                return 0;
+                stringBuilder.Append(text);
             }
-         
-            stringBuilder.Append(Inlines.Text);
-
-            return Inlines.Text.Length;
         }
     }
 }

+ 3 - 12
src/Avalonia.Controls/Documents/TextElement.cs

@@ -1,5 +1,4 @@
-using System;
-using Avalonia.Media;
+using Avalonia.Media;
 
 namespace Avalonia.Controls.Documents
 {
@@ -251,10 +250,7 @@ namespace Avalonia.Controls.Documents
             control.SetValue(ForegroundProperty, value);
         }
 
-        /// <summary>
-        /// Raised when the visual representation of the text element changes.
-        /// </summary>
-        public event EventHandler? Invalidated;
+        internal IInlineHost? InlineHost { get; set; }
 
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
         {
@@ -269,14 +265,9 @@ namespace Avalonia.Controls.Documents
                 case nameof(FontWeight):
                 case nameof(FontStretch):
                 case nameof(Foreground):
-                    Invalidate();
+                    InlineHost?.Invalidate();
                     break;
             }
         }
-
-        /// <summary>
-        /// Raises the <see cref="Invalidate"/> event.
-        /// </summary>
-        protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
     }
 }

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

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

+ 108 - 31
src/Avalonia.Controls/TextBlock.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control that displays a block of text.
     /// </summary>
-    public class TextBlock : Control
+    public class TextBlock : Control, IInlineHost
     {
         /// <summary>
         /// Defines the <see cref="Background"/> property.
@@ -155,9 +155,7 @@ namespace Avalonia.Controls
         /// </summary>
         public TextBlock()
         {
-            Inlines = new InlineCollection(this);
-
-            Inlines.Invalidated += InlinesChanged;
+            Inlines = new InlineCollection(this, this);
         }
 
         /// <summary>
@@ -211,7 +209,7 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Gets or sets the inlines.
+        /// Gets the inlines.
         /// </summary>
         [Content]
         public InlineCollection Inlines { get; }
@@ -552,38 +550,41 @@ namespace Avalonia.Controls
         /// <returns>A <see cref="TextLayout"/> object.</returns>
         protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
         {
-            List<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
+            var defaultProperties = new GenericTextRunProperties(
+                new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
+                FontSize,
+                TextDecorations,
+                Foreground);
+
+            var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
+                defaultProperties, TextWrapping, LineHeight, 0);
+
+            ITextSource textSource;
 
             if (Inlines.HasComplexContent)
             {
-                textStyleOverrides = new List<ValueSpan<TextRunProperties>>(Inlines.Count);
-
-                var textPosition = 0;
-                var stringBuilder = new StringBuilder();
+                var textRuns = new List<TextRun>();
 
                 foreach (var inline in Inlines)
                 {
-                    textPosition += inline.BuildRun(stringBuilder, textStyleOverrides, textPosition);
+                    inline.BuildTextRun(textRuns);
                 }
 
-                text = stringBuilder.ToString();
+                textSource = new InlinesTextSource(textRuns);
+            }
+            else
+            {
+                textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
             }
 
             return new TextLayout(
-                text ?? string.Empty,
-                new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
-                FontSize,
-                Foreground ?? Brushes.Transparent,
-                TextAlignment,
-                TextWrapping,
+                textSource,
+                paragraphProperties,
                 TextTrimming,
-                TextDecorations,
-                FlowDirection,
                 constraint.Width,
                 constraint.Height,
                 maxLines: MaxLines,
-                lineHeight: LineHeight,
-                textStyleOverrides: textStyleOverrides);
+                lineHeight: LineHeight);
         }
 
         /// <summary>
@@ -592,7 +593,7 @@ namespace Avalonia.Controls
         protected void InvalidateTextLayout()
         {
             _textLayout = null;
-            
+
             InvalidateMeasure();
         }
 
@@ -604,9 +605,9 @@ namespace Avalonia.Controls
             }
 
             var padding = Padding;
-            
+
             _constraint = availableSize.Deflate(padding);
-            
+
             _textLayout = null;
 
             InvalidateArrange();
@@ -622,9 +623,13 @@ namespace Avalonia.Controls
             {
                 return finalSize;
             }
-            
-            _constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height));
-            
+
+            var padding = Padding;
+
+            var textSize = finalSize.Deflate(padding);
+
+            _constraint = new Size(textSize.Width, Math.Ceiling(textSize.Height));
+
             _textLayout = null;
 
             return finalSize;
@@ -660,8 +665,6 @@ namespace Avalonia.Controls
                 case nameof (Padding):
                 case nameof (LineHeight):
                 case nameof (MaxLines):
-                    
-                case nameof (InlinesProperty):
 
                 case nameof (Text):
                 case nameof (TextDecorations):
@@ -673,9 +676,83 @@ namespace Avalonia.Controls
             }
         }
 
- 		private void InlinesChanged(object? sender, EventArgs e)
+        private void InlinesChanged(object? sender, EventArgs e)
+        {
+            InvalidateTextLayout();
+        }
+
+        void IInlineHost.AddVisualChild(IControl child)
+        {
+            if (child.VisualParent == null)
+            {
+                VisualChildren.Add(child);
+            }            
+        }
+
+        void IInlineHost.Invalidate()
         {
             InvalidateTextLayout();
         }
+
+        private readonly struct InlinesTextSource : ITextSource
+        {
+            private readonly IReadOnlyList<TextRun> _textRuns;
+
+            public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
+            {
+                _textRuns = textRuns;
+            }
+
+            public TextRun? GetTextRun(int textSourceIndex)
+            {
+                var currentPosition = 0;
+
+                foreach (var textRun in _textRuns)
+                {
+                    if(textRun.TextSourceLength == 0)
+                    {
+                        continue;
+                    }
+
+                    if(currentPosition >= textSourceIndex)
+                    {
+                        return textRun;
+                    }
+
+                    currentPosition += textRun.TextSourceLength;
+                }
+
+                return null;
+            }
+        }
+
+        private readonly struct SimpleTextSource : ITextSource
+        {
+            private readonly ReadOnlySlice<char> _text;
+            private readonly TextRunProperties _defaultProperties;
+
+            public SimpleTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties)
+            {
+                _text = text;
+                _defaultProperties = defaultProperties;
+            }
+
+            public TextRun? GetTextRun(int textSourceIndex)
+            {
+                if (textSourceIndex > _text.Length)
+                {
+                    return null;
+                }
+
+                var runText = _text.Skip(textSourceIndex);
+
+                if (runText.IsEmpty)
+                {
+                    return null;
+                }
+
+                return new TextCharacters(runText, _defaultProperties);
+            }
+        }
     }
 }

+ 1 - 2
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Globalization;
-using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Platform;
@@ -59,7 +58,7 @@ namespace Avalonia.Skia
 
                     var glyphIndex = (ushort)sourceInfo.Codepoint;
 
-                    var glyphCluster = (int)sourceInfo.Cluster;
+                    var glyphCluster = (int)(sourceInfo.Cluster);
 
                     var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
 

+ 0 - 3
tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs

@@ -1,11 +1,8 @@
 using Avalonia.Media;
-using Avalonia.Platform;
 using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Runtime.InteropServices;
-using System.Text;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Media.TextFormatting;

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

@@ -70,12 +70,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 }
             }
         }
-        
+
         [Fact]
         public void Should_Get_Next_Caret_CharacterHit_Bidi()
         {
             const string text = "אבג 1 ABC";
-            
+
             using (Start())
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var clusters = new List<int>();
 
-                foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
+                foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
                 {
                     var shapedRun = (ShapedTextCharacters)textRun;
 
@@ -98,7 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
                         shapedRun.ShapedBuffer.GlyphClusters);
                 }
-                
+
                 var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
 
                 foreach (var cluster in clusters)
@@ -122,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
         public void Should_Get_Previous_Caret_CharacterHit_Bidi()
         {
             const string text = "אבג 1 ABC";
-            
+
             using (Start())
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 var clusters = new List<int>();
 
-                foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
+                foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
                 {
                     var shapedRun = (ShapedTextCharacters)textRun;
 
@@ -147,13 +147,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 }
 
                 clusters.Reverse();
-                
+
                 var nextCharacterHit = new CharacterHit(text.Length - 1);
 
                 foreach (var cluster in clusters)
                 {
                     var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
-                    
+
                     Assert.Equal(cluster, currentCaretIndex);
 
                     nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit);
@@ -168,7 +168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
             }
         }
-        
+
         [InlineData("𐐷𐐷𐐷𐐷𐐷")]
         [InlineData("01234567🎉\n")]
         [InlineData("𐐷1234")]
@@ -324,7 +324,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     }
                 }
 
-                Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
+                Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
             }
         }
 
@@ -371,7 +371,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026");
                 yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026");
                 yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, "");
-                
+
                 object[] CreateData(string text, double width, TextTrimming mode, string expected)
                 {
                     return new object[]
@@ -424,7 +424,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             {
                 var defaultProperties = new GenericTextRunProperties(Typeface.Default);
                 var textSource = new DrawableRunTextSource();
-                
+
                 var formatter = new TextFormatterImpl();
 
                 var textLine =
@@ -471,7 +471,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(4, textLine.TextRuns.Count);
 
-                var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1));
+                var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
 
                 Assert.Equal(3, currentHit.FirstCharacterIndex);
                 Assert.Equal(0, currentHit.TrailingLength);
@@ -552,11 +552,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 switch (textSourceIndex)
                 {
                     case 0:
-                        return new CustomDrawableRun();                     
+                        return new CustomDrawableRun();
                     case 1:
                         return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default));
                     case 2:
-                        return new CustomDrawableRun();                      
+                        return new CustomDrawableRun();
                     case 3:
                         return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default));
                     default:
@@ -564,14 +564,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 }
             }
         }
-        
+
         private class CustomDrawableRun : DrawableTextRun
         {
             public override Size Size => new(14, 14);
             public override double Baseline => 14;
             public override void Draw(DrawingContext drawingContext, Point origin)
             {
-               
+
             }
         }
 
@@ -587,29 +587,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             var shapedTextRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
 
             var lastCluster = -1;
-            
+
             foreach (var textRun in shapedTextRuns)
             {
                 var shapedBuffer = textRun.ShapedBuffer;
 
                 var currentClusters = shapedBuffer.GlyphClusters.ToList();
 
-                foreach (var currentCluster in currentClusters) 
+                foreach (var currentCluster in currentClusters)
                 {
                     if (lastCluster == currentCluster)
                     {
                         continue;
                     }
-                    
+
                     glyphClusters.Add(currentCluster);
 
                     lastCluster = currentCluster;
                 }
             }
-            
+
             return glyphClusters;
         }
-        
+
         private static List<Rect> BuildRects(TextLine textLine)
         {
             var rects = new List<Rect>();
@@ -624,11 +624,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             foreach (var textRun in shapedTextRuns)
             {
                 var shapedBuffer = textRun.ShapedBuffer;
-            
+
                 for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++)
                 {
                     var currentCluster = shapedBuffer.GlyphClusters[index];
-                
+
                     var advance = shapedBuffer.GlyphAdvances[index];
 
                     if (lastCluster != currentCluster)
@@ -642,10 +642,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                         rects.Remove(rect);
 
                         rect = rect.WithWidth(rect.Width + advance);
-                    
+
                         rects.Add(rect);
                     }
-                    
+
                     currentX += advance;
 
                     lastCluster = currentCluster;
@@ -655,8 +655,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             return rects;
         }
 
+
+        [Fact]
+        public void Should_Get_TextBounds_Mixed()
+        {
+            using (Start())
+            {
+                var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+                var text = "0123".AsMemory();
+                var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
+
+                var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, 1, text.Length), shaperOption), defaultProperties);
+
+                var textRuns = new List<TextRun>
+                {
+                    new CustomDrawableRun(),
+                    firstRun,
+                    new CustomDrawableRun(),
+                    new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length + 2, text.Length), shaperOption), defaultProperties),
+                    new CustomDrawableRun(),
+                    new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties)
+                };
+
+                var textSource = new FixedRunsTextSource(textRuns);
+
+                var formatter = new TextFormatterImpl();
+
+                var textLine =
+                    formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+                        new GenericTextParagraphProperties(defaultProperties));
+
+                var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
+
+                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
+
+                textBounds = textLine.GetTextBounds(0, 1);
+
+                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(14, textBounds[0].Rectangle.Width);
+
+                textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1);
+
+                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
+
+                textBounds = textLine.GetTextBounds(1, firstRun.Text.Length);
+
+                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
+
+                textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1);
+
+                Assert.Equal(1, textBounds.Count);
+                Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
+            }
+        }
+
         [Fact]
-        public void Should_Get_TextBounds()
+        public void Should_Get_TextBounds_BiDi()
         {
             using (Start())
             {
@@ -673,7 +730,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 3, text.Length), ltrOptions), defaultProperties)
                 };
 
-             
+
                 var textSource = new FixedRunsTextSource(textRuns);
 
                 var formatter = new TextFormatterImpl();
@@ -700,12 +757,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
             public TextRun? GetTextRun(int textSourceIndex)
             {
+                var currentPosition = 0;
+
                 foreach (var textRun in _textRuns)
                 {
-                    if(textRun.Text.Start == textSourceIndex)
+                    if (currentPosition == textSourceIndex)
                     {
                         return textRun;
                     }
+
+                    currentPosition += textRun.TextSourceLength;
                 }
 
                 return null;