From 267d2470cc26d0fda2d84fe49413c8fc214dd0c5 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 27 Aug 2025 15:18:06 +0000 Subject: [PATCH] Fix BringDescendantIntoView with respect to margins (#19544) * remove margin deflation in BringDescendantIntoView and update related test to test cases with margin and no margin * create transform relative to presenter, instead of child. * update tests * fix tests --- .../Presenters/ScrollContentPresenter.cs | 6 +- .../Presenters/ScrollContentPresenterTests.cs | 80 +++++++++++++++++-- .../VirtualizingStackPanelTests.cs | 2 +- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 465fdc10de..6527a14d5d 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -251,14 +251,16 @@ namespace Avalonia.Controls.Presenters return scrollable.BringIntoView(control, targetRect); } - var transform = target.TransformToVisual(Child); + var transform = target.TransformToVisual(this); if (transform == null) { return false; } - var rectangle = targetRect.TransformToAABB(transform.Value).Deflate(new Thickness(Child.Margin.Left, Child.Margin.Top, 0, 0)); + transform *= Matrix.CreateTranslation(Offset); + + var rectangle = targetRect.TransformToAABB(transform.Value); Rect viewport = new Rect(Offset.X, Offset.Y, Viewport.Width, Viewport.Height); double minX = ComputeScrollOffsetWithMinimalScroll(viewport.Left, viewport.Right, rectangle.Left, rectangle.Right); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index 09ce6d5d27..3a718bdd9a 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; using Avalonia.Layout; using Avalonia.UnitTests; using Xunit; @@ -359,6 +360,8 @@ namespace Avalonia.Controls.UnitTests.Presenters { Width = 100, Height = 100, + CanVerticallyScroll = true, + CanHorizontallyScroll = true, Content = new Border { Width = 200, @@ -404,13 +407,68 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void BringDescendantIntoView_Should_Not_Move_Child_If_Completely_In_View() + public void BringDescendantIntoView_Should_Move_Child_Even_With_Margin_In_Parent() { - Border border = new Border + var namescope = new NameScope(); + var content = new StackPanel() { + Orientation = Orientation.Vertical, Width = 100, - Height = 20 + Margin = new Thickness(0, 200), + }; + + for(int i = 0; i < 100; i++) + { + var child = new Border + { + Width = 100, + Height = 20, + Name = $"Border{i}" + }.RegisterInNameScope(namescope); + content.Children.Add(child); + } + var target = new ScrollContentPresenter + { + CanHorizontallyScroll = true, + CanVerticallyScroll = true, + Width = 200, + Height = 100, + Content = new Decorator + { + Child = content + } }; + + NameScope.SetNameScope(target, namescope); + + target.UpdateChild(); + target.Measure(Size.Infinity); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Border20 is at position 0,600 with bottom at Y=620 + var border20 = target.FindControl("Border20"); + target.BringDescendantIntoView(border20, new Rect(border20.Bounds.Size)); + + // With viewport Height of 100, border becomes fully visible when alligned from the bottom at Offset Y=520, i.e. 620-100 + Assert.Equal(new Vector(0, 520), target.Offset); + + // Reset stack panel's margin + content.Margin = default; + target.Measure(Size.Infinity); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Border20 is at position 0,800 with bottom at Y=820 + var border40 = target.FindControl("Border40"); + target.BringDescendantIntoView(border40, new Rect(border40.Bounds.Size)); + + // With viewport Height of 100, border becomes fully visible when alligned from the bottom at Offset Y=720, i.e. 820-100 + Assert.Equal(new Vector(0, 720), target.Offset); + } + + [Fact] + public void BringDescendantIntoView_Should_Not_Move_Child_If_Completely_In_View() + { + var namescope = new NameScope(); var content = new StackPanel() { Orientation = Orientation.Vertical, @@ -419,12 +477,12 @@ namespace Avalonia.Controls.UnitTests.Presenters for(int i = 0; i < 100; i++) { - // border position will be (0,60) - var child = i == 3 ? border : new Border + var child = new Border { Width = 100, Height = 20, - }; + Name = $"Border{i}" + }.RegisterInNameScope(namescope); content.Children.Add(child); } var target = new ScrollContentPresenter @@ -439,11 +497,15 @@ namespace Avalonia.Controls.UnitTests.Presenters } }; + NameScope.SetNameScope(target, namescope); + target.UpdateChild(); target.Measure(Size.Infinity); target.Arrange(new Rect(0, 0, 100, 100)); - target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); + var border3 = target.FindControl("Border3"); + target.BringDescendantIntoView(border3, new Rect(border3.Bounds.Size)); + // Border3 is still in view, offset hasn't changed Assert.Equal(new Vector(0, 0), target.Offset); } @@ -486,14 +548,17 @@ namespace Avalonia.Controls.UnitTests.Presenters target.UpdateChild(); target.Measure(Size.Infinity); target.Arrange(new Rect(0, 0, 100, 100)); + // move border to above the view port target.Offset = new Vector(0, 90); + target.Arrange(new Rect(0, 0, 100, 100)); target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); Assert.Equal(new Vector(0, 60), target.Offset); // move border to partially above the view port target.Offset = new Vector(0, 70); + target.Arrange(new Rect(0, 0, 100, 100)); target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); Assert.Equal(new Vector(0, 60), target.Offset); @@ -540,6 +605,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Arrange(new Rect(0, 0, 100, 100)); // move border such that it's partially above viewport and partially below viewport target.Offset = new Vector(0, 90); + target.Arrange(new Rect(0, 0, 100, 100)); target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); Assert.Equal(new Vector(0, 90), target.Offset); diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 6c6252d836..18c53efae7 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -1561,7 +1561,7 @@ namespace Avalonia.Controls.UnitTests [InlineData(0.5d, 0, 7, 0, 7, - 0, 9)] + 7, 17)] public void Focused_Container_Is_Positioned_Correctly_when_Container_Size_Change_Causes_It_To_Be_Moved_Into_Visible_Viewport(double bufferFactor, int firstIndex1, int lastIndex1, int firstIndex2, int lastIndex2,