diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 485df1ef1b..39a8ff870e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -273,6 +273,11 @@ namespace Avalonia.Media.TextFormatting } } + /// + /// Get minimum width of all text lines that can be layouted horizontally without trimming or wrapping. + /// + internal double MinTextWidth => _metrics.MinTextWidth; + /// /// Draws the text layout. /// @@ -544,21 +549,13 @@ namespace Avalonia.Media.TextFormatting { var objectPool = FormattingObjectPool.Instance; - var lineStartOfLongestLine = double.MaxValue; - var origin = new Point(); var first = true; - double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom; - - accBlackBoxLeft = accBlackBoxTop = double.MaxValue; - accBlackBoxRight = accBlackBoxBottom = double.MinValue; - if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); - UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, - ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); + UpdateMetrics(textLine, ref first); return new TextLine[] { textLine }; } @@ -576,7 +573,7 @@ namespace Avalonia.Media.TextFormatting while (true) { var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, - _paragraphProperties, previousLine?.TextLineBreak); + _paragraphProperties, previousLine?.TextLineBreak) as TextLineImpl; if (textLine is null) { @@ -587,8 +584,7 @@ namespace Avalonia.Media.TextFormatting textLines.Add(emptyTextLine); - UpdateMetrics(emptyTextLine, ref lineStartOfLongestLine, ref origin, ref first, - ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); + UpdateMetrics(emptyTextLine, ref first); } break; @@ -615,13 +611,12 @@ namespace Avalonia.Media.TextFormatting if (hasOverflowed && _textTrimming != TextTrimming.None) { - textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); + textLine = (TextLineImpl)textLine.Collapse(GetCollapsingProperties(MaxWidth)); } textLines.Add(textLine); - UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, - ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); + UpdateMetrics(textLine, ref first); previousLine = textLine; @@ -648,8 +643,7 @@ namespace Avalonia.Media.TextFormatting textLines.Add(textLine); - UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, - ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom); + UpdateMetrics(textLine, ref first); } if (_paragraphProperties.TextAlignment == TextAlignment.Justify) @@ -683,44 +677,27 @@ namespace Avalonia.Media.TextFormatting } } - private void UpdateMetrics( - TextLine currentLine, - ref double lineStartOfLongestLine, - ref Point origin, - ref bool first, - ref double accBlackBoxLeft, - ref double accBlackBoxTop, - ref double accBlackBoxRight, - ref double accBlackBoxBottom) + private void UpdateMetrics(TextLineImpl currentLine, ref bool first) { - var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading; - var blackBoxRight = origin.X + currentLine.Start + currentLine.Width - currentLine.OverhangTrailing; - var blackBoxBottom = origin.Y + currentLine.Height + currentLine.OverhangAfter; - var blackBoxTop = blackBoxBottom - currentLine.Extent; + _metrics.InkBounds = _metrics.InkBounds.Union(new Rect(new Point(0, _metrics.Bounds.Bottom) + currentLine.InkBounds.Position, currentLine.InkBounds.Size)); + _metrics.Bounds = _metrics.Bounds.Union(new Rect(new Point(0, _metrics.Bounds.Bottom) + currentLine.Bounds.Position, currentLine.Bounds.Size)); - accBlackBoxLeft = Math.Min(accBlackBoxLeft, blackBoxLeft); - accBlackBoxRight = Math.Max(accBlackBoxRight, blackBoxRight); - accBlackBoxBottom = Math.Max(accBlackBoxBottom, blackBoxBottom); - accBlackBoxTop = Math.Min(accBlackBoxTop, blackBoxTop); + _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.Bounds.Width); + _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.InkBounds.Width); - _metrics.OverhangAfter = currentLine.OverhangAfter; - - _metrics.Height += currentLine.Height; - _metrics.Width = Math.Max(_metrics.Width, currentLine.Width); - _metrics.WidthIncludingTrailingWhitespace = Math.Max(_metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace); - lineStartOfLongestLine = Math.Min(lineStartOfLongestLine, currentLine.Start); - - _metrics.Extent = accBlackBoxBottom - accBlackBoxTop; - _metrics.OverhangLeading = accBlackBoxLeft - lineStartOfLongestLine; - _metrics.OverhangTrailing = _metrics.Width - (accBlackBoxRight - lineStartOfLongestLine); + _metrics.Height = _metrics.Bounds.Height; + _metrics.Width = _metrics.InkBounds.Width; + _metrics.WidthIncludingTrailingWhitespace = _metrics.Bounds.Width; + _metrics.Extent = _metrics.InkBounds.Height; + _metrics.OverhangLeading = Math.Max(0, _metrics.Bounds.Left - _metrics.InkBounds.Left); + _metrics.OverhangTrailing = Math.Max(0, _metrics.InkBounds.Right - _metrics.Bounds.Right); + _metrics.OverhangAfter = Math.Max(0, _metrics.InkBounds.Bottom - _metrics.Bounds.Bottom); if (first) { _metrics.Baseline = currentLine.Baseline; first = false; } - - origin = origin.WithY(origin.Y + currentLine.Height); } /// @@ -764,6 +741,11 @@ namespace Avalonia.Media.TextFormatting // horizontal bounding box metrics public double OverhangLeading; public double OverhangTrailing; + + public Rect Bounds; + public Rect InkBounds; + + public double MinTextWidth; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index c6da172604..c038df9468 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -18,6 +18,9 @@ namespace Avalonia.Media.TextFormatting private TextLineBreak? _textLineBreak; private readonly FlowDirection _resolvedFlowDirection; + private Rect _inkBounds; + private Rect _bounds; + public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) @@ -85,10 +88,20 @@ namespace Avalonia.Media.TextFormatting /// public override double WidthIncludingTrailingWhitespace => _textLineMetrics.WidthIncludingTrailingWhitespace; + /// + /// Get the logical text bounds. + /// + internal Rect Bounds => _bounds; + + /// + /// Get the bounding box that is covered with black pixels. + /// + internal Rect InkBounds => _inkBounds; + /// public override void Draw(DrawingContext drawingContext, Point lineOrigin) { - var (currentX, currentY) = lineOrigin + new Point(Start, 0); + var (currentX, currentY) = lineOrigin + new Point(Start, 0); foreach (var textRun in _textRuns) { @@ -1377,6 +1390,10 @@ namespace Avalonia.Media.TextFormatting var start = GetParagraphOffsetX(width, widthIncludingWhitespace); + _inkBounds = new Rect(bounds.Position + new Point(start, 0), bounds.Size); + + _bounds = new Rect(start, 0, widthIncludingWhitespace, height); + return new TextLineMetrics { HasOverflowed = hasOverflowed, diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index f6a0cdbdfd..a8324b2a5a 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -740,9 +740,7 @@ namespace Avalonia.Controls //This implicitly recreated the TextLayout with a new constraint if we previously reset it. var textLayout = TextLayout; - var width = textLayout.OverhangLeading + textLayout.WidthIncludingTrailingWhitespace + textLayout.OverhangTrailing; - - var size = LayoutHelper.RoundLayoutSizeUp(new Size(width, textLayout.Height).Inflate(padding), 1, 1); + var size = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height).Inflate(padding), 1, 1); return size; } diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 4a7281fae3..f1399d83bc 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using Avalonia.Controls.Documents; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -6,6 +6,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; +using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls.UnitTests { @@ -48,6 +49,29 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Should_Measure_MinTextWith() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var textBlock = new TextBlock + { + Text = "Hello שלום Really really really really long line", + HorizontalAlignment = HorizontalAlignment.Center, + TextAlignment = TextAlignment.DetectFromContent, + TextWrapping = TextWrapping.Wrap + }; + + textBlock.Measure(new Size(1920, 1080)); + + var textLayout = textBlock.TextLayout; + + var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height), 1, 1); + + Assert.Equal(textBlock.DesiredSize, constraint); + } + } + [Fact] public void Calling_Arrange_With_Different_Size_Should_Update_Constraint_And_TextLayout() {