Skip to content

Commit 76da4cb

Browse files
author
alexander.marek
committed
fix(text): don't shape stale content when TextLayout is built outside measure
1 parent ac4b7ac commit 76da4cb

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

src/Avalonia.Controls/TextBlock.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,31 @@ public TextBlock()
186186
/// <summary>
187187
/// Gets the <see cref="TextLayout"/> used to render the text.
188188
/// </summary>
189-
public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text);
189+
public TextLayout TextLayout => _textLayout ??= CreateTextLayoutCore();
190+
191+
private TextLayout CreateTextLayoutCore()
192+
{
193+
// MeasureOverride normally builds the text runs before the layout is created. When the
194+
// layout is requested outside of a measure pass - for example by a render pass that runs
195+
// before a queued measure - the runs are still null and CreateTextLayout would silently
196+
// fall back to Text, shaping the wrong (usually empty) content. The shaped result is
197+
// stored in the TextRunCache keyed only by text source index, so that wrong content would
198+
// then be reused by every later layout until the cache is invalidated.
199+
if (_textRuns == null && HasComplexContent)
200+
{
201+
var textRuns = new List<TextRun>();
202+
var constraint = GetMaxSizeFromConstraint();
203+
204+
foreach (var inline in Inlines!)
205+
{
206+
inline.BuildTextRun(textRuns, constraint);
207+
}
208+
209+
_textRuns = textRuns;
210+
}
211+
212+
return CreateTextLayout(Text);
213+
}
190214

191215
/// <summary>
192216
/// Gets or sets the padding to place around the <see cref="Text"/>.

tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,68 @@ public void Measure_And_Arrange_Should_Use_WidthIncludingTrailingWhitespace_For_
614614
Assert.Equal(new Rect(default, expectedSize), target.Bounds);
615615
}
616616

617+
[Fact]
618+
public void Reading_TextLayout_Before_Measure_Should_Not_Poison_Inlines_Shaping()
619+
{
620+
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
621+
622+
var target = new TextBlock
623+
{
624+
Inlines = new InlineCollection { new Run("Hello World") }
625+
};
626+
627+
// Something outside of the layout pass (e.g. a render pass that runs before the
628+
// queued measure) reads TextLayout while _textRuns has not been built yet.
629+
_ = target.TextLayout;
630+
631+
target.Measure(new Size(1000, 1000));
632+
633+
Assert.True(target.DesiredSize.Width > 0, $"DesiredSize was {target.DesiredSize}");
634+
}
635+
636+
[Fact]
637+
public void Reading_TextLayout_Between_Invalidation_And_Measure_Should_Not_Poison_Inlines_Shaping()
638+
{
639+
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
640+
641+
// Starts out empty, exactly like a title bound through a converter that yields an
642+
// empty InlineCollection until the view model delivers the real value.
643+
var target = new TextBlock { Inlines = new InlineCollection() };
644+
645+
target.Measure(new Size(1000, 1000));
646+
target.Arrange(new Rect(0, 0, 1000, 1000));
647+
648+
target.Inlines = new InlineCollection { new Run("Hello World") };
649+
650+
// Render runs before the queued measure is processed.
651+
_ = target.TextLayout;
652+
653+
target.Measure(new Size(1000, 1000));
654+
655+
Assert.True(target.DesiredSize.Width > 0, $"DesiredSize was {target.DesiredSize}");
656+
}
657+
658+
[Fact]
659+
public void Reading_TextLayout_Before_Measure_Should_Not_Poison_TextRunCache_Across_Constraints()
660+
{
661+
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
662+
663+
var target = new TextBlock { Inlines = new InlineCollection() };
664+
665+
target.Measure(new Size(1000, 1000));
666+
target.Arrange(new Rect(0, 0, 1000, 1000));
667+
668+
target.Inlines = new InlineCollection { new Run("Hello World") };
669+
670+
_ = target.TextLayout;
671+
672+
// A *different* constraint forces MeasureOverride to drop _textLayout, so anything
673+
// still wrong here comes from the TextRunCache rather than the stale layout.
674+
target.Measure(new Size(900, 1000));
675+
676+
Assert.True(target.DesiredSize.Width > 0, $"DesiredSize was {target.DesiredSize}");
677+
}
678+
617679
private class TestTextBlock : TextBlock
618680
{
619681
public Size Constraint => _constraint;

0 commit comments

Comments
 (0)