Browse Source

Text layout: ensure RentedList are returned in case of exceptions

Julien Lebosquain 2 years ago
parent
commit
a24e48f105

+ 7 - 4
src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs

@@ -93,16 +93,19 @@ namespace Avalonia.Media.TextFormatting
             [Conditional("DEBUG")]
             public void VerifyAllReturned()
             {
-                if (_pendingReturnCount > 0)
+                var pendingReturnCount = _pendingReturnCount;
+                _pendingReturnCount = 0;
+
+                if (pendingReturnCount > 0)
                 {
                     throw new InvalidOperationException(
-                        $"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!");
+                        $"{pendingReturnCount} RentedList<{typeof(T).Name}> haven't been returned to the pool!");
                 }
 
-                if (_pendingReturnCount < 0)
+                if (pendingReturnCount < 0)
                 {
                     throw new InvalidOperationException(
-                        $"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!");
+                        $"{-pendingReturnCount} RentedList<{typeof(T).Name}> extra lists have been returned to the pool!");
                 }
             }
         }

+ 12 - 8
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -113,14 +113,18 @@ namespace Avalonia.Media.TextFormatting
 
             var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
 
-            var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
-            preSplitRuns.CopyTo(collapsedRuns);
-            collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
-
-            objectPool.TextRunLists.Return(ref preSplitRuns);
-            objectPool.TextRunLists.Return(ref postSplitRuns);
-
-            return collapsedRuns;
+            try
+            {
+                var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
+                preSplitRuns.CopyTo(collapsedRuns);
+                collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
+                return collapsedRuns;
+            }
+            finally
+            {
+                objectPool.TextRunLists.Return(ref preSplitRuns);
+                objectPool.TextRunLists.Return(ref postSplitRuns);
+            }
         }
     }
 }

+ 85 - 69
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -32,58 +32,64 @@ namespace Avalonia.Media.TextFormatting
             var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
                 out var textEndOfLine, out var textSourceLength);
 
-            RentedList<TextRun>? shapedTextRuns;
+            RentedList<TextRun>? shapedTextRuns = null;
 
-            if (previousLineBreak?.RemainingRuns is { } remainingRuns)
+            try
             {
-                resolvedFlowDirection = previousLineBreak.FlowDirection;
-                textRuns = remainingRuns;
-                nextLineBreak = previousLineBreak;
-                shapedTextRuns = null;
-            }
-            else
-            {
-                shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection);
-                textRuns = shapedTextRuns;
-
-                if (nextLineBreak == null && textEndOfLine != null)
+                if (previousLineBreak?.RemainingRuns is { } remainingRuns)
                 {
-                    nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
+                    resolvedFlowDirection = previousLineBreak.FlowDirection;
+                    textRuns = remainingRuns;
+                    nextLineBreak = previousLineBreak;
+                    shapedTextRuns = null;
                 }
-            }
+                else
+                {
+                    shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
+                        out resolvedFlowDirection);
+                    textRuns = shapedTextRuns;
 
-            TextLineImpl textLine;
+                    if (nextLineBreak == null && textEndOfLine != null)
+                    {
+                        nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
+                    }
+                }
 
-            switch (textWrapping)
-            {
-                case TextWrapping.NoWrap:
+                TextLineImpl textLine;
+
+                switch (textWrapping)
                 {
-                    // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
-                    // which already uses an array: ToArray() won't ever be called in this case
-                    var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
+                    case TextWrapping.NoWrap:
+                    {
+                        // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
+                        // which already uses an array: ToArray() won't ever be called in this case
+                        var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
 
-                    textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
-                        paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
+                        textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
+                            paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
 
-                    textLine.FinalizeLine();
+                        textLine.FinalizeLine();
 
-                    break;
-                }
-                case TextWrapping.WrapWithOverflow:
-                case TextWrapping.Wrap:
-                {
-                    textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
-                        paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
-                    break;
+                        break;
+                    }
+                    case TextWrapping.WrapWithOverflow:
+                    case TextWrapping.Wrap:
+                    {
+                        textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
+                            paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
+                        break;
+                    }
+                    default:
+                        throw new ArgumentOutOfRangeException(nameof(textWrapping));
                 }
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(textWrapping));
-            }
-
-            objectPool.TextRunLists.Return(ref shapedTextRuns);
-            objectPool.TextRunLists.Return(ref fetchedRuns);
 
-            return textLine;
+                return textLine;
+            }
+            finally
+            {
+                objectPool.TextRunLists.Return(ref shapedTextRuns);
+                objectPool.TextRunLists.Return(ref fetchedRuns);
+            }
         }
 
         /// <summary>
@@ -224,23 +230,26 @@ namespace Avalonia.Media.TextFormatting
                 (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
 
             var processedRuns = objectPool.TextRunLists.Rent();
+            var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
 
-            CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
+            try
+            {
+                CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
 
-            bidiData.Reset();
-            bidiAlgorithm.Reset();
+                bidiData.Reset();
+                bidiAlgorithm.Reset();
 
-            var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
-            var textShaper = TextShaper.Current;
 
-            for (var index = 0; index < processedRuns.Count; index++)
-            {
-                var currentRun = processedRuns[index];
+                var textShaper = TextShaper.Current;
 
-                switch (currentRun)
+                for (var index = 0; index < processedRuns.Count; index++)
                 {
-                    case UnshapedTextRun shapeableRun:
+                    var currentRun = processedRuns[index];
+
+                    switch (currentRun)
                     {
+                        case UnshapedTextRun shapeableRun:
+                        {
                             groupedRuns.Clear();
                             groupedRuns.Add(shapeableRun);
 
@@ -277,17 +286,20 @@ namespace Avalonia.Media.TextFormatting
 
                             break;
                         }
-                    default:
+                        default:
                         {
                             shapedRuns.Add(currentRun);
 
                             break;
                         }
+                    }
                 }
             }
-
-            objectPool.TextRunLists.Return(ref processedRuns);
-            objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
+            finally
+            {
+                objectPool.TextRunLists.Return(ref processedRuns);
+                objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
+            }
 
             return shapedRuns;
         }
@@ -805,25 +817,29 @@ namespace Avalonia.Media.TextFormatting
 
             var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
 
-            var textLineBreak = postSplitRuns?.Count > 0 ?
-                new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
-                null;
-
-            if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
+            try
             {
-                textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
-            }
+                var textLineBreak = postSplitRuns?.Count > 0 ?
+                    new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
+                    null;
 
-            var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
-                paragraphWidth, paragraphProperties, resolvedFlowDirection,
-                textLineBreak);
-
-            textLine.FinalizeLine();
+                if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
+                {
+                    textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
+                }
 
-            objectPool.TextRunLists.Return(ref preSplitRuns);
-            objectPool.TextRunLists.Return(ref postSplitRuns);
+                var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
+                    paragraphWidth, paragraphProperties, resolvedFlowDirection,
+                    textLineBreak);
 
-            return textLine;
+                textLine.FinalizeLine();
+                return textLine;
+            }
+            finally
+            {
+                objectPool.TextRunLists.Return(ref preSplitRuns);
+                objectPool.TextRunLists.Return(ref postSplitRuns);
+            }
         }
 
         private struct TextRunEnumerator

+ 86 - 81
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -441,128 +441,133 @@ namespace Avalonia.Media.TextFormatting
 
             var textLines = objectPool.TextLines.Rent();
 
-            double left = double.PositiveInfinity, width = 0.0, height = 0.0;
-
-            _textSourceLength = 0;
+            try
+            {
+                double left = double.PositiveInfinity, width = 0.0, height = 0.0;
 
-            TextLine? previousLine = null;
+                _textSourceLength = 0;
 
-            var textFormatter = TextFormatter.Current;
+                TextLine? previousLine = null;
 
-            while (true)
-            {
-                var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties,
-                    previousLine?.TextLineBreak);
+                var textFormatter = TextFormatter.Current;
 
-                if (textLine.Length == 0)
+                while (true)
                 {
-                    if (previousLine != null && previousLine.NewLineLength > 0)
+                    var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
+                        _paragraphProperties, previousLine?.TextLineBreak);
+
+                    if (textLine.Length == 0)
                     {
-                        var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
-                            _paragraphProperties, fontManager);
+                        if (previousLine != null && previousLine.NewLineLength > 0)
+                        {
+                            var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
+                                _paragraphProperties, fontManager);
 
-                        textLines.Add(emptyTextLine);
+                            textLines.Add(emptyTextLine);
 
-                        UpdateBounds(emptyTextLine, ref left, ref width, ref height);
-                    }
+                            UpdateBounds(emptyTextLine, ref left, ref width, ref height);
+                        }
 
-                    break;
-                }
+                        break;
+                    }
 
-                _textSourceLength += textLine.Length;
+                    _textSourceLength += textLine.Length;
 
-                //Fulfill max height constraint
-                if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
-                {
-                    if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
+                    //Fulfill max height constraint
+                    if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight)
+                        && height + textLine.Height > MaxHeight)
                     {
-                        var collapsedLine =
-                            previousLine.Collapse(GetCollapsingProperties(MaxWidth));
+                        if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
+                        {
+                            var collapsedLine =
+                                previousLine.Collapse(GetCollapsingProperties(MaxWidth));
 
-                        textLines[textLines.Count - 1] = collapsedLine;
-                    }
+                            textLines[textLines.Count - 1] = collapsedLine;
+                        }
 
-                    break;
-                }
+                        break;
+                    }
 
-                var hasOverflowed = textLine.HasOverflowed;
+                    var hasOverflowed = textLine.HasOverflowed;
 
-                if (hasOverflowed && _textTrimming != TextTrimming.None)
-                {
-                    textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
-                }
+                    if (hasOverflowed && _textTrimming != TextTrimming.None)
+                    {
+                        textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
+                    }
 
-                textLines.Add(textLine);
+                    textLines.Add(textLine);
 
-                UpdateBounds(textLine, ref left, ref width, ref height);
+                    UpdateBounds(textLine, ref left, ref width, ref height);
 
-                previousLine = textLine;
+                    previousLine = textLine;
 
-                //Fulfill max lines constraint
-                if (MaxLines > 0 && textLines.Count >= MaxLines)
-                {
-                    if(textLine.TextLineBreak?.RemainingRuns is not null)
+                    //Fulfill max lines constraint
+                    if (MaxLines > 0 && textLines.Count >= MaxLines)
                     {
-                        textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
+                        if (textLine.TextLineBreak?.RemainingRuns is not null)
+                        {
+                            textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
+                        }
+
+                        break;
                     }
 
-                    break;
+                    if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+                    {
+                        break;
+                    }
                 }
 
-                if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
+                //Make sure the TextLayout always contains at least on empty line
+                if (textLines.Count == 0)
                 {
-                    break;
-                }
-            }
+                    var textLine =
+                        TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
 
-            //Make sure the TextLayout always contains at least on empty line
-            if (textLines.Count == 0)
-            {
-                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
-
-                textLines.Add(textLine);
-
-                UpdateBounds(textLine, ref left, ref width, ref height);
-            }
+                    textLines.Add(textLine);
 
-            Bounds = new Rect(left, 0, width, height);
+                    UpdateBounds(textLine, ref left, ref width, ref height);
+                }
 
-            if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
-            {
-                var whitespaceWidth = 0d;
+                Bounds = new Rect(left, 0, width, height);
 
-                for (var i = 0; i < textLines.Count; i++)
+                if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
                 {
-                    var line = textLines[i];
-                    var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
+                    var whitespaceWidth = 0d;
 
-                    if (lineWhitespaceWidth > whitespaceWidth)
+                    for (var i = 0; i < textLines.Count; i++)
                     {
-                        whitespaceWidth = lineWhitespaceWidth;
-                    }
-                }
+                        var line = textLines[i];
+                        var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
 
-                var justificationWidth = width - whitespaceWidth;
+                        if (lineWhitespaceWidth > whitespaceWidth)
+                        {
+                            whitespaceWidth = lineWhitespaceWidth;
+                        }
+                    }
 
-                if (justificationWidth > 0)
-                {
-                    var justificationProperties = new InterWordJustification(justificationWidth);
+                    var justificationWidth = width - whitespaceWidth;
 
-                    for (var i = 0; i < textLines.Count - 1; i++)
+                    if (justificationWidth > 0)
                     {
-                        var line = textLines[i];
+                        var justificationProperties = new InterWordJustification(justificationWidth);
 
-                        line.Justify(justificationProperties);
+                        for (var i = 0; i < textLines.Count - 1; i++)
+                        {
+                            var line = textLines[i];
+
+                            line.Justify(justificationProperties);
+                        }
                     }
                 }
-            }
 
-            var result = textLines.ToArray();
-
-            objectPool.TextLines.Return(ref textLines);
-            objectPool.VerifyAllReturned();
-
-            return result;
+                return textLines.ToArray();
+            }
+            finally
+            {
+                objectPool.TextLines.Return(ref textLines);
+                objectPool.VerifyAllReturned();
+            }
         }
 
         /// <summary>

+ 4 - 8
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -86,7 +86,6 @@ namespace Avalonia.Media.TextFormatting
 
                                     RentedList<TextRun>? rentedPreSplitRuns = null;
                                     RentedList<TextRun>? rentedPostSplitRuns = null;
-                                    TextRun[]? results;
 
                                     try
                                     {
@@ -113,9 +112,7 @@ namespace Avalonia.Media.TextFormatting
 
                                         if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
                                         {
-                                            results = collapsedRuns.ToArray();
-                                            objectPool.TextRunLists.Return(ref collapsedRuns);
-                                            return results;
+                                            return collapsedRuns.ToArray();
                                         }
 
                                         var availableSuffixWidth = availableWidth;
@@ -157,16 +154,15 @@ namespace Avalonia.Media.TextFormatting
                                                 }
                                             }
                                         }
+
+                                        return collapsedRuns.ToArray();
                                     }
                                     finally
                                     {
                                         objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
                                         objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
+                                        objectPool.TextRunLists.Return(ref collapsedRuns);
                                     }
-
-                                    results = collapsedRuns.ToArray();
-                                    objectPool.TextRunLists.Return(ref collapsedRuns);
-                                    return results;
                                 }
 
                                 return new TextRun[] { shapedSymbol };