Browse Source

Allow combining TextTrimming and TextWrapping

Benedikt Schroeder 5 years ago
parent
commit
e87697f901

+ 1 - 14
src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs

@@ -4,14 +4,12 @@
     {
         private TextAlignment _textAlignment;
         private TextWrapping _textWrapping;
-        private TextTrimming _textTrimming;
         private double _lineHeight;
 
         public GenericTextParagraphProperties(
             TextRunProperties defaultTextRunProperties,
             TextAlignment textAlignment = TextAlignment.Left,
-            TextWrapping textWrapping = TextWrapping.WrapWithOverflow,
-            TextTrimming textTrimming = TextTrimming.None,
+            TextWrapping textWrapping = TextWrapping.NoWrap,
             double lineHeight = 0)
         {
             DefaultTextRunProperties = defaultTextRunProperties;
@@ -20,8 +18,6 @@
 
             _textWrapping = textWrapping;
 
-            _textTrimming = textTrimming;
-
             _lineHeight = lineHeight;
         }
 
@@ -31,8 +27,6 @@
 
         public override TextWrapping TextWrapping => _textWrapping;
 
-        public override TextTrimming TextTrimming => _textTrimming;
-
         public override double LineHeight => _lineHeight;
 
         /// <summary>
@@ -50,13 +44,6 @@
         {
             _textWrapping = textWrapping;
         }
-        /// <summary>
-        /// Set text trimming
-        /// </summary>
-        internal void SetTextTrimming(TextTrimming textTrimming)
-        {
-            _textTrimming = textTrimming;
-        }
 
         /// <summary>
         /// Set line height

+ 1 - 2
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@@ -1,5 +1,4 @@
-using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Utilities;
+using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {

+ 23 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs

@@ -0,0 +1,23 @@
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Properties of text collapsing
+    /// </summary>
+    public abstract class TextCollapsingProperties
+    {
+        /// <summary>
+        /// Gets the width in which the collapsible range is constrained to
+        /// </summary>
+        public abstract double Width { get; }
+
+        /// <summary>
+        /// Gets the text run that is used as collapsing symbol
+        /// </summary>
+        public abstract TextRun Symbol { get; }
+
+        /// <summary>
+        /// Gets the style of collapsing
+        /// </summary>
+        public abstract TextCollapsingStyle Style { get; }
+    }
+}

+ 18 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs

@@ -0,0 +1,18 @@
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// Text collapsing style
+    /// </summary>
+    public enum TextCollapsingStyle
+    {
+        /// <summary>
+        /// Collapse trailing characters
+        /// </summary>
+        TrailingCharacter,
+
+        /// <summary>
+        /// Collapse trailing words
+        /// </summary>
+        TrailingWord,
+    }
+}

+ 55 - 166
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@@ -1,49 +1,41 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Platform;
-using Avalonia.Utilities;
 
 namespace Avalonia.Media.TextFormatting
 {
     internal class TextFormatterImpl : TextFormatter
     {
-        private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
-
         /// <inheritdoc cref="TextFormatter.FormatLine"/>
         public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
             TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null)
         {
-            var textTrimming = paragraphProperties.TextTrimming;
             var textWrapping = paragraphProperties.TextWrapping;
-            TextLine textLine = null;
 
             var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak);
 
             var textRange = GetTextRange(textRuns);
 
-            if (textTrimming != TextTrimming.None)
-            {
-                textLine = PerformTextTrimming(textRuns, textRange, paragraphWidth, paragraphProperties);
-            }
-            else
+            TextLine textLine;
+
+            switch (textWrapping)
             {
-                switch (textWrapping)
-                {
-                    case TextWrapping.NoWrap:
-                        {
-                            var textLineMetrics =
-                                TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
+                case TextWrapping.NoWrap:
+                    {
+                        var textLineMetrics =
+                            TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
 
-                            textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
-                            break;
-                        }
-                    case TextWrapping.WrapWithOverflow:
-                    case TextWrapping.Wrap:
-                        {
-                            textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties);
-                            break;
-                        }
-                }
+                        textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
+                        break;
+                    }
+                case TextWrapping.WrapWithOverflow:
+                case TextWrapping.Wrap:
+                    {
+                        textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties);
+                        break;
+                    }
+                default:
+                    throw new ArgumentOutOfRangeException();
             }
 
             return textLine;
@@ -174,87 +166,6 @@ namespace Avalonia.Media.TextFormatting
             return false;
         }
 
-        /// <summary>
-        /// Performs text trimming and returns a trimmed line.
-        /// </summary>
-        /// <param name="textRuns">The text runs to perform the trimming on.</param>
-        /// <param name="textRange">The text range that is covered by the text runs.</param>
-        /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
-        /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
-        /// such as TextWrapping, TextAlignment, or TextStyle.</param>
-        /// <returns></returns>
-        private static TextLine PerformTextTrimming(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange,
-            double paragraphWidth, TextParagraphProperties paragraphProperties)
-        {
-            var textTrimming = paragraphProperties.TextTrimming;
-            var availableWidth = paragraphWidth;
-            var currentWidth = 0.0;
-            var runIndex = 0;
-
-            while (runIndex < textRuns.Count)
-            {
-                var currentRun = textRuns[runIndex];
-
-                currentWidth += currentRun.GlyphRun.Bounds.Width;
-
-                if (currentWidth > availableWidth)
-                {
-                    var ellipsisRun = CreateEllipsisRun(currentRun.Properties);
-
-                    var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width);
-
-                    if (textTrimming == TextTrimming.WordEllipsis)
-                    {
-                        if (measuredLength < textRange.End)
-                        {
-                            var currentBreakPosition = 0;
-
-                            var lineBreaker = new LineBreakEnumerator(currentRun.Text);
-
-                            while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
-                            {
-                                var nextBreakPosition = lineBreaker.Current.PositionWrap;
-
-                                if (nextBreakPosition == 0)
-                                {
-                                    break;
-                                }
-
-                                if (nextBreakPosition > measuredLength)
-                                {
-                                    break;
-                                }
-
-                                currentBreakPosition = nextBreakPosition;
-                            }
-
-                            measuredLength = currentBreakPosition;
-                        }
-                    }
-
-                    var splitResult = SplitTextRuns(textRuns, measuredLength);
-
-                    var trimmedRuns = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
-
-                    trimmedRuns.AddRange(splitResult.First);
-
-                    trimmedRuns.Add(ellipsisRun);
-
-                    var textLineMetrics =
-                        TextLineMetrics.Create(trimmedRuns, textRange, paragraphWidth, paragraphProperties);
-
-                    return new TextLineImpl(trimmedRuns, textLineMetrics);
-                }
-
-                availableWidth -= currentRun.GlyphRun.Bounds.Width;
-
-                runIndex++;
-            }
-
-            return new TextLineImpl(textRuns,
-                TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties));
-        }
-
         /// <summary>
         /// Performs text wrapping returns a list of text lines.
         /// </summary>
@@ -269,7 +180,7 @@ namespace Avalonia.Media.TextFormatting
             var availableWidth = paragraphWidth;
             var currentWidth = 0.0;
             var runIndex = 0;
-            var length = 0;
+            var currentLength = 0;
 
             while (runIndex < textRuns.Count)
             {
@@ -279,58 +190,53 @@ namespace Avalonia.Media.TextFormatting
                 {
                     var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
 
+                    var breakFound = false;
+
+                    var currentBreakPosition = 0;
+
                     if (measuredLength < currentRun.Text.Length)
                     {
-                        if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
-                        {
-                            var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength));
+                        var lineBreaker = new LineBreakEnumerator(currentRun.Text);
 
-                            if (lineBreaker.MoveNext())
-                            {
-                                measuredLength += lineBreaker.Current.PositionWrap;
-                            }
-                            else
-                            {
-                                measuredLength = currentRun.Text.Length;
-                            }
-                        }
-                        else
+                        while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
                         {
-                            var currentBreakPosition = -1;
+                            var nextBreakPosition = lineBreaker.Current.PositionWrap;
 
-                            var lineBreaker = new LineBreakEnumerator(currentRun.Text);
-
-                            while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+                            if (nextBreakPosition == 0 || nextBreakPosition > measuredLength)
                             {
-                                var nextBreakPosition = lineBreaker.Current.PositionWrap;
+                                break;
+                            }
 
-                                if (nextBreakPosition == 0)
-                                {
-                                    break;
-                                }
+                            breakFound = lineBreaker.Current.Required ||
+                                         lineBreaker.Current.PositionWrap != currentRun.Text.Length;
 
-                                if (nextBreakPosition > measuredLength)
-                                {
-                                    break;
-                                }
+                            currentBreakPosition = nextBreakPosition;
+                        }
+                    }
 
-                                currentBreakPosition = nextBreakPosition;
-                            }
+                    if (breakFound)
+                    {
+                        measuredLength = currentBreakPosition;
+                    }
+                    else
+                    {
+                        if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
+                        {
+                            var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition));
 
-                            if (currentBreakPosition != -1)
+                            if (lineBreaker.MoveNext())
                             {
-                                measuredLength = currentBreakPosition;
+                                measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap;
                             }
-
                         }
                     }
 
-                    length += measuredLength;
+                    currentLength += measuredLength;
 
-                    var splitResult = SplitTextRuns(textRuns, length);
+                    var splitResult = SplitTextRuns(textRuns, currentLength);
 
                     var textLineMetrics = TextLineMetrics.Create(splitResult.First,
-                        new TextRange(textRange.Start, length), paragraphWidth, paragraphProperties);
+                        new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties);
 
                     var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ?
                         new TextLineBreak(splitResult.Second) :
@@ -341,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
 
                 currentWidth += currentRun.GlyphRun.Bounds.Width;
 
-                length += currentRun.GlyphRun.Characters.Length;
+                currentLength += currentRun.GlyphRun.Characters.Length;
 
                 runIndex++;
             }
@@ -356,7 +262,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textCharacters">The text run.</param>
         /// <param name="availableWidth">The available width.</param>
         /// <returns></returns>
-        private static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth)
+        internal static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth)
         {
             var glyphRun = textCharacters.GlyphRun;
 
@@ -391,10 +297,8 @@ namespace Avalonia.Media.TextFormatting
             }
             else
             {
-                for (var i = 0; i < glyphRun.GlyphAdvances.Length; i++)
+                foreach (var advance in glyphRun.GlyphAdvances)
                 {
-                    var advance = glyphRun.GlyphAdvances[i];
-
                     if (currentWidth + advance > availableWidth)
                     {
                         break;
@@ -423,21 +327,6 @@ namespace Avalonia.Media.TextFormatting
             return lastCluster - firstCluster;
         }
 
-        /// <summary>
-        /// Creates an ellipsis.
-        /// </summary>
-        /// <param name="properties">The text run properties.</param>
-        /// <returns></returns>
-        private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties)
-        {
-            var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
-
-            var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize,
-                properties.CultureInfo);
-
-            return new ShapedTextCharacters(glyphRun, properties);
-        }
-
         /// <summary>
         /// Gets the text range that is covered by the text runs.
         /// </summary>
@@ -470,7 +359,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="textRuns">The text run's.</param>
         /// <param name="length">The length to split at.</param>
         /// <returns>The split text runs.</returns>
-        private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length)
+        internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length)
         {
             var currentLength = 0;
 
@@ -543,7 +432,7 @@ namespace Avalonia.Media.TextFormatting
             return new SplitTextRunsResult(textRuns, null);
         }
 
-        private readonly struct SplitTextRunsResult
+        internal readonly struct SplitTextRunsResult
         {
             public SplitTextRunsResult(IReadOnlyList<ShapedTextCharacters> first, IReadOnlyList<ShapedTextCharacters> second)
             {

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

@@ -1,9 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Utilities;
-using Avalonia.Platform;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -17,6 +15,7 @@ namespace Avalonia.Media.TextFormatting
         private readonly ReadOnlySlice<char> _text;
         private readonly TextParagraphProperties _paragraphProperties;
         private readonly IReadOnlyList<ValueSpan<TextRunProperties>> _textStyleOverrides;
+        private readonly TextTrimming _textTrimming;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="TextLayout" /> class.
@@ -54,9 +53,11 @@ namespace Avalonia.Media.TextFormatting
                 new ReadOnlySlice<char>(text.AsMemory());
 
             _paragraphProperties =
-                CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming,
+                CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
                     textDecorations, lineHeight);
 
+            _textTrimming = textTrimming;
+
             _textStyleOverrides = textStyleOverrides;
 
             LineHeight = lineHeight;
@@ -143,18 +144,16 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="foreground">The foreground.</param>
         /// <param name="textAlignment">The text alignment.</param>
         /// <param name="textWrapping">The text wrapping.</param>
-        /// <param name="textTrimming">The text trimming.</param>
         /// <param name="textDecorations">The text decorations.</param>
         /// <param name="lineHeight">The height of each line of text.</param>
         /// <returns></returns>
         private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
-            IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming,
+            IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping,
             TextDecorationCollection textDecorations, double lineHeight)
         {
             var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
 
-            return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming,
-                lineHeight);
+            return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, lineHeight);
         }
 
         /// <summary>
@@ -214,25 +213,44 @@ namespace Avalonia.Media.TextFormatting
                 var textSource = new FormattedTextSource(_text,
                     _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
 
-                TextLineBreak previousLineBreak = null;
+                TextLine previousLine = null;
 
-                while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines))
+                while (currentPosition < _text.Length)
                 {
                     var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
-                        _paragraphProperties, previousLineBreak);
+                        _paragraphProperties, previousLine?.LineBreak);
 
-                    previousLineBreak = textLine.LineBreak;
+                    currentPosition += textLine.TextRange.Length;
 
-                    textLines.Add(textLine);
+                    if (textLines.Count > 0)
+                    {
+                        if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
+                            height + textLine.LineMetrics.Size.Height > MaxHeight)
+                        {
+                            if (previousLine?.LineBreak != null && _textTrimming != TextTrimming.None)
+                            {
+                                var collapsedLine =
+                                    previousLine.Collapse(GetCollapsingProperties(MaxWidth));
 
-                    UpdateBounds(textLine, ref width, ref height);
+                                textLines[textLines.Count - 1] = collapsedLine;
+                            }
+
+                            break;
+                        }
+                    }
 
-                    if (!double.IsPositiveInfinity(MaxHeight) && height > MaxHeight)
+                    var hasOverflowed = textLine.LineMetrics.HasOverflowed;
+
+                    if (hasOverflowed && _textTrimming != TextTrimming.None)
                     {
-                        break;
+                        textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
                     }
 
-                    currentPosition += textLine.TextRange.Length;
+                    textLines.Add(textLine);
+
+                    UpdateBounds(textLine, ref width, ref height);
+
+                    previousLine = textLine;
 
                     if (currentPosition != _text.Length || textLine.LineBreak == null)
                     {
@@ -250,6 +268,18 @@ namespace Avalonia.Media.TextFormatting
             }
         }
 
+        private TextCollapsingProperties GetCollapsingProperties(double width)
+        {
+            return _textTrimming switch
+            {
+                TextTrimming.CharacterEllipsis => new TextTrailingCharacterEllipsis(width,
+                    _paragraphProperties.DefaultTextRunProperties),
+                TextTrimming.WordEllipsis => new TextTrailingWordEllipsis(width,
+                    _paragraphProperties.DefaultTextRunProperties),
+                _ => throw new ArgumentOutOfRangeException(),
+            };
+        }
+
         private readonly struct FormattedTextSource : ITextSource
         {
             private readonly ReadOnlySlice<char> _text;

+ 11 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@@ -39,6 +39,11 @@ namespace Avalonia.Media.TextFormatting
         /// </returns>
         public abstract TextLineBreak LineBreak { get; }
 
+        /// <summary>
+        /// Client to get a boolean value indicates whether a line has been collapsed
+        /// </summary>
+        public abstract bool HasCollapsed { get; }
+
         /// <summary>
         /// Draws the <see cref="TextLine"/> at the given origin.
         /// </summary>
@@ -46,6 +51,12 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="origin">The origin.</param>
         public abstract void Draw(DrawingContext drawingContext, Point origin);
 
+        /// <summary>
+        /// Client to collapse the line and get a collapsed line that fits for display
+        /// </summary>
+        /// <param name="collapsingPropertiesList">a list of collapsing properties</param>
+        public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList);
+
         /// <summary>
         /// Client to get the character hit corresponding to the specified 
         /// distance from the beginning of the line.

+ 135 - 1
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@@ -1,4 +1,6 @@
 using System.Collections.Generic;
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Platform;
 
 namespace Avalonia.Media.TextFormatting
 {
@@ -7,11 +9,12 @@ namespace Avalonia.Media.TextFormatting
         private readonly IReadOnlyList<ShapedTextCharacters> _textRuns;
 
         public TextLineImpl(IReadOnlyList<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics,
-            TextLineBreak lineBreak = null)
+            TextLineBreak lineBreak = null, bool hasCollapsed = false)
         {
             _textRuns = textRuns;
             LineMetrics = lineMetrics;
             LineBreak = lineBreak;
+            HasCollapsed = hasCollapsed;
         }
 
         /// <inheritdoc/>
@@ -26,6 +29,9 @@ namespace Avalonia.Media.TextFormatting
         /// <inheritdoc/>
         public override TextLineBreak LineBreak { get; }
 
+        /// <inheritdoc/>
+        public override bool HasCollapsed { get; }
+
         /// <inheritdoc/>
         public override void Draw(DrawingContext drawingContext, Point origin)
         {
@@ -41,6 +47,98 @@ namespace Avalonia.Media.TextFormatting
             }
         }
 
+        public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
+        {
+            if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0)
+            {
+                return this;
+            }
+
+            var collapsingProperties = collapsingPropertiesList[0];
+            var runIndex = 0;
+            var currentWidth = 0.0;
+            var textRange = TextRange;
+            var collapsedLength = 0;
+            TextLineMetrics textLineMetrics;
+
+            var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol);
+
+            var availableWidth = collapsingProperties.Width - shapedSymbol.Bounds.Width;
+
+            while (runIndex < _textRuns.Count)
+            {
+                var currentRun = _textRuns[runIndex];
+
+                currentWidth += currentRun.GlyphRun.Bounds.Width;
+
+                if (currentWidth > availableWidth)
+                {
+                    var measuredLength = TextFormatterImpl.MeasureText(currentRun, availableWidth);
+
+                    var currentBreakPosition = 0;
+
+                    if (measuredLength < textRange.End)
+                    {
+                        var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+
+                        while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+                        {
+                            var nextBreakPosition = lineBreaker.Current.PositionWrap;
+
+                            if (nextBreakPosition == 0)
+                            {
+                                break;
+                            }
+
+                            if (nextBreakPosition > measuredLength)
+                            {
+                                break;
+                            }
+
+                            currentBreakPosition = nextBreakPosition;
+                        }
+                    }
+
+                    if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord)
+                    {
+                        measuredLength = currentBreakPosition;
+                    }
+
+                    collapsedLength += measuredLength;
+
+                    var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength);
+
+                    var shapedTextCharacters = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
+
+                    shapedTextCharacters.AddRange(splitResult.First);
+
+                    shapedTextCharacters.Add(shapedSymbol);
+
+                    textRange = new TextRange(textRange.Start, collapsedLength);
+
+                    var shapedWidth = GetShapedWidth(shapedTextCharacters);
+
+                    textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
+                        LineMetrics.TextBaseline, textRange, false);
+
+                    return new TextLineImpl(shapedTextCharacters, textLineMetrics, LineBreak, true);
+                }
+
+                availableWidth -= currentRun.GlyphRun.Bounds.Width;
+
+                collapsedLength += currentRun.GlyphRun.Characters.Length;
+
+                runIndex++;
+            }
+
+            textLineMetrics =
+                new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Bounds.Width),
+                    LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed);
+
+            return new TextLineImpl(new List<ShapedTextCharacters>(_textRuns) { shapedSymbol }, textLineMetrics, null,
+                true);
+        }
+
         /// <inheritdoc/>
         public override CharacterHit GetCharacterHitFromDistance(double distance)
         {
@@ -230,5 +328,41 @@ namespace Avalonia.Media.TextFormatting
 
             return runIndex;
         }
+
+        /// <summary>
+        /// Creates a shaped symbol.
+        /// </summary>
+        /// <param name="textRun">The symbol run to shape.</param>
+        /// <returns>
+        /// The shaped symbol.
+        /// </returns>
+        internal static ShapedTextCharacters CreateShapedSymbol(TextRun textRun)
+        {
+            var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
+
+            var glyphRun = formatterImpl.ShapeText(textRun.Text, textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize,
+                textRun.Properties.CultureInfo);
+
+            return new ShapedTextCharacters(glyphRun, textRun.Properties);
+        }
+        
+        /// <summary>
+        /// Gets the shaped width of specified shaped text characters.
+        /// </summary>
+        /// <param name="shapedTextCharacters">The shaped text characters.</param>
+        /// <returns>
+        /// The shaped width.
+        /// </returns>
+        private static double GetShapedWidth(IReadOnlyList<ShapedTextCharacters> shapedTextCharacters)
+        {
+            var shapedWidth = 0.0;
+
+            for (var i = 0; i < shapedTextCharacters.Count; i++)
+            {
+                shapedWidth += shapedTextCharacters[i].Bounds.Width;
+            }
+
+            return shapedWidth;
+        }
     }
 }

+ 9 - 2
src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs

@@ -9,11 +9,12 @@ namespace Avalonia.Media.TextFormatting
     /// </summary>
     public readonly struct TextLineMetrics
     {
-        public TextLineMetrics(Size size, double textBaseline, TextRange textRange)
+        public TextLineMetrics(Size size, double textBaseline, TextRange textRange, bool hasOverflowed)
         {
             Size = size;
             TextBaseline = textBaseline;
             TextRange = textRange;
+            HasOverflowed = hasOverflowed;
         }
 
         /// <summary>
@@ -37,6 +38,12 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         public double TextBaseline { get; }
 
+        /// <summary>
+        /// Gets a boolean value that indicates whether content of the line overflows 
+        /// the specified paragraph width.
+        /// </summary>
+        public bool HasOverflowed { get; }
+
         /// <summary>
         /// Creates the text line metrics.
         /// </summary>
@@ -83,7 +90,7 @@ namespace Avalonia.Media.TextFormatting
                     descent - ascent + lineGap :
                     paragraphProperties.LineHeight);
 
-            return new TextLineMetrics(size, -ascent, textRange);
+            return new TextLineMetrics(size, -ascent, textRange, size.Width > paragraphWidth);
         }
     }
 }

+ 0 - 5
src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs

@@ -26,11 +26,6 @@
         /// </summary>
         public abstract TextWrapping TextWrapping { get; }
 
-        /// <summary>
-        /// Gets the text trimming.
-        /// </summary>
-        public abstract TextTrimming TextTrimming { get; }
-
         /// <summary>
         /// Paragraph's line height
         /// </summary>

+ 33 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@@ -0,0 +1,33 @@
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// a collapsing properties to collapse whole line toward the end
+    /// at character granularity and with ellipsis being the collapsing symbol
+    /// </summary>
+    public class TextTrailingCharacterEllipsis : TextCollapsingProperties
+    {
+        private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
+
+        /// <summary>
+        /// Construct a text trailing character ellipsis collapsing properties
+        /// </summary>
+        /// <param name="width">width in which collapsing is constrained to</param>
+        /// <param name="textRunProperties">text run properties of ellispis symbol</param>
+        public TextTrailingCharacterEllipsis(double width, TextRunProperties textRunProperties)
+        {
+            Width = width;
+            Symbol = new TextCharacters(s_ellipsis, textRunProperties);
+        }
+
+        /// <inheritdoc/>
+        public sealed override double Width { get; }
+
+        /// <inheritdoc/>
+        public sealed override TextRun Symbol { get; }
+
+        /// <inheritdoc/>
+        public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter;
+    }
+}

+ 37 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs

@@ -0,0 +1,37 @@
+using Avalonia.Utilities;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>
+    /// a collapsing properties to collapse whole line toward the end
+    /// at word granularity and with ellipsis being the collapsing symbol
+    /// </summary>
+    public class TextTrailingWordEllipsis : TextCollapsingProperties
+    {
+        private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
+
+        /// <summary>
+        /// Construct a text trailing word ellipsis collapsing properties
+        /// </summary>
+        /// <param name="width">width in which collapsing is constrained to</param>
+        /// <param name="textRunProperties">text run properties of ellispis symbol</param>
+        public TextTrailingWordEllipsis(
+            double width,
+            TextRunProperties textRunProperties
+        )
+        {
+            Width = width;
+            Symbol = new TextCharacters(s_ellipsis, textRunProperties);
+        }
+
+
+        /// <inheritdoc/>
+        public sealed override double Width { get; }
+
+        /// <inheritdoc/>
+        public sealed override TextRun Symbol { get; }
+
+        /// <inheritdoc/>
+        public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord;
+    }
+}

+ 0 - 1
src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@@ -109,7 +109,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
                 {
                     case PairBreakType.DI: // Direct break
                         shouldBreak = true;
-                        _lastPos = _pos;
                         break;
 
                     case PairBreakType.IN: // possible indirect break

+ 8 - 8
src/Avalonia.Visuals/Media/TextWrapping.cs

@@ -5,13 +5,6 @@ namespace Avalonia.Media
     /// </summary>
     public enum TextWrapping
     {
-        /// <summary>
-        /// Line-breaking occurs if the line overflows the available block width.
-        /// However, a line may overflow the block width if the line breaking algorithm
-        /// cannot determine a break opportunity, as in the case of a very long word.
-        /// </summary>
-        WrapWithOverflow,
-
         /// <summary>
         /// Text should not wrap.
         /// </summary>
@@ -20,6 +13,13 @@ namespace Avalonia.Media
         /// <summary>
         /// Text can wrap.
         /// </summary>
-        Wrap
+        Wrap, 
+        
+        /// <summary>
+        /// Line-breaking occurs if the line overflows the available block width.
+        /// However, a line may overflow the block width if the line breaking algorithm
+        /// cannot determine a break opportunity, as in the case of a very long word.
+        /// </summary>
+        WrapWithOverflow
     }
 }

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

@@ -490,10 +490,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             }
         }
 
-        [InlineData("0123456789\r0123456789", 2)]
-        [InlineData("0123456789", 1)]
+        [InlineData("0123456789\r0123456789")]
+        [InlineData("0123456789")]
         [Theory]
-        public void Should_Include_Last_Line_When_Constraint_Is_Surpassed(string text, int numberOfLines)
+        public void Should_Include_First_Line_When_Constraint_Is_Surpassed(string text)
         {
             using (Start())
             {
@@ -508,11 +508,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                     Typeface.Default,
                     12,
                     Brushes.Black.ToImmutable(),
-                    maxHeight: lineHeight * numberOfLines - lineHeight * 0.5);
+                    maxHeight: lineHeight - lineHeight * 0.5);
 
-                Assert.Equal(numberOfLines, layout.TextLines.Count);
+                Assert.Equal(1, layout.TextLines.Count);
 
-                Assert.Equal(numberOfLines * lineHeight, layout.Size.Height);
+                Assert.Equal(lineHeight, layout.Size.Height);
             }
         }
 

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

@@ -162,6 +162,64 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             }
         }
 
+        [InlineData("01234 01234", 8, TextCollapsingStyle.TrailingCharacter, "01234 0\u2026")]
+        [InlineData("01234 01234", 8, TextCollapsingStyle.TrailingWord, "01234 \u2026")]
+        [Theory]
+        public void Should_Collapse_Line(string text, int numberOfCharacters, TextCollapsingStyle style, string expected)
+        {
+            using (Start())
+            {
+                var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+                var textSource = new SingleBufferTextSource(text, defaultProperties);
+
+                var formatter = new TextFormatterImpl();
+
+                var textLine =
+                    formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+                        new GenericTextParagraphProperties(defaultProperties));
+
+                Assert.False(textLine.HasCollapsed);
+
+                var glyphTypeface = Typeface.Default.GlyphTypeface;
+
+                var scale = defaultProperties.FontRenderingEmSize / glyphTypeface.DesignEmHeight;
+
+                var width = 1.0;
+
+                for (var i = 0; i < numberOfCharacters; i++)
+                {
+                    var glyph = glyphTypeface.GetGlyph(text[i]);
+
+                    width += glyphTypeface.GetGlyphAdvance(glyph) * scale;
+                }
+
+                TextCollapsingProperties collapsingProperties;
+
+                if (style == TextCollapsingStyle.TrailingCharacter)
+                {
+                    collapsingProperties = new TextTrailingCharacterEllipsis(width, defaultProperties);
+                }
+                else
+                {
+                    collapsingProperties = new TextTrailingWordEllipsis(width, defaultProperties);
+                }
+
+                var collapsedLine = textLine.Collapse(collapsingProperties);
+
+                Assert.True(collapsedLine.HasCollapsed);
+
+                var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text).ToArray();
+
+                Assert.Equal(expected.Length, trimmedText.Length);
+
+                for (var i = 0; i < expected.Length; i++)
+                {
+                    Assert.Equal(expected[i], trimmedText[i]);
+                }
+            }
+        }
+
         private static IDisposable Start()
         {
             var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface