From 56f6296066dde10afeaf7417868cdde17023aa95 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 10 Dec 2024 06:47:44 +0100 Subject: [PATCH] Calculate Bounds and InkBounds for TextLayout to be able to calculate the minmal required width. (#17721) --- .../Media/TextFormatting/TextLayout.cs | 74 +++++++------------ .../Media/TextFormatting/TextLineImpl.cs | 19 ++++- src/Avalonia.Controls/TextBlock.cs | 4 +- .../TextBlockTests.cs | 26 ++++++- 4 files changed, 72 insertions(+), 51 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 485df1ef1b2..39a8ff870ea 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -273,6 +273,11 @@ public double WidthIncludingTrailingWhitespace } } + /// + /// 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 @@ private TextLine[] CreateTextLines() { 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 @@ private TextLine[] CreateTextLines() 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 @@ private TextLine[] CreateTextLines() 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 @@ private TextLine[] CreateTextLines() 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 @@ private TextLine[] CreateTextLines() 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 @@ private TextLine[] CreateTextLines() } } - 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 @@ private class CachedMetrics // 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 c6da1726047..c038df94685 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -18,6 +18,9 @@ internal class TextLineImpl : TextLine 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 @@ public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, do /// 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 @@ private TextLineMetrics CreateLineMetrics() 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 f6a0cdbdfdf..a8324b2a5a2 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -740,9 +740,7 @@ protected override Size MeasureOverride(Size availableSize) //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 4a7281fae3a..f1399d83bc5 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.Media; using Avalonia.UnitTests; using Xunit; +using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls.UnitTests { @@ -48,6 +49,29 @@ public void Calling_Measure_Should_Update_TextLayout() } } + [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() {