diff --git a/src/Avalonia.Controls/Utils/RealizedStackElements.cs b/src/Avalonia.Controls/Utils/RealizedStackElements.cs
new file mode 100644
index 0000000000..ba1f07a632
--- /dev/null
+++ b/src/Avalonia.Controls/Utils/RealizedStackElements.cs
@@ -0,0 +1,448 @@
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Controls.Utils
+{
+ ///
+ /// Stores the realized element state for a virtualizing panel that arranges its children
+ /// in a stack layout, such as .
+ ///
+ internal class RealizedStackElements
+ {
+ private int _firstIndex;
+ private List? _elements;
+ private List? _sizes;
+ private double _startU;
+ private bool _startUUnstable;
+
+ ///
+ /// Gets the number of realized elements.
+ ///
+ public int Count => _elements?.Count ?? 0;
+
+ ///
+ /// Gets the index of the first realized element, or -1 if no elements are realized.
+ ///
+ public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1;
+
+ ///
+ /// Gets the index of the last realized element, or -1 if no elements are realized.
+ ///
+ public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1;
+
+ ///
+ /// Gets the elements.
+ ///
+ public IReadOnlyList Elements => _elements ??= new List();
+
+ ///
+ /// Gets the sizes of the elements on the primary axis.
+ ///
+ public IReadOnlyList SizeU => _sizes ??= new List();
+
+ ///
+ /// Gets the position of the first element on the primary axis.
+ ///
+ public double StartU => _startU;
+
+ ///
+ /// Adds a newly realized element to the collection.
+ ///
+ /// The index of the element.
+ /// The element.
+ /// The position of the elemnt on the primary axis.
+ /// The size of the element on the primary axis.
+ public void Add(int index, Control element, double u, double sizeU)
+ {
+ if (index < 0)
+ throw new ArgumentOutOfRangeException(nameof(index));
+
+ _elements ??= new List();
+ _sizes ??= new List();
+
+ if (Count == 0)
+ {
+ _elements.Add(element);
+ _sizes.Add(sizeU);
+ _startU = u;
+ _firstIndex = index;
+ }
+ else if (index == LastIndex + 1)
+ {
+ _elements.Add(element);
+ _sizes.Add(sizeU);
+ }
+ else if (index == FirstIndex - 1)
+ {
+ --_firstIndex;
+ _elements.Insert(0, element);
+ _sizes.Insert(0, sizeU);
+ _startU = u;
+ }
+ else
+ {
+ throw new NotSupportedException("Can only add items to the beginning or end of realized elements.");
+ }
+ }
+
+ ///
+ /// Gets the element at the specified index, if realized.
+ ///
+ /// The index in the source collection of the element to get.
+ /// The element if realized; otherwise null.
+ public Control? GetElement(int index)
+ {
+ var i = index - FirstIndex;
+ if (i >= 0 && i < _elements?.Count)
+ return _elements[i];
+ return null;
+ }
+
+ ///
+ /// Gets the index and start U position of the element at the specified U position.
+ ///
+ /// The U position.
+ ///
+ /// A tuple containing:
+ /// - The index of the item at the specified U position, or -1 if the item could not be
+ /// determined
+ /// - The U position of the start of the item, if determined
+ ///
+ public (int index, double position) GetIndexAt(double u)
+ {
+ if (_elements is null || _sizes is null || _startU > u || _startUUnstable)
+ return (-1, 0);
+
+ var index = 0;
+ var position = _startU;
+
+ while (index < _elements.Count)
+ {
+ var size = _sizes[index];
+ if (double.IsNaN(size))
+ break;
+ if (u >= position && u < position + size)
+ return (index + FirstIndex, position);
+ position += size;
+ ++index;
+ }
+
+ return (-1, 0);
+ }
+
+ ///
+ /// Gets the element at the specified position on the primary axis, if realized.
+ ///
+ /// The position.
+ ///
+ /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the
+ /// primary axis.
+ ///
+ public (int index, double position) GetElementAt(double position)
+ {
+ if (_sizes is null || position < StartU)
+ return (-1, 0);
+
+ var u = StartU;
+ var i = FirstIndex;
+
+ foreach (var size in _sizes)
+ {
+ var endU = u + size;
+ if (position < endU)
+ return (i, u);
+ u += size;
+ ++i;
+ }
+
+ return (-1, 0);
+ }
+
+ ///
+ /// Estimates the average U size of all elements in the source collection based on the
+ /// realized elements.
+ ///
+ ///
+ /// The estimated U size of an element, or -1 if not enough information is present to make
+ /// an estimate.
+ ///
+ public double EstimateElementSizeU()
+ {
+ var total = 0.0;
+ var divisor = 0.0;
+
+ // Start by averaging the size of the elements before the first realized element.
+ if (FirstIndex >= 0 && !_startUUnstable)
+ {
+ total += _startU;
+ divisor += FirstIndex;
+ }
+
+ // Average the size of the realized elements.
+ if (_sizes is not null)
+ {
+ foreach (var size in _sizes)
+ {
+ if (double.IsNaN(size))
+ continue;
+ total += size;
+ ++divisor;
+ }
+ }
+
+ // We don't have any elements on which to base our estimate.
+ if (divisor == 0 || total == 0)
+ return -1;
+
+ return total / divisor;
+ }
+
+ ///
+ /// Gets the index of the specified element.
+ ///
+ /// The element.
+ /// The index or -1 if the element is not present in the collection.
+ public int GetIndex(Control element)
+ {
+ return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1;
+ }
+
+ ///
+ /// Updates the elements in response to items being inserted into the source collection.
+ ///
+ /// The index in the source collection of the insert.
+ /// The number of items inserted.
+ /// A method used to update the element indexes.
+ public void ItemsInserted(int index, int count, Action updateElementIndex)
+ {
+ if (index < 0)
+ throw new ArgumentOutOfRangeException(nameof(index));
+ if (_elements is null || _elements.Count == 0)
+ return;
+
+ // Get the index within the realized _elements collection.
+ var first = FirstIndex;
+ var realizedIndex = index - first;
+
+ if (realizedIndex < Count)
+ {
+ // The insertion point affects the realized elements. Update the index of the
+ // elements after the insertion point.
+ var elementCount = _elements.Count;
+ var start = Math.Max(realizedIndex, 0);
+ var newIndex = realizedIndex + count;
+
+ for (var i = start; i < elementCount; ++i)
+ {
+ if (_elements[i] is Control element)
+ updateElementIndex(element, newIndex - count, newIndex);
+ ++newIndex;
+ }
+
+ if (realizedIndex < 0)
+ {
+ // The insertion point was before the first element, update the first index.
+ _firstIndex += count;
+ }
+ else
+ {
+ // The insertion point was within the realized elements, insert an empty space
+ // in _elements and _sizes.
+ _elements!.InsertMany(realizedIndex, null, count);
+ _sizes!.InsertMany(realizedIndex, double.NaN, count);
+ }
+ }
+ }
+
+ ///
+ /// Updates the elements in response to items being removed from the source collection.
+ ///
+ /// The index in the source collection of the remove.
+ /// The number of items removed.
+ /// A method used to update the element indexes.
+ /// A method used to recycle elements.
+ public void ItemsRemoved(
+ int index,
+ int count,
+ Action updateElementIndex,
+ Action recycleElement)
+ {
+ if (index < 0)
+ throw new ArgumentOutOfRangeException(nameof(index));
+ if (_elements is null || _elements.Count == 0)
+ return;
+
+ // Get the removal start and end index within the realized _elements collection.
+ var first = FirstIndex;
+ var last = LastIndex;
+ var startIndex = index - first;
+ var endIndex = (index + count) - first;
+
+ if (endIndex < 0)
+ {
+ // 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)
+ {
+ if (_elements[i] is Control element)
+ updateElementIndex(element, newIndex - count, newIndex);
+ ++newIndex;
+ }
+ }
+ else if (startIndex < _elements.Count)
+ {
+ // Recycle and remove the affected elements.
+ var start = Math.Max(startIndex, 0);
+ var end = Math.Min(endIndex, _elements.Count);
+
+ for (var i = start; i < end; ++i)
+ {
+ if (_elements[i] is Control element)
+ recycleElement(element);
+ }
+
+ _elements.RemoveRange(start, end - start);
+ _sizes!.RemoveRange(start, end - start);
+
+ // If the remove started before and ended within our realized elements, then our new
+ // first index will be the index where the remove started. Mark StartU as unstable
+ // because we can't rely on it now to estimate element heights.
+ if (startIndex <= 0 && end < last)
+ {
+ _firstIndex = first = index;
+ _startUUnstable = true;
+ }
+
+ // Update the indexes of the elements after the removed range.
+ end = _elements.Count;
+ var newIndex = first + start;
+ for (var i = start; i < end; ++i)
+ {
+ if (_elements[i] is Control element)
+ updateElementIndex(element, newIndex + count, newIndex);
+ ++newIndex;
+ }
+ }
+ }
+
+ ///
+ /// Recycles all elements in response to the source collection being reset.
+ ///
+ /// A method used to recycle elements.
+ public void ItemsReset(Action recycleElement)
+ {
+ if (_elements is null || _elements.Count == 0)
+ return;
+
+ foreach (var e in _elements)
+ {
+ if (e is not null)
+ recycleElement(e);
+ }
+
+ _startU = _firstIndex = 0;
+ _elements?.Clear();
+ _sizes?.Clear();
+
+ }
+
+ ///
+ /// Recycles elements before a specific index.
+ ///
+ /// The index in the source collection of new first element.
+ /// A method used to recycle elements.
+ public void RecycleElementsBefore(int index, Action recycleElement)
+ {
+ if (index <= FirstIndex || _elements is null || _elements.Count == 0)
+ return;
+
+ if (index > LastIndex)
+ {
+ RecycleAllElements(recycleElement);
+ }
+ else
+ {
+ var endIndex = index - FirstIndex;
+
+ for (var i = 0; i < endIndex; ++i)
+ {
+ if (_elements[i] is Control e)
+ recycleElement(e, i + FirstIndex);
+ }
+
+ _elements.RemoveRange(0, endIndex);
+ _sizes!.RemoveRange(0, endIndex);
+ _firstIndex = index;
+ }
+ }
+
+ ///
+ /// Recycles elements after a specific index.
+ ///
+ /// The index in the source collection of new last element.
+ /// A method used to recycle elements.
+ public void RecycleElementsAfter(int index, Action recycleElement)
+ {
+ if (index >= LastIndex || _elements is null || _elements.Count == 0)
+ return;
+
+ if (index < FirstIndex)
+ {
+ RecycleAllElements(recycleElement);
+ }
+ else
+ {
+ var startIndex = (index + 1) - FirstIndex;
+ var count = _elements.Count;
+
+ for (var i = startIndex; i < count; ++i)
+ {
+ if (_elements[i] is Control e)
+ recycleElement(e, i + FirstIndex);
+ }
+
+ _elements.RemoveRange(startIndex, _elements.Count - startIndex);
+ _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex);
+ }
+ }
+
+ ///
+ /// Recycles all realized elements.
+ ///
+ /// A method used to recycle elements.
+ public void RecycleAllElements(Action recycleElement)
+ {
+ if (_elements is null || _elements.Count == 0)
+ return;
+
+ var i = FirstIndex;
+
+ foreach (var e in _elements)
+ {
+ if (e is not null)
+ recycleElement(e, i);
+ ++i;
+ }
+
+ _startU = _firstIndex = 0;
+ _elements?.Clear();
+ _sizes?.Clear();
+ }
+
+ ///
+ /// Resets the element list and prepares it for reuse.
+ ///
+ public void ResetForReuse()
+ {
+ _startU = _firstIndex = 0;
+ _startUUnstable = false;
+ _elements?.Clear();
+ _sizes?.Clear();
+ }
+ }
+
+}
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index e86a0de657..5a766b9cd3 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -65,8 +65,8 @@ namespace Avalonia.Controls
private bool _isInLayout;
private bool _isWaitingForViewportUpdate;
private double _lastEstimatedElementSizeU = 25;
- private RealizedElementList? _measureElements;
- private RealizedElementList? _realizedElements;
+ private RealizedStackElements? _measureElements;
+ private RealizedStackElements? _realizedElements;
private Rect _viewport = s_invalidViewport;
private Stack? _recyclePool;
private Control? _unrealizedFocusedElement;
@@ -853,447 +853,6 @@ namespace Avalonia.Controls
return snapPoint;
}
- ///
- /// Stores the realized element state for a .
- ///
- internal class RealizedElementList
- {
- private int _firstIndex;
- private List? _elements;
- private List? _sizes;
- private double _startU;
- private bool _startUUnstable;
-
- ///
- /// Gets the number of realized elements.
- ///
- public int Count => _elements?.Count ?? 0;
-
- ///
- /// Gets the index of the first realized element, or -1 if no elements are realized.
- ///
- public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1;
-
- ///
- /// Gets the index of the last realized element, or -1 if no elements are realized.
- ///
- public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1;
-
- ///
- /// Gets the elements.
- ///
- public IReadOnlyList Elements => _elements ??= new List();
-
- ///
- /// Gets the sizes of the elements on the primary axis.
- ///
- public IReadOnlyList SizeU => _sizes ??= new List();
-
- ///
- /// Gets the position of the first element on the primary axis.
- ///
- public double StartU => _startU;
-
- ///
- /// Adds a newly realized element to the collection.
- ///
- /// The index of the element.
- /// The element.
- /// The position of the elemnt on the primary axis.
- /// The size of the element on the primary axis.
- public void Add(int index, Control element, double u, double sizeU)
- {
- if (index < 0)
- throw new ArgumentOutOfRangeException(nameof(index));
-
- _elements ??= new List();
- _sizes ??= new List();
-
- if (Count == 0)
- {
- _elements.Add(element);
- _sizes.Add(sizeU);
- _startU = u;
- _firstIndex = index;
- }
- else if (index == LastIndex + 1)
- {
- _elements.Add(element);
- _sizes.Add(sizeU);
- }
- else if (index == FirstIndex - 1)
- {
- --_firstIndex;
- _elements.Insert(0, element);
- _sizes.Insert(0, sizeU);
- _startU = u;
- }
- else
- {
- throw new NotSupportedException("Can only add items to the beginning or end of realized elements.");
- }
- }
-
- ///
- /// Gets the element at the specified index, if realized.
- ///
- /// The index in the source collection of the element to get.
- /// The element if realized; otherwise null.
- public Control? GetElement(int index)
- {
- var i = index - FirstIndex;
- if (i >= 0 && i < _elements?.Count)
- return _elements[i];
- return null;
- }
-
- ///
- /// Gets the index and start U position of the element at the specified U position.
- ///
- /// The U position.
- ///
- /// A tuple containing:
- /// - The index of the item at the specified U position, or -1 if the item could not be
- /// determined
- /// - The U position of the start of the item, if determined
- ///
- public (int index, double position) GetIndexAt(double u)
- {
- if (_elements is null || _sizes is null || _startU > u || _startUUnstable)
- return (-1, 0);
-
- var index = 0;
- var position = _startU;
-
- while (index < _elements.Count)
- {
- var size = _sizes[index];
- if (double.IsNaN(size))
- break;
- if (u >= position && u < position + size)
- return (index + FirstIndex, position);
- position += size;
- ++index;
- }
-
- return (-1, 0);
- }
-
- ///
- /// Gets the element at the specified position on the primary axis, if realized.
- ///
- /// The position.
- ///
- /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the
- /// primary axis.
- ///
- public (int index, double position) GetElementAt(double position)
- {
- if (_sizes is null || position < StartU)
- return (-1, 0);
-
- var u = StartU;
- var i = FirstIndex;
-
- foreach (var size in _sizes)
- {
- var endU = u + size;
- if (position < endU)
- return (i, u);
- u += size;
- ++i;
- }
-
- return (-1, 0);
- }
-
- ///
- /// Estimates the average U size of all elements in the source collection based on the
- /// realized elements.
- ///
- ///
- /// The estimated U size of an element, or -1 if not enough information is present to make
- /// an estimate.
- ///
- public double EstimateElementSizeU()
- {
- var total = 0.0;
- var divisor = 0.0;
-
- // Start by averaging the size of the elements before the first realized element.
- if (FirstIndex >= 0 && !_startUUnstable)
- {
- total += _startU;
- divisor += FirstIndex;
- }
-
- // Average the size of the realized elements.
- if (_sizes is not null)
- {
- foreach (var size in _sizes)
- {
- if (double.IsNaN(size))
- continue;
- total += size;
- ++divisor;
- }
- }
-
- // We don't have any elements on which to base our estimate.
- if (divisor == 0 || total == 0)
- return -1;
-
- return total / divisor;
- }
-
- ///
- /// Gets the index of the specified element.
- ///
- /// The element.
- /// The index or -1 if the element is not present in the collection.
- public int GetIndex(Control element)
- {
- return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1;
- }
-
- ///
- /// Updates the elements in response to items being inserted into the source collection.
- ///
- /// The index in the source collection of the insert.
- /// The number of items inserted.
- /// A method used to update the element indexes.
- public void ItemsInserted(int index, int count, Action updateElementIndex)
- {
- if (index < 0)
- throw new ArgumentOutOfRangeException(nameof(index));
- if (_elements is null || _elements.Count == 0)
- return;
-
- // Get the index within the realized _elements collection.
- var first = FirstIndex;
- var realizedIndex = index - first;
-
- if (realizedIndex < Count)
- {
- // The insertion point affects the realized elements. Update the index of the
- // elements after the insertion point.
- var elementCount = _elements.Count;
- var start = Math.Max(realizedIndex, 0);
- var newIndex = realizedIndex + count;
-
- for (var i = start; i < elementCount; ++i)
- {
- if (_elements[i] is Control element)
- updateElementIndex(element, newIndex - count, newIndex);
- ++newIndex;
- }
-
- if (realizedIndex < 0)
- {
- // The insertion point was before the first element, update the first index.
- _firstIndex += count;
- }
- else
- {
- // The insertion point was within the realized elements, insert an empty space
- // in _elements and _sizes.
- _elements!.InsertMany(realizedIndex, null, count);
- _sizes!.InsertMany(realizedIndex, double.NaN, count);
- }
- }
- }
-
- ///
- /// Updates the elements in response to items being removed from the source collection.
- ///
- /// The index in the source collection of the remove.
- /// The number of items removed.
- /// A method used to update the element indexes.
- /// A method used to recycle elements.
- public void ItemsRemoved(
- int index,
- int count,
- Action updateElementIndex,
- Action recycleElement)
- {
- if (index < 0)
- throw new ArgumentOutOfRangeException(nameof(index));
- if (_elements is null || _elements.Count == 0)
- return;
-
- // Get the removal start and end index within the realized _elements collection.
- var first = FirstIndex;
- var last = LastIndex;
- var startIndex = index - first;
- var endIndex = (index + count) - first;
-
- if (endIndex < 0)
- {
- // 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)
- {
- if (_elements[i] is Control element)
- updateElementIndex(element, newIndex - count, newIndex);
- ++newIndex;
- }
- }
- else if (startIndex < _elements.Count)
- {
- // Recycle and remove the affected elements.
- var start = Math.Max(startIndex, 0);
- var end = Math.Min(endIndex, _elements.Count);
-
- for (var i = start; i < end; ++i)
- {
- if (_elements[i] is Control element)
- recycleElement(element);
- }
-
- _elements.RemoveRange(start, end - start);
- _sizes!.RemoveRange(start, end - start);
-
- // If the remove started before and ended within our realized elements, then our new
- // first index will be the index where the remove started. Mark StartU as unstable
- // because we can't rely on it now to estimate element heights.
- if (startIndex <= 0 && end < last)
- {
- _firstIndex = first = index;
- _startUUnstable = true;
- }
-
- // Update the indexes of the elements after the removed range.
- end = _elements.Count;
- var newIndex = first + start;
- for (var i = start; i < end; ++i)
- {
- if (_elements[i] is Control element)
- updateElementIndex(element, newIndex + count, newIndex);
- ++newIndex;
- }
- }
- }
-
- ///
- /// Recycles all elements in response to the source collection being reset.
- ///
- /// A method used to recycle elements.
- public void ItemsReset(Action recycleElement)
- {
- if (_elements is null || _elements.Count == 0)
- return;
-
- foreach (var e in _elements)
- {
- if (e is not null)
- recycleElement(e);
- }
-
- _startU = _firstIndex = 0;
- _elements?.Clear();
- _sizes?.Clear();
-
- }
-
- ///
- /// Recycles elements before a specific index.
- ///
- /// The index in the source collection of new first element.
- /// A method used to recycle elements.
- public void RecycleElementsBefore(int index, Action recycleElement)
- {
- if (index <= FirstIndex || _elements is null || _elements.Count == 0)
- return;
-
- if (index > LastIndex)
- {
- RecycleAllElements(recycleElement);
- }
- else
- {
- var endIndex = index - FirstIndex;
-
- for (var i = 0; i < endIndex; ++i)
- {
- if (_elements[i] is Control e)
- recycleElement(e, i + FirstIndex);
- }
-
- _elements.RemoveRange(0, endIndex);
- _sizes!.RemoveRange(0, endIndex);
- _firstIndex = index;
- }
- }
-
- ///
- /// Recycles elements after a specific index.
- ///
- /// The index in the source collection of new last element.
- /// A method used to recycle elements.
- public void RecycleElementsAfter(int index, Action recycleElement)
- {
- if (index >= LastIndex || _elements is null || _elements.Count == 0)
- return;
-
- if (index < FirstIndex)
- {
- RecycleAllElements(recycleElement);
- }
- else
- {
- var startIndex = (index + 1) - FirstIndex;
- var count = _elements.Count;
-
- for (var i = startIndex; i < count; ++i)
- {
- if (_elements[i] is Control e)
- recycleElement(e, i + FirstIndex);
- }
-
- _elements.RemoveRange(startIndex, _elements.Count - startIndex);
- _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex);
- }
- }
-
- ///
- /// Recycles all realized elements.
- ///
- /// A method used to recycle elements.
- public void RecycleAllElements(Action recycleElement)
- {
- if (_elements is null || _elements.Count == 0)
- return;
-
- var i = FirstIndex;
-
- foreach (var e in _elements)
- {
- if (e is not null)
- recycleElement(e, i);
- ++i;
- }
-
- _startU = _firstIndex = 0;
- _elements?.Clear();
- _sizes?.Clear();
- }
-
- ///
- /// Resets the element list and prepares it for reuse.
- ///
- public void ResetForReuse()
- {
- _startU = _firstIndex = 0;
- _startUUnstable = false;
- _elements?.Clear();
- _sizes?.Clear();
- }
- }
-
private struct MeasureViewport
{
public int firstIndex;