From 406a5feb8935c8b89f39eee0f9b93903cbd38d0e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 14 Feb 2024 04:40:51 +0100 Subject: [PATCH] Fix InlineUIContainer focus (#14590) * Fix TextBlock MeasureOverride visual child handling * Make sure InlineUIContainer's child retains focus on measure * Resolve merge error #Conflicts: # src/Avalonia.Controls/TextBlock.cs --- .../Documents/IInlineHost.cs | 5 +- .../Documents/InlineCollection.cs | 16 ++++- src/Avalonia.Controls/TextBlock.cs | 38 ++++------ .../TextBlockTests.cs | 69 +++++++++++++++++++ .../TextBoxTests.cs | 2 +- 5 files changed, 103 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/Documents/IInlineHost.cs b/src/Avalonia.Controls/Documents/IInlineHost.cs index 5d142952ab..e16da219b4 100644 --- a/src/Avalonia.Controls/Documents/IInlineHost.cs +++ b/src/Avalonia.Controls/Documents/IInlineHost.cs @@ -1,9 +1,12 @@ -using Avalonia.LogicalTree; +using Avalonia.Collections; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Documents { internal interface IInlineHost : ILogical { void Invalidate(); + + IAvaloniaList VisualChildren { get; } } } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 39750d3672..a12fae3dc9 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -26,13 +26,27 @@ namespace Avalonia.Controls.Documents x => { x.InlineHost = InlineHost; + LogicalChildren?.Add(x); + + if (x is InlineUIContainer container) + { + InlineHost?.VisualChildren.Add(container.Child); + } + Invalidate(); }, x => { LogicalChildren?.Remove(x); - x.InlineHost = InlineHost; + + if(x is InlineUIContainer container) + { + InlineHost?.VisualChildren.Remove(container.Child); + } + + x.InlineHost = null; + Invalidate(); }, () => throw new NotSupportedException()); diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index ca5339086e..92f0b18127 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using Avalonia.Automation.Peers; +using Avalonia.Collections; using Avalonia.Controls.Documents; using Avalonia.Layout; using Avalonia.Media; @@ -668,16 +669,18 @@ namespace Avalonia.Controls /// Invalidates . /// protected void InvalidateTextLayout() + { + InvalidateMeasure(); + } + + protected override void OnMeasureInvalidated() { _textLayout?.Dispose(); _textLayout = null; - - VisualChildren.Clear(); _textRuns = null; - InvalidateVisual(); - InvalidateMeasure(); + base.OnMeasureInvalidated(); } protected override Size MeasureOverride(Size availableSize) @@ -686,8 +689,6 @@ namespace Avalonia.Controls var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); _constraint = availableSize.Deflate(padding); - _textLayout?.Dispose(); - _textLayout = null; var inlines = Inlines; @@ -701,21 +702,6 @@ namespace Avalonia.Controls } _textRuns = textRuns; - - foreach (var textLine in TextLayout.TextLines) - { - foreach (var run in textLine.TextRuns) - { - if (run is DrawableTextRun drawable) - { - if (drawable is EmbeddedControlRun controlRun - && controlRun.Control is Control control) - { - VisualChildren.Add(control); - } - } - } - } } var width = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing; @@ -827,26 +813,30 @@ namespace Avalonia.Controls private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue) { + VisualChildren.Clear(); + if (oldValue is not null) { oldValue.LogicalChildren = null; oldValue.InlineHost = null; - oldValue.Invalidated -= (s, e) => InvalidateTextLayout(); + oldValue.Invalidated -= (s, e) => InvalidateMeasure(); } if (newValue is not null) { newValue.LogicalChildren = LogicalChildren; newValue.InlineHost = this; - newValue.Invalidated += (s, e) => InvalidateTextLayout(); + newValue.Invalidated += (s, e) => InvalidateMeasure(); } } void IInlineHost.Invalidate() { - InvalidateTextLayout(); + InvalidateMeasure(); } + IAvaloniaList IInlineHost.VisualChildren => VisualChildren; + private readonly record struct SimpleTextSource : ITextSource { private readonly string _text; diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 8191013dad..79a6706983 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Documents; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Input; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Rendering; @@ -50,6 +51,55 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Can_Call_Measure_Without_InvalidateTextLayout() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new TextBlock(); + + target.Inlines.Add(new TextBox { Text = "Hello"}); + + target.Measure(Size.Infinity); + + target.InvalidateMeasure(); + + target.Measure(Size.Infinity); + } + } + + [Fact] + public void Embedded_Control_Should_Keep_Focus() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target = new TextBlock(); + + var root = new TestRoot + { + Child = target + }; + + var textBox = new TextBox { Text = "Hello", Template = TextBoxTests.CreateTemplate() }; + + target.Inlines.Add(textBox); + + target.Measure(Size.Infinity); + + textBox.Focus(); + + Assert.Same(textBox, root.FocusManager.GetFocusedElement()); + + target.InvalidateMeasure(); + + Assert.Same(textBox, root.FocusManager.GetFocusedElement()); + + target.Measure(Size.Infinity); + + Assert.Same(textBox, root.FocusManager.GetFocusedElement()); + } + } + [Fact] public void Changing_Inlines_Properties_Should_Invalidate_Measure() { @@ -115,6 +165,25 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Changing_Inlines_Should_Reset_VisualChildren() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new TextBlock(); + + target.Inlines.Add(new Border()); + + target.Measure(Size.Infinity); + + Assert.NotEmpty(target.VisualChildren); + + target.Inlines = null; + + Assert.Empty(target.VisualChildren); + } + } + [Fact] public void Changing_Inlines_Should_Reset_InlineUIContainer_VisualParent_On_Measure() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 4019136221..505cf6c2d6 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1349,7 +1349,7 @@ namespace Avalonia.Controls.UnitTests textShaperImpl: new HeadlessTextShaperStub(), fontManagerImpl: new HeadlessFontManagerStub()); - private IControlTemplate CreateTemplate() + internal static IControlTemplate CreateTemplate() { return new FuncControlTemplate((control, scope) => new ScrollViewer