Skip to content

Commit

Permalink
Calculate Bounds and InkBounds for TextLayout to be able to calculate…
Browse files Browse the repository at this point in the history
… the minmal required width. (#17721)
  • Loading branch information
Gillibald authored Dec 10, 2024
1 parent 0a8b2c7 commit 894d238
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 51 deletions.
74 changes: 28 additions & 46 deletions src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ public double WidthIncludingTrailingWhitespace
}
}

/// <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>
Expand Down Expand Up @@ -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 };
}
Expand All @@ -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)
{
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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)
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -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;
}
}
}
19 changes: 18 additions & 1 deletion src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -85,10 +88,20 @@ public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, do
/// <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)
{
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions src/Avalonia.Controls/TextBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
26 changes: 25 additions & 1 deletion tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System;
using System;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
using static System.Net.Mime.MediaTypeNames;

namespace Avalonia.Controls.UnitTests
{
Expand Down Expand Up @@ -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&#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()
{
Expand Down

0 comments on commit 894d238

Please sign in to comment.