Browse Source

Calculate Bounds and InkBounds for TextLayout to be able to calculate the minmal required width. (#17721)

pull/17742/head
Benedikt Stebner 2 years ago
committed by GitHub
parent
commit
894d2386ab
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 74
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  2. 19
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  3. 4
      src/Avalonia.Controls/TextBlock.cs
  4. 26
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

74
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -273,6 +273,11 @@ namespace Avalonia.Media.TextFormatting
} }
} }
/// <summary>
/// Get minimum width of all text lines that can be layouted horizontally without trimming or wrapping.
/// </summary>
internal double MinTextWidth => _metrics.MinTextWidth;
/// <summary> /// <summary>
/// Draws the text layout. /// Draws the text layout.
/// </summary> /// </summary>
@ -544,21 +549,13 @@ namespace Avalonia.Media.TextFormatting
{ {
var objectPool = FormattingObjectPool.Instance; var objectPool = FormattingObjectPool.Instance;
var lineStartOfLongestLine = double.MaxValue;
var origin = new Point();
var first = true; var first = true;
double accBlackBoxLeft, accBlackBoxTop, accBlackBoxRight, accBlackBoxBottom;
accBlackBoxLeft = accBlackBoxTop = double.MaxValue;
accBlackBoxRight = accBlackBoxBottom = double.MinValue;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, UpdateMetrics(textLine, ref first);
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
return new TextLine[] { textLine }; return new TextLine[] { textLine };
} }
@ -576,7 +573,7 @@ namespace Avalonia.Media.TextFormatting
while (true) while (true)
{ {
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak); _paragraphProperties, previousLine?.TextLineBreak) as TextLineImpl;
if (textLine is null) if (textLine is null)
{ {
@ -587,8 +584,7 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
UpdateMetrics(emptyTextLine, ref lineStartOfLongestLine, ref origin, ref first, UpdateMetrics(emptyTextLine, ref first);
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
} }
break; break;
@ -615,13 +611,12 @@ namespace Avalonia.Media.TextFormatting
if (hasOverflowed && _textTrimming != TextTrimming.None) if (hasOverflowed && _textTrimming != TextTrimming.None)
{ {
textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); textLine = (TextLineImpl)textLine.Collapse(GetCollapsingProperties(MaxWidth));
} }
textLines.Add(textLine); textLines.Add(textLine);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, UpdateMetrics(textLine, ref first);
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
previousLine = textLine; previousLine = textLine;
@ -648,8 +643,7 @@ namespace Avalonia.Media.TextFormatting
textLines.Add(textLine); textLines.Add(textLine);
UpdateMetrics(textLine, ref lineStartOfLongestLine, ref origin, ref first, UpdateMetrics(textLine, ref first);
ref accBlackBoxLeft, ref accBlackBoxTop, ref accBlackBoxRight, ref accBlackBoxBottom);
} }
if (_paragraphProperties.TextAlignment == TextAlignment.Justify) if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
@ -683,44 +677,27 @@ namespace Avalonia.Media.TextFormatting
} }
} }
private void UpdateMetrics( private void UpdateMetrics(TextLineImpl currentLine, ref bool first)
TextLine currentLine,
ref double lineStartOfLongestLine,
ref Point origin,
ref bool first,
ref double accBlackBoxLeft,
ref double accBlackBoxTop,
ref double accBlackBoxRight,
ref double accBlackBoxBottom)
{ {
var blackBoxLeft = origin.X + currentLine.Start + currentLine.OverhangLeading; _metrics.InkBounds = _metrics.InkBounds.Union(new Rect(new Point(0, _metrics.Bounds.Bottom) + currentLine.InkBounds.Position, currentLine.InkBounds.Size));
var blackBoxRight = origin.X + currentLine.Start + currentLine.Width - currentLine.OverhangTrailing; _metrics.Bounds = _metrics.Bounds.Union(new Rect(new Point(0, _metrics.Bounds.Bottom) + currentLine.Bounds.Position, currentLine.Bounds.Size));
var blackBoxBottom = origin.Y + currentLine.Height + currentLine.OverhangAfter;
var blackBoxTop = blackBoxBottom - currentLine.Extent;
accBlackBoxLeft = Math.Min(accBlackBoxLeft, blackBoxLeft); _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.Bounds.Width);
accBlackBoxRight = Math.Max(accBlackBoxRight, blackBoxRight); _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.InkBounds.Width);
accBlackBoxBottom = Math.Max(accBlackBoxBottom, blackBoxBottom);
accBlackBoxTop = Math.Min(accBlackBoxTop, blackBoxTop);
_metrics.OverhangAfter = currentLine.OverhangAfter; _metrics.Height = _metrics.Bounds.Height;
_metrics.Width = _metrics.InkBounds.Width;
_metrics.Height += currentLine.Height; _metrics.WidthIncludingTrailingWhitespace = _metrics.Bounds.Width;
_metrics.Width = Math.Max(_metrics.Width, currentLine.Width); _metrics.Extent = _metrics.InkBounds.Height;
_metrics.WidthIncludingTrailingWhitespace = Math.Max(_metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace); _metrics.OverhangLeading = Math.Max(0, _metrics.Bounds.Left - _metrics.InkBounds.Left);
lineStartOfLongestLine = Math.Min(lineStartOfLongestLine, currentLine.Start); _metrics.OverhangTrailing = Math.Max(0, _metrics.InkBounds.Right - _metrics.Bounds.Right);
_metrics.OverhangAfter = Math.Max(0, _metrics.InkBounds.Bottom - _metrics.Bounds.Bottom);
_metrics.Extent = accBlackBoxBottom - accBlackBoxTop;
_metrics.OverhangLeading = accBlackBoxLeft - lineStartOfLongestLine;
_metrics.OverhangTrailing = _metrics.Width - (accBlackBoxRight - lineStartOfLongestLine);
if (first) if (first)
{ {
_metrics.Baseline = currentLine.Baseline; _metrics.Baseline = currentLine.Baseline;
first = false; first = false;
} }
origin = origin.WithY(origin.Y + currentLine.Height);
} }
/// <summary> /// <summary>
@ -764,6 +741,11 @@ namespace Avalonia.Media.TextFormatting
// horizontal bounding box metrics // horizontal bounding box metrics
public double OverhangLeading; public double OverhangLeading;
public double OverhangTrailing; public double OverhangTrailing;
public Rect Bounds;
public Rect InkBounds;
public double MinTextWidth;
} }
} }
} }

19
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -18,6 +18,9 @@ namespace Avalonia.Media.TextFormatting
private TextLineBreak? _textLineBreak; private TextLineBreak? _textLineBreak;
private readonly FlowDirection _resolvedFlowDirection; private readonly FlowDirection _resolvedFlowDirection;
private Rect _inkBounds;
private Rect _bounds;
public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false) TextLineBreak? lineBreak = null, bool hasCollapsed = false)
@ -85,10 +88,20 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override double WidthIncludingTrailingWhitespace => _textLineMetrics.WidthIncludingTrailingWhitespace; public override double WidthIncludingTrailingWhitespace => _textLineMetrics.WidthIncludingTrailingWhitespace;
/// <summary>
/// Get the logical text bounds.
/// </summary>
internal Rect Bounds => _bounds;
/// <summary>
/// Get the bounding box that is covered with black pixels.
/// </summary>
internal Rect InkBounds => _inkBounds;
/// <inheritdoc/> /// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point lineOrigin) 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) foreach (var textRun in _textRuns)
{ {
@ -1377,6 +1390,10 @@ namespace Avalonia.Media.TextFormatting
var start = GetParagraphOffsetX(width, widthIncludingWhitespace); 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 return new TextLineMetrics
{ {
HasOverflowed = hasOverflowed, HasOverflowed = hasOverflowed,

4
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. //This implicitly recreated the TextLayout with a new constraint if we previously reset it.
var textLayout = TextLayout; var textLayout = TextLayout;
var width = textLayout.OverhangLeading + textLayout.WidthIncludingTrailingWhitespace + textLayout.OverhangTrailing; var size = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height).Inflate(padding), 1, 1);
var size = LayoutHelper.RoundLayoutSizeUp(new Size(width, textLayout.Height).Inflate(padding), 1, 1);
return size; return size;
} }

26
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -1,4 +1,4 @@
using System; using System;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
@ -6,6 +6,7 @@ using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls.UnitTests 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&#10;שלום&#10;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] [Fact]
public void Calling_Arrange_With_Different_Size_Should_Update_Constraint_And_TextLayout() public void Calling_Arrange_With_Different_Size_Should_Update_Constraint_And_TextLayout()
{ {

Loading…
Cancel
Save