From b285b88b8e216538440c3ec48829ea9cbfc4c128 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 15:15:22 +0100 Subject: [PATCH 1/4] Added some failing virtualization tests. --- .../VirtualizingStackPanelTests.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index f1dd874c71..55c43f6f96 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -278,6 +279,82 @@ namespace Avalonia.Controls.UnitTests Assert.Same(focused, target.GetRealizedElements().First()); } + [Fact] + public void Removing_Range_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new AvaloniaList(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.RemoveRange(0, 80); + Layout(target); + + AssertRealizedItems(target, itemsControl, 10, 10); + Assert.Equal(new Vector(0, 100), scroll.Offset); + } + + [Fact] + public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new AvaloniaList(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.RemoveRange(0, 95); + Layout(target); + + AssertRealizedItems(target, itemsControl, 0, 5); + Assert.Equal(new Vector(0, 0), scroll.Offset); + } + + [Fact] + public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}")); + Layout(target); + + AssertRealizedItems(target, itemsControl, 10, 10); + Assert.Equal(new Vector(0, 100), scroll.Offset); + } + + [Fact] + public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}")); + Layout(target); + + AssertRealizedItems(target, itemsControl, 0, 5); + Assert.Equal(new Vector(0, 0), scroll.Offset); + } + private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) { return target.GetRealizedElements() @@ -378,5 +455,24 @@ namespace Avalonia.Controls.UnitTests } private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus); + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(IEnumerable items) + { + AddRange(items); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + } } } From 10fae098b9d531ff08be542a05861dca919ed3a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 15:16:30 +0100 Subject: [PATCH 2/4] Fix index clamping. - Before we were clamping indexes too early, meaning that `firstIndexU` was calculated with a non-clamped index - `_startUUnstable` needs to be set when the remove happens before the realized elements --- src/Avalonia.Controls/VirtualizingStackPanel.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 634efbd699..8e7690aa6c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls if (firstIndex == -1) { estimatedElementSize = EstimateElementSizeU(); - firstIndex = (int)(viewportStart / estimatedElementSize); + firstIndex = Math.Min((int)(viewportStart / estimatedElementSize), maxIndex); firstIndexU = firstIndex * estimatedElementSize; } @@ -411,13 +411,13 @@ namespace Avalonia.Controls { if (estimatedElementSize == -1) estimatedElementSize = EstimateElementSizeU(); - lastIndex = (int)(viewportEnd / estimatedElementSize); + lastIndex = Math.Min((int)(viewportEnd / estimatedElementSize), maxIndex); } return new MeasureViewport { - firstIndex = MathUtilities.Clamp(firstIndex, 0, maxIndex), - lastIndex = MathUtilities.Clamp(lastIndex, 0, maxIndex), + firstIndex = firstIndex, + lastIndex = lastIndex, viewportUStart = viewportStart, viewportUEnd = viewportEnd, startU = firstIndexU, @@ -1131,6 +1131,7 @@ namespace Avalonia.Controls // The removed range was before the realized elements. Update the first index and // the indexes of the realized elements. _firstIndex -= count; + _startUUnstable = true; var newIndex = _firstIndex; for (var i = 0; i < _elements.Count; ++i) From 68074ce4b150f16582fe21c8688781bdd48b49c4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 15:16:41 +0100 Subject: [PATCH 3/4] Added failing ScrollViewer test. --- .../ScrollViewerTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index c3d35653cc..d3eb42f147 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -237,6 +237,40 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Reducing_Extent_Should_Constrain_Offset() + { + var target = new ScrollViewer + { + Template = new FuncControlTemplate(CreateTemplate), + }; + var root = new TestRoot(target); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(50, 50); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(new Vector(-30, -30), e.ExtentDelta); + Assert.Equal(new Vector(-30, -30), e.OffsetDelta); + Assert.Equal(default, e.ViewportDelta); + ++raised; + }; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(70, 70)); + + Assert.Equal(0, raised); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(1, raised); + Assert.Equal(new Vector(20, 20), target.Offset); + } + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid From 7da9bb9d434ecb70e5ef2fc7af768067c43709f1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 15:17:40 +0100 Subject: [PATCH 4/4] Coerce offset in SCP arrange. If viewport or extent were changed, this could affect the current offset so make sure we coerce the offset during arrange. --- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 454f7eac9d..bc86558ab3 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -427,6 +427,7 @@ namespace Avalonia.Controls.Presenters Viewport = finalSize; Extent = Child!.Bounds.Size.Inflate(Child.Margin); + Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset); _isAnchorElementDirty = true; return finalSize;