Browse Source

Implement InlineUIContainer

Benedikt Stebner 3 years ago
parent
commit
768024a879

+ 8 - 3
src/Avalonia.Controls/Documents/Inline.cs

@@ -1,8 +1,8 @@
 using System.Collections.Generic;
 using System.Text;
+using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
-using Avalonia.Utilities;
 
 namespace Avalonia.Controls.Documents 
 {
@@ -45,9 +45,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, IInlinesHost parent);
 
-        internal abstract int AppendText(StringBuilder stringBuilder);
+        internal abstract void AppendText(StringBuilder stringBuilder);
 
         protected TextRunProperties CreateTextRunProperties()
         {
@@ -68,4 +68,9 @@ namespace Avalonia.Controls.Documents
             }
         }
     }
+
+    public interface IInlinesHost : ILogical
+    {
+        void AddVisualChild(IControl child);
+    }
 }

+ 12 - 0
src/Avalonia.Controls/Documents/InlineCollection.cs

@@ -96,6 +96,18 @@ namespace Avalonia.Controls.Documents
             }
         }
 
+        public void Add(IControl child)
+        {
+            if (!HasComplexContent && !string.IsNullOrEmpty(_text))
+            {
+                base.Add(new Run(_text));
+
+                _text = string.Empty;
+            }
+
+            base.Add(new InlineUIContainer(child));
+        }
+
         public override void Add(Inline item)
         {
             if (!HasComplexContent)

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

@@ -0,0 +1,121 @@
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.LogicalTree;
+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, IInlinesHost parent)
+        {
+            ((ISetLogicalParent)Child).SetParent(parent);
+
+            parent.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 = (double)Control.GetValue(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, IInlinesHost parent)
         {
-            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 - 10
src/Avalonia.Controls/Documents/Run.cs

@@ -2,9 +2,9 @@ using System;
 using System.Collections.Generic;
 using System.Text;
 using Avalonia.Data;
+using Avalonia.LogicalTree;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
-using Avalonia.Utilities;
 
 namespace Avalonia.Controls.Documents
 {
@@ -51,24 +51,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, IInlinesHost parent)
         {
-            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<T>(AvaloniaPropertyChangedEventArgs<T> change)

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

@@ -1,5 +1,7 @@
+using System;
 using System.Collections.Generic;
 using System.Text;
+using Avalonia.LogicalTree;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
 using Avalonia.Utilities;
@@ -35,61 +37,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, IInlinesHost parent)
         {
-            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, parent);
                 }
             }
             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;
         }
     }
 }

+ 101 - 25
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, IInlinesHost
     {
         /// <summary>
         /// Defines the <see cref="Background"/> property.
@@ -400,38 +400,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, this);
                 }
 
-                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>
@@ -440,7 +443,7 @@ namespace Avalonia.Controls
         protected void InvalidateTextLayout()
         {
             _textLayout = null;
-            
+
             InvalidateMeasure();
         }
 
@@ -452,9 +455,9 @@ namespace Avalonia.Controls
             }
 
             var padding = Padding;
-            
+
             _constraint = availableSize.Deflate(padding);
-            
+
             _textLayout = null;
 
             InvalidateArrange();
@@ -470,9 +473,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;
@@ -521,9 +528,78 @@ namespace Avalonia.Controls
             }
         }
 
- 		private void InlinesChanged(object? sender, EventArgs e)
+        private void InlinesChanged(object? sender, EventArgs e)
         {
             InvalidateTextLayout();
         }
+
+        void IInlinesHost.AddVisualChild(IControl child)
+        {
+            if (child.VisualParent == null)
+            {
+                VisualChildren.Add(child);
+            }            
+        }
+
+        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);
+            }
+        }
     }
 }

+ 2 - 4
src/Avalonia.Visuals/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.
@@ -40,7 +38,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="previousLineBreak">A <see cref="TextLineBreak"/> value that specifies the text formatter state,
         /// in terms of where the previous line in the paragraph was broken by the text formatting process.</param>
         /// <returns>The formatted line.</returns>
-        public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+        public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
             TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null);
     }
 }

+ 16 - 7
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting
     internal class TextFormatterImpl : TextFormatter
     {
         /// <inheritdoc cref="TextFormatter.FormatLine"/>
-        public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+        public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
             TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
         {
             var textWrapping = paragraphProperties.TextWrapping;
@@ -20,6 +20,11 @@ namespace Avalonia.Media.TextFormatting
             var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
                 out var textEndOfLine, out var textSourceLength);
 
+            if(textRuns.Count == 0)
+            {
+                return null;
+            }
+
             if (previousLineBreak?.RemainingRuns != null)
             {
                 flowDirection = previousLineBreak.FlowDirection;
@@ -471,11 +476,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 +487,17 @@ namespace Avalonia.Media.TextFormatting
                 {
                     case ShapedTextCharacters shapedTextCharacters:
                         {
+                            var firstCluster = shapedTextCharacters.Text.Start;
+                            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 + 1);
+
                                     goto found;
                                 }
 
@@ -496,6 +505,8 @@ namespace Avalonia.Media.TextFormatting
                                 currentWidth += glyphInfo.GlyphAdvance;
                             }
 
+                            measuredLength += currentRun.TextSourceLength;
+
                             break;
                         }
 
@@ -506,7 +517,7 @@ namespace Avalonia.Media.TextFormatting
                                 goto found;
                             }
 
-                            lastCluster += currentRun.TextSourceLength;
+                            measuredLength += currentRun.TextSourceLength;
                             currentWidth += currentRun.Size.Width;
 
                             break;
@@ -516,8 +527,6 @@ namespace Avalonia.Media.TextFormatting
 
             found:
 
-            measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1);
-
             return measuredLength != 0;
         }
 
@@ -535,7 +544,7 @@ namespace Avalonia.Media.TextFormatting
             double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
             TextLineBreak? currentLineBreak)
         {
-            if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength))
+            if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
             {
                 measuredLength = 1;
             }

+ 65 - 29
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -12,11 +12,12 @@ namespace Avalonia.Media.TextFormatting
     {
         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 +51,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 +180,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 +306,7 @@ namespace Avalonia.Media.TextFormatting
                 return 0;
             }
 
-            if (charIndex > _text.Length)
+            if (charIndex > _textSourceLength)
             {
                 return TextLines.Count - 1;
             }
@@ -398,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
 
         private IReadOnlyList<TextLine> CreateTextLines()
         {
-            if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
+            if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
             {
                 var textLine = CreateEmptyTextLine(0);
 
@@ -411,26 +444,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 = CreateEmptyTextLine(_textSourceLength);
+
+                        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 +501,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 = CreateEmptyTextLine(0);
 
-                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);