Browse Source

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

release/11.2.3
Benedikt Stebner 1 year ago
committed by Max Katz
parent
commit
56f6296066
  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>
/// Draws the text layout.
/// </summary>
@ -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);
}
/// <summary>
@ -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;
}
}
}

19
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
/// <inheritdoc/>
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/>
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,

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.
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;
}

26
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&#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]
public void Calling_Arrange_With_Different_Size_Should_Update_Constraint_And_TextLayout()
{

Loading…
Cancel
Save