Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/Avalonia.Controls/TextBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,31 @@ public TextBlock()
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text);
public TextLayout TextLayout => _textLayout ??= CreateTextLayoutCore();

private TextLayout CreateTextLayoutCore()
{
// MeasureOverride normally builds the text runs before the layout is created. When the
// layout is requested outside of a measure pass - for example by a render pass that runs
// before a queued measure - the runs are still null and CreateTextLayout would silently
// fall back to Text, shaping the wrong (usually empty) content. The shaped result is
// stored in the TextRunCache keyed only by text source index, so that wrong content would
// then be reused by every later layout until the cache is invalidated.
if (_textRuns == null && HasComplexContent)
{
var textRuns = new List<TextRun>();
var constraint = GetMaxSizeFromConstraint();

foreach (var inline in Inlines!)
{
inline.BuildTextRun(textRuns, constraint);
}

_textRuns = textRuns;
}

return CreateTextLayout(Text);
}

/// <summary>
/// Gets or sets the padding to place around the <see cref="Text"/>.
Expand Down
62 changes: 62 additions & 0 deletions tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,68 @@ public void Measure_And_Arrange_Should_Use_WidthIncludingTrailingWhitespace_For_
Assert.Equal(new Rect(default, expectedSize), target.Bounds);
}

[Fact]
public void Reading_TextLayout_Before_Measure_Should_Not_Poison_Inlines_Shaping()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);

var target = new TextBlock
{
Inlines = new InlineCollection { new Run("Hello World") }
};

// Something outside of the layout pass (e.g. a render pass that runs before the
// queued measure) reads TextLayout while _textRuns has not been built yet.
_ = target.TextLayout;

target.Measure(new Size(1000, 1000));

Assert.True(target.DesiredSize.Width > 0, $"DesiredSize was {target.DesiredSize}");
}

[Fact]
public void Reading_TextLayout_Between_Invalidation_And_Measure_Should_Not_Poison_Inlines_Shaping()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);

// Starts out empty, exactly like a title bound through a converter that yields an
// empty InlineCollection until the view model delivers the real value.
var target = new TextBlock { Inlines = new InlineCollection() };

target.Measure(new Size(1000, 1000));
target.Arrange(new Rect(0, 0, 1000, 1000));

target.Inlines = new InlineCollection { new Run("Hello World") };

// Render runs before the queued measure is processed.
_ = target.TextLayout;

target.Measure(new Size(1000, 1000));

Assert.True(target.DesiredSize.Width > 0, $"DesiredSize was {target.DesiredSize}");
}

[Fact]
public void Reading_TextLayout_Before_Measure_Should_Not_Poison_TextRunCache_Across_Constraints()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);

var target = new TextBlock { Inlines = new InlineCollection() };

target.Measure(new Size(1000, 1000));
target.Arrange(new Rect(0, 0, 1000, 1000));

target.Inlines = new InlineCollection { new Run("Hello World") };

_ = target.TextLayout;

// A *different* constraint forces MeasureOverride to drop _textLayout, so anything
// still wrong here comes from the TextRunCache rather than the stale layout.
target.Measure(new Size(900, 1000));

Assert.True(target.DesiredSize.Width > 0, $"DesiredSize was {target.DesiredSize}");
}

private class TestTextBlock : TextBlock
{
public Size Constraint => _constraint;
Expand Down