From 7eb4d9ef1986eae0f9407c5bb62d7c71eee44789 Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:25:24 +0100 Subject: [PATCH] Fix for remaining Ghost-Item in VirtualizationStackPanel (#20784) * Add failing test for #20688 Unit test created from issue description * Add a failing test for wrong CacheLength handling All items would be realized which is obviously wrong * fix test setup used StackPanel instead of expcected VirtualizingStackPanel * Make sure the test actually fails * update comment * Fix for focusedElement and focusedIndex * add another unit test * Fixes for new test cases * Addressing Review * Update tests to match new behavior * only recycle focused element if it is not null * Address review * Address copilot review * add failing test * fix StartU estimation * remove unused sample file --- .../VirtualizingStackPanel.cs | 27 ++++++++++++++- .../VirtualizingStackPanelTests.cs | 34 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 2baf3d6147..65e3adf3dc 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -858,7 +858,32 @@ namespace Avalonia.Controls // Estimate the element size. var estimatedSize = EstimateElementSizeU(); - // TODO: Use _startU to work this out. + // If we have a valid StartU, use it to anchor estimates relative to the realized range. + if (_realizedElements is { } realized && !double.IsNaN(realized.StartU)) + { + var first = realized.FirstIndex; + var last = realized.LastIndex; + + if (index < first) + { + return realized.StartU - ((first - index) * estimatedSize); + } + + if (index > last) + { + var sizes = realized.SizeU; + var realizedSpan = 0.0; + + for (var i = 0; i < sizes.Count; ++i) + { + var sizeU = sizes[i]; + realizedSpan += double.IsNaN(sizeU) ? estimatedSize : sizeU; + } + + return realized.StartU + realizedSpan + ((index - last - 1) * estimatedSize); + } + } + return index * estimatedSize; } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 3dffc5d47e..89eaab6f66 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -1553,6 +1553,40 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 125, 100, 25), container.Bounds); } + [Fact] + public void Focused_Container_Is_Positioned_Correctly_When_Scrolled_Past_Items_With_Different_Heights() + { + using var app = App(); + + var items = Enumerable.Range(0, 20) + .Select(x => new ItemWithHeight(x, x < 10 ? 10 : 50)) + .ToList(); + + var (target, _, _) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate); + + var focused = Assert.IsType(target.ContainerFromIndex(5)); + focused.Focusable = true; + focused.Focus(); + + target.ScrollIntoView(15); + Layout(target); + + Assert.True(target.FirstRealizedIndex > 5); + + var firstIndex = target.FirstRealizedIndex; + var firstRealized = Assert.IsType(target.ContainerFromIndex(firstIndex)); + var realized = target.GetRealizedElements() + .Where(x => x is not null) + .Cast() + .ToList(); + + var estimatedSize = realized.Average(x => x.DesiredSize.Height); + var expectedTop = firstRealized.Bounds.Top - ((firstIndex - 5) * estimatedSize); + + focused = Assert.IsType(target.ContainerFromIndex(5)); + Assert.Equal(expectedTop, focused.Bounds.Top, 3); + } + [Theory] [InlineData(0d, 4, 7,