|
|
|
@ -632,6 +632,69 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
Assert.Equal(new Vector(0, 0), scroll.Offset); |
|
|
|
} |
|
|
|
|
|
|
|
[Fact] |
|
|
|
public void Shrinking_Viewport_Then_Growing_Back_Triggers_Remeasure() |
|
|
|
{ |
|
|
|
// Regression test for stale _extendedViewport comparison in OnEffectiveViewportChanged.
|
|
|
|
//
|
|
|
|
// When the viewport shrinks (e.g., ComboBox popup shrinks during filtering),
|
|
|
|
// OnEffectiveViewportChanged doesn't trigger a measure (needsMeasure=false because
|
|
|
|
// the smaller viewport is within the old extended viewport). The _extendedViewport
|
|
|
|
// comparison baseline is NOT updated. When the viewport later grows back,
|
|
|
|
// OnEffectiveViewportChanged compares against the stale large _extendedViewport,
|
|
|
|
// concludes "no significant change", and skips the measure. This prevents item
|
|
|
|
// realization when the only measure trigger is OnEffectiveViewportChanged.
|
|
|
|
//
|
|
|
|
// The fix uses a separate _lastKnownExtendedViewport that is always updated,
|
|
|
|
// so the comparison correctly detects viewport growth after a shrink.
|
|
|
|
//
|
|
|
|
// Key: ScrollContentPresenter passes infinite height for vertical scroll, so
|
|
|
|
// the panel's MeasureOverride is NOT called from the layout cascade when only
|
|
|
|
// the root size changes. OnEffectiveViewportChanged is the sole measure trigger.
|
|
|
|
using var app = App(); |
|
|
|
|
|
|
|
var items = Enumerable.Range(0, 20).Select(x => $"Item {x}"); |
|
|
|
var (target, scroll, itemsControl) = |
|
|
|
CreateUnrootedTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>( |
|
|
|
items: items, bufferFactor: 0); |
|
|
|
var root = CreateRoot(itemsControl, new Size(100, 100)); |
|
|
|
|
|
|
|
root.LayoutManager.ExecuteInitialLayoutPass(); |
|
|
|
|
|
|
|
// Initial state: viewport 0-100, 10 items visible, _extendedViewport = (0,0,100,100)
|
|
|
|
AssertRealizedItems(target, itemsControl, 0, 10); |
|
|
|
|
|
|
|
// Shrink viewport (simulates popup shrinking when items are filtered).
|
|
|
|
// Panel MeasureOverride is NOT called (ScrollContentPresenter passes infinite height).
|
|
|
|
// OnEffectiveViewportChanged fires with small viewport but needsMeasure=false
|
|
|
|
// because the small viewport is within the old _extendedViewport.
|
|
|
|
root.ClientSize = new Size(100, 10); |
|
|
|
root.InvalidateMeasure(); |
|
|
|
Layout(target); |
|
|
|
|
|
|
|
// Reset counters after shrink
|
|
|
|
target.ResetMeasureArrangeCounters(); |
|
|
|
|
|
|
|
// Grow viewport back (simulates popup growing when filter is removed).
|
|
|
|
// Panel MeasureOverride is NOT called from layout cascade (same infinite constraint).
|
|
|
|
// OnEffectiveViewportChanged is the ONLY path to trigger a remeasure.
|
|
|
|
root.ClientSize = new Size(100, 100); |
|
|
|
root.InvalidateMeasure(); |
|
|
|
Layout(target); |
|
|
|
|
|
|
|
// Without fix: OnEffectiveViewportChanged compares new viewport (0-100) against
|
|
|
|
// stale _extendedViewport (0-100, never updated during shrink). Sees no change.
|
|
|
|
// needsMeasure=false. No remeasure triggered. Measure count = 0.
|
|
|
|
//
|
|
|
|
// With fix: compares against _lastKnownExtendedViewport (0-10, updated during
|
|
|
|
// shrink). Detects that viewport grew past it (100 > 10). needsMeasure=true.
|
|
|
|
// InvalidateMeasure called. Measure count >= 1.
|
|
|
|
Assert.True(target.Measured >= 1, |
|
|
|
"Panel should be re-measured when viewport grows back after a previous shrink. " + |
|
|
|
"OnEffectiveViewportChanged must detect viewport growth by comparing against " + |
|
|
|
"the last known extended viewport, not the stale _extendedViewport."); |
|
|
|
} |
|
|
|
|
|
|
|
[Theory] |
|
|
|
[InlineData(0d, 10, "4,9")] |
|
|
|
[InlineData(0.5d, 20, "4,9,14,19")] |
|
|
|
@ -1655,8 +1718,8 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
Assert.Equal(0, target.ViewPort.Top); |
|
|
|
Assert.Equal(100, target.ViewPort.Bottom); |
|
|
|
|
|
|
|
Assert.Equal(0, target.ExtendedViewPort.Top); |
|
|
|
Assert.Equal(200, target.ExtendedViewPort.Bottom); |
|
|
|
Assert.Equal(0, target.LastMeasuredExtendedViewPort.Top); |
|
|
|
Assert.Equal(200, target.LastMeasuredExtendedViewPort.Bottom); |
|
|
|
} |
|
|
|
|
|
|
|
[Fact] |
|
|
|
@ -1680,8 +1743,8 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
Assert.Equal(900, target.ViewPort.Top); |
|
|
|
Assert.Equal(1000, target.ViewPort.Bottom); |
|
|
|
|
|
|
|
Assert.Equal(800, target.ExtendedViewPort.Top); |
|
|
|
Assert.Equal(1000, target.ExtendedViewPort.Bottom); |
|
|
|
Assert.Equal(800, target.LastMeasuredExtendedViewPort.Top); |
|
|
|
Assert.Equal(1000, target.LastMeasuredExtendedViewPort.Bottom); |
|
|
|
} |
|
|
|
|
|
|
|
[Fact] |
|
|
|
@ -1705,8 +1768,8 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
Assert.Equal(500, target.ViewPort.Top); |
|
|
|
Assert.Equal(600, target.ViewPort.Bottom); |
|
|
|
|
|
|
|
Assert.Equal(450, target.ExtendedViewPort.Top); |
|
|
|
Assert.Equal(650, target.ExtendedViewPort.Bottom); |
|
|
|
Assert.Equal(450, target.LastMeasuredExtendedViewPort.Top); |
|
|
|
Assert.Equal(650, target.LastMeasuredExtendedViewPort.Bottom); |
|
|
|
} |
|
|
|
|
|
|
|
[Fact] |
|
|
|
@ -1729,8 +1792,8 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
Assert.Equal(0, target.ViewPort.Left); |
|
|
|
Assert.Equal(100, target.ViewPort.Right); |
|
|
|
|
|
|
|
Assert.Equal(0, target.ExtendedViewPort.Left); |
|
|
|
Assert.Equal(200, target.ExtendedViewPort.Right); |
|
|
|
Assert.Equal(0, target.LastMeasuredExtendedViewPort.Left); |
|
|
|
Assert.Equal(200, target.LastMeasuredExtendedViewPort.Right); |
|
|
|
} |
|
|
|
|
|
|
|
[Fact] |
|
|
|
@ -1754,8 +1817,8 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
Assert.Equal(900, target.ViewPort.Left); |
|
|
|
Assert.Equal(1000, target.ViewPort.Right); |
|
|
|
|
|
|
|
Assert.Equal(800, target.ExtendedViewPort.Left); |
|
|
|
Assert.Equal(1000, target.ExtendedViewPort.Right); |
|
|
|
Assert.Equal(800, target.LastMeasuredExtendedViewPort.Left); |
|
|
|
Assert.Equal(1000, target.LastMeasuredExtendedViewPort.Right); |
|
|
|
} |
|
|
|
|
|
|
|
[Fact] |
|
|
|
@ -1780,8 +1843,8 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
Assert.Equal(500, target.ViewPort.Left); |
|
|
|
Assert.Equal(600, target.ViewPort.Right); |
|
|
|
|
|
|
|
Assert.Equal(450, target.ExtendedViewPort.Left); |
|
|
|
Assert.Equal(650, target.ExtendedViewPort.Right); |
|
|
|
Assert.Equal(450, target.LastMeasuredExtendedViewPort.Left); |
|
|
|
Assert.Equal(650, target.LastMeasuredExtendedViewPort.Right); |
|
|
|
} |
|
|
|
|
|
|
|
[Fact] |
|
|
|
@ -1806,11 +1869,15 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
// visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
|
|
|
|
// until then no measure-arrange call should happen
|
|
|
|
|
|
|
|
var count = 0; |
|
|
|
// Scroll down until the extended viewport bounds are reached
|
|
|
|
while (target.LastRealizedIndex < 20) |
|
|
|
{ |
|
|
|
scroll.Offset = new Vector(0, scroll.Offset.Y + 5); |
|
|
|
Layout(target); |
|
|
|
count++; |
|
|
|
if (count > 1000) |
|
|
|
throw new InvalidOperationException("infinite scroll detected"); |
|
|
|
} |
|
|
|
|
|
|
|
// Assert
|
|
|
|
@ -1862,11 +1929,15 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
|
|
|
|
var initialFirstRealizedIndex = target.FirstRealizedIndex; |
|
|
|
|
|
|
|
var count = 0; |
|
|
|
// Scroll down until the extended viewport bounds are reached
|
|
|
|
while (target.FirstRealizedIndex >= 15) |
|
|
|
{ |
|
|
|
scroll.Offset = new Vector(0, scroll.Offset.Y - 5); |
|
|
|
Layout(target); |
|
|
|
count++; |
|
|
|
if (count > 1000) |
|
|
|
throw new InvalidOperationException("infinite scroll detected"); |
|
|
|
} |
|
|
|
|
|
|
|
// Assert
|
|
|
|
@ -1918,11 +1989,15 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
|
|
|
|
var initialLastRealizedIndex = target.LastRealizedIndex; |
|
|
|
|
|
|
|
var count = 0; |
|
|
|
// Scroll down until we reached the very last item
|
|
|
|
while (target.LastRealizedIndex < 99) |
|
|
|
{ |
|
|
|
scroll.Offset = new Vector(0, scroll.Offset.Y + 5); |
|
|
|
Layout(target); |
|
|
|
count++; |
|
|
|
if (count > 1000) |
|
|
|
throw new InvalidOperationException("infinite scroll detected"); |
|
|
|
} |
|
|
|
|
|
|
|
// Assert
|
|
|
|
@ -1972,11 +2047,15 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
// visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
|
|
|
|
// until then no measure-arrange call should happen
|
|
|
|
|
|
|
|
var count = 0; |
|
|
|
// Scroll down until the extended viewport bounds are reached
|
|
|
|
while (target.FirstRealizedIndex > 0) |
|
|
|
{ |
|
|
|
scroll.Offset = new Vector(0, scroll.Offset.Y - 5); |
|
|
|
Layout(target); |
|
|
|
count++; |
|
|
|
if (count > 1000) |
|
|
|
throw new InvalidOperationException("infinite scroll detected"); |
|
|
|
} |
|
|
|
|
|
|
|
// Assert
|
|
|
|
@ -2022,12 +2101,15 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
// shows 20 items, each is 10 high.
|
|
|
|
// visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
|
|
|
|
// until then no measure-arrange call should happen
|
|
|
|
|
|
|
|
var count = 0; |
|
|
|
// Scroll down until the extended viewport bounds are reached
|
|
|
|
while (target.LastRealizedIndex < 20) |
|
|
|
{ |
|
|
|
scroll.Offset = new Vector(scroll.Offset.X + 5, 0); |
|
|
|
Layout(target); |
|
|
|
count++; |
|
|
|
if (count > 1000) |
|
|
|
throw new InvalidOperationException("infinite scroll detected"); |
|
|
|
} |
|
|
|
|
|
|
|
// Assert
|
|
|
|
@ -2079,12 +2161,15 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
// until then no measure-arrange call should happen
|
|
|
|
|
|
|
|
var initialFirstRealizedIndex = target.FirstRealizedIndex; |
|
|
|
|
|
|
|
var count = 0; |
|
|
|
// Scroll down until the extended viewport bounds are reached
|
|
|
|
while (target.FirstRealizedIndex >= 15) |
|
|
|
{ |
|
|
|
scroll.Offset = new Vector(scroll.Offset.X - 5, 0); |
|
|
|
Layout(target); |
|
|
|
count++; |
|
|
|
if (count > 1000) |
|
|
|
throw new InvalidOperationException("infinite scroll detected"); |
|
|
|
} |
|
|
|
|
|
|
|
// Assert
|
|
|
|
@ -2137,11 +2222,15 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
|
|
|
|
var initialLastRealizedIndex = target.LastRealizedIndex; |
|
|
|
|
|
|
|
var count = 0; |
|
|
|
// Scroll down until we reached the very last item
|
|
|
|
while (target.LastRealizedIndex < 99) |
|
|
|
{ |
|
|
|
scroll.Offset = new Vector(scroll.Offset.X + 5, 0); |
|
|
|
Layout(target); |
|
|
|
count++; |
|
|
|
if (count > 1000) |
|
|
|
throw new InvalidOperationException("infinite scroll detected"); |
|
|
|
} |
|
|
|
|
|
|
|
// Assert
|
|
|
|
@ -2192,11 +2281,15 @@ namespace Avalonia.Controls.UnitTests |
|
|
|
// visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
|
|
|
|
// until then no measure-arrange call should happen
|
|
|
|
|
|
|
|
var count = 0; |
|
|
|
// Scroll down until the extended viewport bounds are reached
|
|
|
|
while (target.FirstRealizedIndex > 0) |
|
|
|
{ |
|
|
|
scroll.Offset = new Vector(scroll.Offset.X - 5, 0); |
|
|
|
Layout(target); |
|
|
|
count++; |
|
|
|
if (count > 1000) |
|
|
|
throw new InvalidOperationException("infinite scroll detected"); |
|
|
|
} |
|
|
|
|
|
|
|
// Assert
|
|
|
|
|