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()
{