diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 1cddb9d295..4c9b4eaed5 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -26,6 +26,7 @@
+
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
new file mode 100644
index 0000000000..a0cbefa4d9
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
new file mode 100644
index 0000000000..a6ca27cb67
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class ItemsRepeaterPage : UserControl
+ {
+ public ItemsRepeaterPage()
+ {
+ this.InitializeComponent();
+ DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" )
+ .ToArray();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ }
+}
diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs
index c04c66a77f..8fa80e55d2 100644
--- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs
@@ -13,6 +13,7 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Repeaters")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")]
diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs
new file mode 100644
index 0000000000..173388b68e
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Avalonia.Controls.Repeaters
+{
+ public sealed class ElementFactoryGetArgs : EventArgs
+ {
+ public object Data { get; set; }
+ public IControl Parent { get; set; }
+ internal int Index { get; set; }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs
new file mode 100644
index 0000000000..3025546f67
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls.Repeaters
+{
+ public sealed class ElementFactoryRecycleArgs : EventArgs
+ {
+ public IControl Element { get; set; }
+ public IControl Parent { get; set; }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ElementManager.cs b/src/Avalonia.Controls/Repeaters/ElementManager.cs
new file mode 100644
index 0000000000..2ce0ad24fa
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ElementManager.cs
@@ -0,0 +1,426 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Text;
+using Avalonia.Controls.Utils;
+
+namespace Avalonia.Controls.Repeaters
+{
+ internal class ElementManager
+ {
+ private readonly List _realizedElements = new List();
+ private readonly List _realizedElementLayoutBounds = new List();
+ private int _firstRealizedDataIndex;
+ private VirtualizingLayoutContext _context;
+
+ private bool IsVirtualizingContext
+ {
+ get
+ {
+ if (_context != null)
+ {
+ var rect = _context.RealizationRect;
+ bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
+ return !hasInfiniteSize;
+ }
+ return false;
+ }
+ }
+
+ public void SetContext(VirtualizingLayoutContext virtualContext) => _context = virtualContext;
+
+ public void OnBeginMeasure(Orientation orientation)
+ {
+ if (_context != null)
+ {
+ if (IsVirtualizingContext)
+ {
+ // We proactively clear elements laid out outside of the realizaton
+ // rect so that they are available for reuse during the current
+ // measure pass.
+ // This is useful during fast panning scenarios in which the realization
+ // window is constantly changing and we want to reuse elements from
+ // the end that's opposite to the panning direction.
+ DiscardElementsOutsideWindow(_context.RealizationRect, orientation);
+ }
+ else
+ {
+ // If we are initialized with a non-virtualizing context, make sure that
+ // we have enough space to hold the bounds for all the elements.
+ int count = _context.ItemCount;
+ if (_realizedElementLayoutBounds.Count != count)
+ {
+ // Make sure there is enough space for the bounds.
+ // Note: We could optimize when the count becomes smaller, but keeping
+ // it always up to date is the simplest option for now.
+ _realizedElementLayoutBounds.Resize(count);
+ }
+ }
+ }
+ }
+
+ public int GetRealizedElementCount()
+ {
+ return IsVirtualizingContext ? _realizedElements.Count : _context.ItemCount;
+ }
+
+ public IControl GetAt(int realizedIndex)
+ {
+ IControl element;
+
+ if (IsVirtualizingContext)
+ {
+ if (_realizedElements[realizedIndex] == null)
+ {
+ // Sentinel. Create the element now since we need it.
+ int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
+ element = _context.GetOrCreateElementAt(
+ dataIndex,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ _realizedElements[realizedIndex] = element;
+ }
+ else
+ {
+ element = _realizedElements[realizedIndex];
+ }
+ }
+ else
+ {
+ // realizedIndex and dataIndex are the same (everything is realized)
+ element = _context.GetOrCreateElementAt(
+ realizedIndex,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ }
+
+ return element;
+ }
+
+ public void Add(IControl element, int dataIndex)
+ {
+ if (_realizedElements.Count == 0)
+ {
+ _firstRealizedDataIndex = dataIndex;
+ }
+
+ _realizedElements.Add(element);
+ _realizedElementLayoutBounds.Add(default);
+ }
+
+ public void Insert(int realizedIndex, int dataIndex, IControl element)
+ {
+ if (realizedIndex == 0)
+ {
+ _firstRealizedDataIndex = dataIndex;
+ }
+
+ _realizedElements.Insert(realizedIndex, element);
+
+ // Set bounds to an invalid rect since we do not know it yet.
+ _realizedElementLayoutBounds.Insert(realizedIndex, new Rect(-1, -1, -1, -1));
+ }
+
+ public void ClearRealizedRange(int realizedIndex, int count)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ // Clear from the edges so that ItemsRepeater can optimize on maintaining
+ // realized indices without walking through all the children every time.
+ int index = realizedIndex == 0 ? realizedIndex + i : (realizedIndex + count - 1) - i;
+ var elementRef = _realizedElements[index];
+
+ if (elementRef != null)
+ {
+ _context.RecycleElement(elementRef);
+ }
+ }
+
+ int endIndex = realizedIndex + count;
+ _realizedElements.RemoveRange(realizedIndex, endIndex - realizedIndex);
+ _realizedElementLayoutBounds.RemoveRange(realizedIndex, endIndex - realizedIndex);
+
+ if (realizedIndex == 0)
+ {
+ _firstRealizedDataIndex = _realizedElements.Count == 0 ?
+ -1 : _firstRealizedDataIndex + count;
+ }
+ }
+
+ public void DiscardElementsOutsideWindow(bool forward, int startIndex)
+ {
+ // Remove layout elements that are outside the realized range.
+ if (IsDataIndexRealized(startIndex))
+ {
+ int rangeIndex = GetRealizedRangeIndexFromDataIndex(startIndex);
+
+ if (forward)
+ {
+ ClearRealizedRange(rangeIndex, GetRealizedElementCount() - rangeIndex);
+ }
+ else
+ {
+ ClearRealizedRange(0, rangeIndex + 1);
+ }
+ }
+ }
+
+ public void ClearRealizedRange() => ClearRealizedRange(0, GetRealizedElementCount());
+
+ public Rect GetLayoutBoundsForDataIndex(int dataIndex)
+ {
+ int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
+ return _realizedElementLayoutBounds[realizedIndex];
+ }
+
+ public void SetLayoutBoundsForDataIndex(int dataIndex, in Rect bounds)
+ {
+ int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex);
+ _realizedElementLayoutBounds[realizedIndex] = bounds;
+ }
+
+ public Rect GetLayoutBoundsForRealizedIndex(int realizedIndex) => _realizedElementLayoutBounds[realizedIndex];
+
+ public void SetLayoutBoundsForRealizedIndex(int realizedIndex, in Rect bounds)
+ {
+ _realizedElementLayoutBounds[realizedIndex] = bounds;
+ }
+
+ public bool IsDataIndexRealized(int index)
+ {
+ if (IsVirtualizingContext)
+ {
+ int realizedCount = GetRealizedElementCount();
+ return
+ realizedCount > 0 &&
+ GetDataIndexFromRealizedRangeIndex(0) <= index &&
+ GetDataIndexFromRealizedRangeIndex(realizedCount - 1) >= index;
+ }
+ else
+ {
+ // Non virtualized - everything is realized
+ return index >= 0 && index < _context.ItemCount;
+ }
+ }
+
+ public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount;
+
+ public IControl GetRealizedElement(int dataIndex)
+ {
+ return IsVirtualizingContext ?
+ GetAt(GetRealizedRangeIndexFromDataIndex(dataIndex)) :
+ _context.GetOrCreateElementAt(
+ dataIndex,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ }
+
+ public void EnsureElementRealized(bool forward, int dataIndex, string layoutId)
+ {
+ if (IsDataIndexRealized(dataIndex) == false)
+ {
+ var element = _context.GetOrCreateElementAt(
+ dataIndex,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+
+ if (forward)
+ {
+ Add(element, dataIndex);
+ }
+ else
+ {
+ Insert(0, dataIndex, element);
+ }
+ }
+ }
+
+ public bool IsWindowConnected(in Rect window, Orientation orientation, bool scrollOrientationSameAsFlow)
+ {
+ bool intersects = false;
+
+ if (_realizedElementLayoutBounds.Count > 0)
+ {
+ var firstElementBounds = GetLayoutBoundsForRealizedIndex(0);
+ var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1);
+
+ var effectiveOrientation = scrollOrientationSameAsFlow ?
+ (orientation == Orientation.Vertical ? Orientation.Horizontal : Orientation.Vertical) :
+ orientation;
+
+
+ var windowStart = effectiveOrientation == Orientation.Vertical ? window.Y : window.X;
+ var windowEnd = effectiveOrientation == Orientation.Vertical ? window.Y + window.Height : window.X + window.Width;
+ var firstElementStart = effectiveOrientation == Orientation.Vertical ? firstElementBounds.Y : firstElementBounds.X;
+ var lastElementEnd = effectiveOrientation == Orientation.Vertical ? lastElementBounds.Y + lastElementBounds.Height : lastElementBounds.X + lastElementBounds.Width;
+
+ intersects =
+ firstElementStart <= windowEnd &&
+ lastElementEnd >= windowStart;
+ }
+
+ return intersects;
+ }
+
+ public void DataSourceChanged(object source, NotifyCollectionChangedEventArgs args)
+ {
+ if (_realizedElements.Count > 0)
+ {
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ {
+ OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ {
+ OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
+ OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ {
+ OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ ClearRealizedRange();
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ throw new NotImplementedException();
+ }
+ }
+ }
+
+ public int GetElementDataIndex(IControl suggestedAnchor)
+ {
+ var it = _realizedElements.IndexOf(suggestedAnchor);
+ return it != -1 ? GetDataIndexFromRealizedRangeIndex(it) : -1;
+ }
+
+ public int GetDataIndexFromRealizedRangeIndex(int rangeIndex)
+ {
+ return IsVirtualizingContext ? rangeIndex + _firstRealizedDataIndex : rangeIndex;
+ }
+
+ private int GetRealizedRangeIndexFromDataIndex(int dataIndex)
+ {
+ return IsVirtualizingContext ? dataIndex - _firstRealizedDataIndex : dataIndex;
+ }
+
+ private void DiscardElementsOutsideWindow(in Rect window, Orientation orientation)
+ {
+ // The following illustration explains the cutoff indices.
+ // We will clear all the realized elements from both ends
+ // up to the corresponding cutoff index.
+ // '-' means the element is outside the cutoff range.
+ // '*' means the element is inside the cutoff range and will be cleared.
+ //
+ // Window:
+ // |______________________________|
+ // Realization range:
+ // |*****----------------------------------*********|
+ // | |
+ // frontCutoffIndex backCutoffIndex
+ //
+ // Note that we tolerate at most one element outside of the window
+ // because the FlowLayoutAlgorithm.Generate routine stops *after*
+ // it laid out an element outside the realization window.
+ // This is also convenient because it protects the anchor
+ // during a BringIntoView operation during which the anchor may
+ // not be in the realization window (in fact, the realization window
+ // might be empty if the BringIntoView is issued before the first
+ // layout pass).
+
+ int realizedRangeSize = GetRealizedElementCount();
+ int frontCutoffIndex = -1;
+ int backCutoffIndex = realizedRangeSize;
+
+ for (int i = 0;
+ i= 0 &&
+ !Intersects(window, _realizedElementLayoutBounds[i], orientation);
+ --i)
+ {
+ --backCutoffIndex;
+ }
+
+ if (backCutoffIndex 0)
+ {
+ ClearRealizedRange(0, Math.Min(frontCutoffIndex, GetRealizedElementCount()));
+ }
+ }
+
+ private static bool Intersects(in Rect lhs, in Rect rhs, Orientation orientation)
+ {
+ var lhsStart = orientation == Orientation.Vertical ? lhs.Y : lhs.X;
+ var lhsEnd = orientation == Orientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width;
+ var rhsStart = orientation == Orientation.Vertical ? rhs.Y : rhs.X;
+ var rhsEnd = orientation == Orientation.Vertical ? rhs.Y + rhs.Height : rhs.X + rhs.Width;
+
+ return lhsEnd >= rhsStart && lhsStart <= rhsEnd;
+ }
+
+ private void OnItemsAdded(int index, int count)
+ {
+ // Using the old indices here (before it was updated by the collection change)
+ // if the insert data index is between the first and last realized data index, we need
+ // to insert items.
+ int lastRealizedDataIndex = _firstRealizedDataIndex + GetRealizedElementCount() - 1;
+ int newStartingIndex = index;
+ if (newStartingIndex > _firstRealizedDataIndex &&
+ newStartingIndex <= lastRealizedDataIndex)
+ {
+ // Inserted within the realized range
+ int insertRangeStartIndex = newStartingIndex - _firstRealizedDataIndex;
+ for (int i = 0; i < count; i++)
+ {
+ // Insert null (sentinel) here instead of an element, that way we dont
+ // end up creating a lot of elements only to be thrown out in the next layout.
+ int insertRangeIndex = insertRangeStartIndex + i;
+ int dataIndex = newStartingIndex + i;
+ // This is to keep the contiguousness of the mapping
+ Insert(insertRangeIndex, dataIndex, null);
+ }
+ }
+ else if (index <= _firstRealizedDataIndex)
+ {
+ // Items were inserted before the realized range.
+ // We need to update m_firstRealizedDataIndex;
+ _firstRealizedDataIndex += count;
+ }
+ }
+
+ private void OnItemsRemoved(int index, int count)
+ {
+ int lastRealizedDataIndex = _firstRealizedDataIndex + _realizedElements.Count - 1;
+ int startIndex = Math.Max(_firstRealizedDataIndex, index);
+ int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1);
+ bool removeAffectsFirstRealizedDataIndex = (index <= _firstRealizedDataIndex);
+
+ if (endIndex >= startIndex)
+ {
+ ClearRealizedRange(GetRealizedRangeIndexFromDataIndex(startIndex), endIndex - startIndex + 1);
+ }
+
+ if (removeAffectsFirstRealizedDataIndex &&
+ _firstRealizedDataIndex != -1)
+ {
+ _firstRealizedDataIndex -= count;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs
new file mode 100644
index 0000000000..a0ced65076
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs
@@ -0,0 +1,689 @@
+using System;
+using System.Collections.Specialized;
+
+namespace Avalonia.Controls.Repeaters
+{
+ internal class FlowLayoutAlgorithm
+ {
+ private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
+ private readonly ElementManager _elementManager = new ElementManager();
+ private Size _lastAvailableSize;
+ private double _lastItemSpacing;
+ private bool _collectionChangePending;
+ private VirtualizingLayoutContext _context;
+ private IFlowLayoutAlgorithmDelegates _algorithmCallbacks;
+ private Rect _lastExtent;
+ private int _firstRealizedDataIndexInsideRealizationWindow = -1;
+ private int _lastRealizedDataIndexInsideRealizationWindow = -1;
+
+ // If the scroll orientation is the same as the folow orientation
+ // we will only have one line since we will never wrap. In that case
+ // we do not want to align the line. We could potentially switch the
+ // meaning of line alignment in this case, but I'll hold off on that
+ // feature until someone asks for it - This is not a common scenario
+ // anyway.
+ private bool _scrollOrientationSameAsFlow;
+
+ public Rect LastExtent => _lastExtent;
+
+ private bool IsVirtualizingContext
+ {
+ get
+ {
+ if (_context != null)
+ {
+ var rect = _context.RealizationRect;
+ bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width);
+ return !hasInfiniteSize;
+ }
+ return false;
+ }
+ }
+
+ private Rect RealizationRect => IsVirtualizingContext ? _context.RealizationRect : new Rect(Size.Infinity);
+
+ public void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
+ {
+ _algorithmCallbacks = callbacks;
+ _context = context;
+ _elementManager.SetContext(context);
+ }
+
+ public void UninitializeForContext(VirtualizingLayoutContext context)
+ {
+ if (IsVirtualizingContext)
+ {
+ // This layout is about to be detached. Let go of all elements
+ // being held and remove the layout state from the context.
+ _elementManager.ClearRealizedRange();
+ }
+
+ context.LayoutState = null;
+ }
+
+ public Size Measure(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ bool isWrapping,
+ double minItemSpacing,
+ double lineSpacing,
+ Orientation orientation,
+ string layoutId)
+ {
+ _orientation.ScrollOrientation = orientation;
+
+ // If minor size is infinity, there is only one line and no need to align that line.
+ _scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize));
+ var realizationRect = RealizationRect;
+
+ var suggestedAnchorIndex = _context.RecommendedAnchorIndex;
+ if (_elementManager.IsIndexValidInData(suggestedAnchorIndex))
+ {
+ var anchorRealized = _elementManager.IsDataIndexRealized(suggestedAnchorIndex);
+ if (!anchorRealized)
+ {
+ MakeAnchor(_context, suggestedAnchorIndex, availableSize);
+ }
+ }
+
+ _elementManager.OnBeginMeasure(orientation);
+
+ int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
+ Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
+ Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId);
+ if (isWrapping && IsReflowRequired())
+ {
+ var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
+ _orientation.SetMinorStart(ref firstElementBounds, 0);
+ _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
+ Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId);
+ }
+
+ RaiseLineArranged();
+ _collectionChangePending = false;
+ _lastExtent = EstimateExtent(availableSize, layoutId);
+ SetLayoutOrigin();
+
+ return new Size(_lastExtent.Width, _lastExtent.Height);
+ }
+
+ public Size Arrange(
+ Size finalSize,
+ VirtualizingLayoutContext context,
+ LineAlignment lineAlignment,
+ string layoutId)
+ {
+ ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId);
+
+ return new Size(
+ Math.Max(finalSize.Width, _lastExtent.Width),
+ Math.Max(finalSize.Height, _lastExtent.Height));
+ }
+
+ public void OnItemsSourceChanged(
+ object source,
+ NotifyCollectionChangedEventArgs args,
+ VirtualizingLayoutContext context)
+ {
+ _elementManager.DataSourceChanged(source, args);
+ _collectionChangePending = true;
+ }
+
+ public Size MeasureElement(
+ IControl element,
+ int index,
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ var measureSize = _algorithmCallbacks.Algorithm_GetMeasureSize(index, availableSize, context);
+ element.Measure(measureSize);
+ var provisionalArrangeSize = _algorithmCallbacks.Algorithm_GetProvisionalArrangeSize(index, measureSize, element.DesiredSize, context);
+ _algorithmCallbacks.Algorithm_OnElementMeasured(element, index, availableSize, measureSize, element.DesiredSize, provisionalArrangeSize, context);
+
+ return provisionalArrangeSize;
+ }
+
+ private int GetAnchorIndex(
+ Size availableSize,
+ bool isWrapping,
+ double minItemSpacing,
+ string layoutId)
+ {
+ int anchorIndex = -1;
+ var anchorPosition= new Point();
+ var context = _context;
+
+ if (!IsVirtualizingContext)
+ {
+ // Non virtualizing host, start generating from the element 0
+ anchorIndex = context.ItemCount > 0 ? 0 : -1;
+ }
+ else
+ {
+ bool isRealizationWindowConnected = _elementManager.IsWindowConnected(RealizationRect, _orientation.ScrollOrientation, _scrollOrientationSameAsFlow);
+ // Item spacing and size in non-virtualizing direction change can cause elements to reflow
+ // and get a new column position. In that case we need the anchor to be positioned in the
+ // correct column.
+ bool needAnchorColumnRevaluation = isWrapping && (
+ _orientation.Minor(_lastAvailableSize) != _orientation.Minor(availableSize) ||
+ _lastItemSpacing != minItemSpacing ||
+ _collectionChangePending);
+
+ var suggestedAnchorIndex = _context.RecommendedAnchorIndex;
+
+ var isAnchorSuggestionValid = suggestedAnchorIndex >= 0 &&
+ _elementManager.IsDataIndexRealized(suggestedAnchorIndex);
+
+ if (isAnchorSuggestionValid)
+ {
+ anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(
+ suggestedAnchorIndex,
+ availableSize,
+ context).Index;
+
+ if (_elementManager.IsDataIndexRealized(anchorIndex))
+ {
+ var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
+ if (needAnchorColumnRevaluation)
+ {
+ // We were provided a valid anchor, but its position might be incorrect because for example it is in
+ // the wrong column. We do know that the anchor is the first element in the row, so we can force the minor position
+ // to start at 0.
+ anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
+ }
+ else
+ {
+ anchorPosition = new Point(anchorBounds.X, anchorBounds.Y);
+ }
+ }
+ else
+ {
+ // It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index
+ // which is not in the realized range. Eg. insert one item at index 0 for a grid layout.
+ // SuggestedAnchor will be 1 (used to be 0) and GetAnchorForTargetElement will return 0 (left most item in row). However 0 is not in the
+ // realized range yet. In this case we realize the gap between the target anchor and the suggested anchor.
+ int firstRealizedDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
+
+ for (int i = firstRealizedDataIndex - 1; i >= anchorIndex; --i)
+ {
+ _elementManager.EnsureElementRealized(false /*forward*/, i, layoutId);
+ }
+
+ var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(suggestedAnchorIndex);
+ anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds));
+ }
+ }
+ else if (needAnchorColumnRevaluation || !isRealizationWindowConnected)
+ {
+ // The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window
+ // but not the visible window. In that situation, we still need to produce a valid anchor.
+ var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context);
+ anchorIndex = anchorInfo.Index;
+ anchorPosition = _orientation.MinorMajorPoint(0, anchorInfo.Offset);
+ }
+ else
+ {
+ // No suggestion - just pick first in realized range
+ anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
+ var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
+ anchorPosition = new Point(firstElementBounds.X, firstElementBounds.Y);
+ }
+ }
+
+ _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex;
+ if (_elementManager.IsIndexValidInData(anchorIndex))
+ {
+ if (!_elementManager.IsDataIndexRealized(anchorIndex))
+ {
+ // Disconnected, throw everything and create new anchor
+ _elementManager.ClearRealizedRange();
+
+ var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ _elementManager.Add(anchor, anchorIndex);
+ }
+
+ var anchorElement = _elementManager.GetRealizedElement(anchorIndex);
+ var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context);
+ var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height);
+ _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds);
+ }
+ else
+ {
+ _elementManager.ClearRealizedRange();
+ }
+
+ // TODO: Perhaps we can track changes in the property setter
+ _lastAvailableSize = availableSize;
+ _lastItemSpacing = minItemSpacing;
+
+ return anchorIndex;
+ }
+
+ private void Generate(
+ GenerateDirection direction,
+ int anchorIndex,
+ Size availableSize,
+ double minItemSpacing,
+ double lineSpacing,
+ string layoutId)
+ {
+ if (anchorIndex != -1)
+ {
+ int step = (direction == GenerateDirection.Forward) ? 1 : -1;
+ int previousIndex = anchorIndex;
+ int currentIndex = anchorIndex + step;
+ var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
+ var lineOffset = _orientation.MajorStart(anchorBounds);
+ var lineMajorSize = _orientation.MajorSize(anchorBounds);
+ int countInLine = 1;
+ bool lineNeedsReposition = false;
+
+ while (_elementManager.IsIndexValidInData(currentIndex) &&
+ ShouldContinueFillingUpSpace(previousIndex, direction))
+ {
+ // Ensure layout element.
+ _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);
+ var currentElement = _elementManager.GetRealizedElement(currentIndex);
+ var desiredSize = MeasureElement(currentElement, currentIndex, availableSize, _context);
+
+ // Lay it out.
+ var previousElement = _elementManager.GetRealizedElement(previousIndex);
+ var currentBounds = new Rect(0, 0, desiredSize.Width, desiredSize.Height);
+ var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(previousIndex);
+
+ if (direction == GenerateDirection.Forward)
+ {
+ double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize));
+ if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
+ {
+ // No more space in this row. wrap to next row.
+ _orientation.SetMinorStart(ref currentBounds, 0);
+ _orientation.SetMajorStart(ref currentBounds, _orientation.MajorStart(previousElementBounds) + lineMajorSize + lineSpacing);
+
+ if (lineNeedsReposition)
+ {
+ // reposition the previous line (countInLine items)
+ for (int i = 0; i < countInLine; i++)
+ {
+ var dataIndex = currentIndex - 1 - i;
+ var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
+ _orientation.SetMajorSize(ref bounds, lineMajorSize);
+ _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
+ }
+ }
+
+ // Setup for next line.
+ lineMajorSize = _orientation.MajorSize(currentBounds);
+ lineOffset = _orientation.MajorStart(currentBounds);
+ lineNeedsReposition = false;
+ countInLine = 1;
+ }
+ else
+ {
+ // More space is available in this row.
+ _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing);
+ _orientation.SetMajorStart(ref currentBounds, lineOffset);
+ lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
+ lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
+ countInLine++;
+ }
+ }
+ else
+ {
+ // Backward
+ double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing);
+ if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace))
+ {
+ // Does not fit, wrap to the previous row
+ var availableSizeMinor = _orientation.Minor(availableSize);
+
+ _orientation.SetMinorStart(ref currentBounds, double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0);
+ _orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing);
+
+ if (lineNeedsReposition)
+ {
+ var previousLineOffset = _orientation.MajorStart(_elementManager.GetLayoutBoundsForDataIndex(currentIndex + countInLine + 1));
+ // reposition the previous line (countInLine items)
+ for (int i = 0; i < countInLine; i++)
+ {
+ var dataIndex = currentIndex + 1 + i;
+ if (dataIndex != anchorIndex)
+ {
+ var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex);
+ _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing);
+ _orientation.SetMajorSize(ref bounds, lineMajorSize);
+ _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
+ }
+ }
+ }
+
+ // Setup for next line.
+ lineMajorSize = _orientation.MajorSize(currentBounds);
+ lineOffset = _orientation.MajorStart(currentBounds);
+ lineNeedsReposition = false;
+ countInLine = 1;
+ }
+ else
+ {
+ // Fits in this row. put it in the previous position
+ _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) - _orientation.Minor(desiredSize) - minItemSpacing);
+ _orientation.SetMajorStart(ref currentBounds, lineOffset);
+ lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds));
+ lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds);
+ countInLine++;
+ }
+ }
+
+ _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds);
+ previousIndex = currentIndex;
+ currentIndex += step;
+ }
+
+ // If we did not reach the top or bottom of the extent, we realized one
+ // extra item before we knew we were outside the realization window. Do not
+ // account for that element in the indicies inside the realization window.
+ if (direction == GenerateDirection.Forward)
+ {
+ int dataCount = _context.ItemCount;
+ _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1;
+ _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow);
+ }
+ else
+ {
+ int dataCount = _context.ItemCount;
+ _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1;
+ _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow);
+ }
+
+ _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex);
+ }
+ }
+
+ private void MakeAnchor(
+ VirtualizingLayoutContext context,
+ int index,
+ Size availableSize)
+ {
+ _elementManager.ClearRealizedRange();
+ // FlowLayout requires that the anchor is the first element in the row.
+ var internalAnchor = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(index, availableSize, context);
+ //MUX_ASSERT(internalAnchor.Index <= index);
+
+ // No need to set the position of the anchor.
+ // (0,0) is fine for now since the extent can
+ // grow in any direction.
+ for (int dataIndex = internalAnchor.Index; dataIndex < index + 1; ++dataIndex)
+ {
+ var element = context.GetOrCreateElementAt(dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ element.Measure(_algorithmCallbacks.Algorithm_GetMeasureSize(dataIndex, availableSize, context));
+ _elementManager.Add(element, dataIndex);
+ }
+ }
+
+ private bool IsReflowRequired()
+ {
+ // If first element is realized and is not at the very beginning we need to reflow.
+ return
+ _elementManager.GetRealizedElementCount() > 0 &&
+ _elementManager.GetDataIndexFromRealizedRangeIndex(0) == 0 &&
+ _orientation.MinorStart(_elementManager.GetLayoutBoundsForRealizedIndex(0)) != 0;
+ }
+
+ private bool ShouldContinueFillingUpSpace(
+ int index,
+ GenerateDirection direction)
+ {
+ bool shouldContinue = false;
+ if (!IsVirtualizingContext)
+ {
+ shouldContinue = true;
+ }
+ else
+ {
+ var realizationRect = _context.RealizationRect;
+ var elementBounds = _elementManager.GetLayoutBoundsForDataIndex(index);
+
+ var elementMajorStart = _orientation.MajorStart(elementBounds);
+ var elementMajorEnd = _orientation.MajorEnd(elementBounds);
+ var rectMajorStart = _orientation.MajorStart(realizationRect);
+ var rectMajorEnd = _orientation.MajorEnd(realizationRect);
+
+ var elementMinorStart = _orientation.MinorStart(elementBounds);
+ var elementMinorEnd = _orientation.MinorEnd(elementBounds);
+ var rectMinorStart = _orientation.MinorStart(realizationRect);
+ var rectMinorEnd = _orientation.MinorEnd(realizationRect);
+
+ // Ensure that both minor and major directions are taken into consideration so that if the scrolling direction
+ // is the same as the flow direction we still stop at the end of the viewport rectangle.
+ shouldContinue =
+ (direction == GenerateDirection.Forward && elementMajorStart < rectMajorEnd && elementMinorStart < rectMinorEnd) ||
+ (direction == GenerateDirection.Backward && elementMajorEnd > rectMajorStart && elementMinorEnd > rectMinorStart);
+ }
+
+ return shouldContinue;
+ }
+
+ private Rect EstimateExtent(Size availableSize, string layoutId)
+ {
+ IControl firstRealizedElement = null;
+ Rect firstBounds = new Rect();
+ IControl lastRealizedElement = null;
+ Rect lastBounds = new Rect();
+ int firstDataIndex = -1;
+ int lastDataIndex = -1;
+
+ if (_elementManager.GetRealizedElementCount() > 0)
+ {
+ firstRealizedElement = _elementManager.GetAt(0);
+ firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
+ firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);;
+
+ int last = _elementManager.GetRealizedElementCount() - 1;
+ lastRealizedElement = _elementManager.GetAt(last);
+ lastDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(last);
+ lastBounds = _elementManager.GetLayoutBoundsForRealizedIndex(last);
+ }
+
+ Rect extent = _algorithmCallbacks.Algorithm_GetExtent(
+ availableSize,
+ _context,
+ firstRealizedElement,
+ firstDataIndex,
+ firstBounds,
+ lastRealizedElement,
+ lastDataIndex,
+ lastBounds);
+
+ return extent;
+ }
+
+ private void RaiseLineArranged()
+ {
+ var realizationRect = RealizationRect;
+ if (realizationRect.Width != 0.0f || realizationRect.Height != 0.0f)
+ {
+ int realizedElementCount = _elementManager.GetRealizedElementCount();
+ if (realizedElementCount > 0)
+ {
+ //MUX_ASSERT(_firstRealizedDataIndexInsideRealizationWindow != -1 && _lastRealizedDataIndexInsideRealizationWindow != -1);
+ int countInLine = 0;
+ var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow);
+ var currentLineOffset = _orientation.MajorStart(previousElementBounds);
+ var currentLineSize = _orientation.MajorSize(previousElementBounds);
+ for (int currentDataIndex = _firstRealizedDataIndexInsideRealizationWindow; currentDataIndex <= _lastRealizedDataIndexInsideRealizationWindow; currentDataIndex++)
+ {
+ var currentBounds = _elementManager.GetLayoutBoundsForDataIndex(currentDataIndex);
+ if (_orientation.MajorStart(currentBounds) != currentLineOffset)
+ {
+ // Staring a new line
+ _algorithmCallbacks.Algorithm_OnLineArranged(currentDataIndex - countInLine, countInLine, currentLineSize, _context);
+ countInLine = 0;
+ currentLineOffset = _orientation.MajorStart(currentBounds);
+ currentLineSize = 0;
+ }
+
+ currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
+ countInLine++;
+ previousElementBounds = currentBounds;
+ }
+
+ // Raise for the last line.
+ _algorithmCallbacks.Algorithm_OnLineArranged(_lastRealizedDataIndexInsideRealizationWindow - countInLine + 1, countInLine, currentLineSize, _context);
+ }
+ }
+ }
+
+ private void ArrangeVirtualizingLayout(
+ Size finalSize,
+ LineAlignment lineAlignment,
+ string layoutId)
+ {
+ // Walk through the realized elements one line at a time and
+ // align them, Then call element.Arrange with the arranged bounds.
+ int realizedElementCount = _elementManager.GetRealizedElementCount();
+ if (realizedElementCount > 0)
+ {
+ var countInLine = 1;
+ var previousElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
+ var currentLineOffset = _orientation.MajorStart(previousElementBounds);
+ var spaceAtLineStart = _orientation.MinorStart(previousElementBounds);
+ var spaceAtLineEnd = 0.0;
+ var currentLineSize = _orientation.MajorSize(previousElementBounds);
+ for (int i = 1; i < realizedElementCount; i++)
+ {
+ var currentBounds = _elementManager.GetLayoutBoundsForRealizedIndex(i);
+ if (_orientation.MajorStart(currentBounds) != currentLineOffset)
+ {
+ spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
+ PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId);
+ spaceAtLineStart = _orientation.MinorStart(currentBounds);
+ countInLine = 0;
+ currentLineOffset = _orientation.MajorStart(currentBounds);
+ currentLineSize = 0;
+ }
+
+ countInLine++; // for current element
+ currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds));
+ previousElementBounds = currentBounds;
+ }
+
+ // Last line - potentially have a property to customize
+ // aligning the last line or not.
+ if (countInLine > 0)
+ {
+ var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds);
+ PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId);
+ }
+ }
+ }
+
+ // Align elements within a line. Note that this does not modify LayoutBounds. So if we get
+ // repeated measures, the LayoutBounds remain the same in each layout.
+ private void PerformLineAlignment(
+ int lineStartIndex,
+ int countInLine,
+ double spaceAtLineStart,
+ double spaceAtLineEnd,
+ double lineSize,
+ LineAlignment lineAlignment,
+ string layoutId)
+ {
+ for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex)
+ {
+ var bounds = _elementManager.GetLayoutBoundsForRealizedIndex(rangeIndex);
+ _orientation.SetMajorSize(ref bounds, lineSize);
+
+ if (!_scrollOrientationSameAsFlow)
+ {
+ // Note: Space at start could potentially be negative
+ if (spaceAtLineStart != 0 || spaceAtLineEnd != 0)
+ {
+ var totalSpace = spaceAtLineStart + spaceAtLineEnd;
+ var minorStart = _orientation.MinorStart(bounds);
+ switch (lineAlignment)
+ {
+ case LineAlignment.Start:
+ {
+ _orientation.SetMinorStart(ref bounds, minorStart - spaceAtLineStart);
+ break;
+ }
+
+ case LineAlignment.End:
+ {
+ _orientation.SetMinorStart(ref bounds, minorStart + spaceAtLineEnd);
+ break;
+ }
+
+ case LineAlignment.Center:
+ {
+ _orientation.SetMinorStart(ref bounds, (minorStart - spaceAtLineStart) + (totalSpace / 2));
+ break;
+ }
+
+ case LineAlignment.SpaceAround:
+ {
+ var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine * 2) : 0;
+ _orientation.SetMinorStart(
+ ref bounds,
+ (minorStart - spaceAtLineStart) + (interItemSpace * ((rangeIndex - lineStartIndex + 1) * 2 - 1)));
+ break;
+ }
+
+ case LineAlignment.SpaceBetween:
+ {
+ var interItemSpace = countInLine > 1 ? totalSpace / (countInLine - 1) : 0;
+ _orientation.SetMinorStart(
+ ref bounds,
+ (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex)));
+ break;
+ }
+
+ case LineAlignment.SpaceEvenly:
+ {
+ var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine + 1) : 0;
+ _orientation.SetMinorStart(
+ ref bounds,
+ (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex + 1)));
+ break;
+ }
+ }
+ }
+ }
+
+ bounds = bounds.Translate(-_lastExtent.Position);
+ var element = _elementManager.GetAt(rangeIndex);
+ element.Arrange(bounds);
+ }
+ }
+
+ void SetLayoutOrigin()
+ {
+ if (IsVirtualizingContext)
+ {
+ _context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y);
+ }
+ else
+ {
+ // Should have 0 origin for non-virtualizing layout since we always start from
+ // the first item
+ //MUX_ASSERT(m_lastExtent.X == 0 && m_lastExtent.Y == 0);
+ }
+ }
+
+ public enum LineAlignment
+ {
+ Start,
+ Center,
+ End,
+ SpaceAround,
+ SpaceBetween,
+ SpaceEvenly,
+ }
+
+ private enum GenerateDirection
+ {
+ Forward,
+ Backward,
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs
new file mode 100644
index 0000000000..e27f00540c
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls.Repeaters
+{
+ internal struct FlowLayoutAnchorInfo
+ {
+ public int Index { get; set; }
+ public double Offset { get; set; }
+ }
+
+ internal interface IFlowLayoutAlgorithmDelegates
+ {
+ Size Algorithm_GetMeasureSize(int index, Size availableSize, VirtualizingLayoutContext context);
+ Size Algorithm_GetProvisionalArrangeSize(int index, Size measureSize, Size desiredSize, VirtualizingLayoutContext context);
+ bool Algorithm_ShouldBreakLine(int index, double remainingSpace);
+ FlowLayoutAnchorInfo Algorithm_GetAnchorForRealizationRect(Size availableSize, VirtualizingLayoutContext context);
+ FlowLayoutAnchorInfo Algorithm_GetAnchorForTargetElement(int targetIndex, Size availableSize, VirtualizingLayoutContext context);
+ Rect Algorithm_GetExtent(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ IControl firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ IControl lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds);
+ void Algorithm_OnElementMeasured(
+ IControl element,
+ int index,
+ Size availableSize,
+ Size measureSize,
+ Size desiredSize,
+ Size provisionalArrangeSize,
+ VirtualizingLayoutContext context);
+ void Algorithm_OnLineArranged(
+ int startIndex,
+ int countInLine,
+ double lineSize,
+ VirtualizingLayoutContext context);
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs b/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs
new file mode 100644
index 0000000000..55ce6c28ca
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls.Repeaters
+{
+ internal interface ITrackerHandleManager
+ {
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs
new file mode 100644
index 0000000000..c26d73c02c
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Controls.Templates;
+
+namespace Avalonia.Controls.Repeaters
+{
+ internal class ItemTemplateWrapper
+ {
+ private readonly IDataTemplate _dataTemplate;
+
+ public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
+
+ public IControl GetElement(ElementFactoryGetArgs args)
+ {
+ var selectedTemplate = _dataTemplate;
+ var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
+ IControl element = null;
+
+ if (recyclePool != null)
+ {
+ // try to get an element from the recycle pool.
+ element = recyclePool.TryGetElement(string.Empty, args.Parent);
+ }
+
+ if (element == null)
+ {
+ // no element was found in recycle pool, create a new element
+ element = selectedTemplate.Build(args.Data);
+
+ // Associate template with element
+ element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate);
+ }
+
+ return element;
+ }
+
+ public void RecycleElement(ElementFactoryRecycleArgs args)
+ {
+ var element = args.Element;
+ var selectedTemplate = _dataTemplate;
+ var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
+ if (recyclePool == null)
+ {
+ // No Recycle pool in the template, create one.
+ recyclePool = new RecyclePool();
+ RecyclePool.SetPoolInstance(selectedTemplate, recyclePool);
+ }
+
+ recyclePool.PutElement(args.Element, "" /* key */, args.Parent);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs
new file mode 100644
index 0000000000..9d71685b11
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs
@@ -0,0 +1,631 @@
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Repeaters
+{
+ public class ItemsRepeater : Panel
+ {
+ public static readonly AvaloniaProperty HorizontalCacheLengthProperty =
+ AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0);
+ public static readonly StyledProperty ItemTemplateProperty =
+ ItemsControl.ItemTemplateProperty.AddOwner();
+ public static readonly DirectProperty ItemsProperty =
+ ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v);
+ public static readonly AvaloniaProperty LayoutProperty =
+ AvaloniaProperty.Register(nameof(Layout), new StackLayout());
+ public static readonly AvaloniaProperty VerticalCacheLengthProperty =
+ AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0);
+ private static readonly AttachedProperty VirtualizationInfoProperty =
+ AvaloniaProperty.RegisterAttached("VirtualizationInfo");
+
+ internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1);
+ internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0);
+
+ private readonly ViewManager _viewManager;
+ private readonly ViewportManager _viewportManager;
+ private IEnumerable _items;
+ private VirtualizingLayoutContext _layoutContext;
+ private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
+ private Size _lastAvailableSize;
+ private bool _isLayoutInProgress;
+ private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
+ private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
+ private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
+
+ public ItemsRepeater()
+ {
+ _viewManager = new ViewManager(this);
+ _viewportManager = new ViewportManager(this);
+ KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once);
+ OnLayoutChanged(null, Layout);
+ }
+
+ static ItemsRepeater()
+ {
+ ClipToBoundsProperty.OverrideDefaultValue(true);
+ }
+
+ public Layout Layout
+ {
+ get => GetValue(LayoutProperty);
+ set => SetValue(LayoutProperty, value);
+ }
+
+ public IEnumerable Items
+ {
+ get => _items;
+ set => SetAndRaise(ItemsProperty, ref _items, value);
+ }
+
+ public IDataTemplate ItemTemplate
+ {
+ get => GetValue(ItemTemplateProperty);
+ set => SetValue(ItemTemplateProperty, value);
+ }
+
+ public double HorizontalCacheLength
+ {
+ get => GetValue(HorizontalCacheLengthProperty);
+ set => SetValue(HorizontalCacheLengthProperty, value);
+ }
+
+ public double VerticalCacheLength
+ {
+ get => GetValue(VerticalCacheLengthProperty);
+ set => SetValue(VerticalCacheLengthProperty, value);
+ }
+
+ public ItemsSourceView ItemsSourceView { get; private set; }
+
+ internal ItemTemplateWrapper ItemTemplateShim { get; set; }
+ internal Point LayoutOrigin { get; set; }
+ internal object LayoutState { get; set; }
+ internal IControl MadeAnchor => _viewportManager.MadeAnchor;
+ internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow();
+ internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor;
+
+ private bool IsProcessingCollectionChange => _processingItemsSourceChange != null;
+
+ private LayoutContext LayoutContext
+ {
+ get
+ {
+ if (_layoutContext == null)
+ {
+ _layoutContext = new RepeaterLayoutContext(this);
+ }
+
+ return _layoutContext;
+ }
+ }
+
+ public event EventHandler ElementClearing;
+ public event EventHandler ElementIndexChanged;
+ public event EventHandler ElementPrepared;
+
+ public int GetElementIndex(IControl element) => GetElementIndexImpl(element);
+
+ public IControl TryGetElement(int index) => GetElementFromIndexImpl(index);
+
+ public void PinElement(IControl element) => _viewManager.UpdatePin(element, true);
+
+ public void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false);
+
+ public IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index);
+
+ internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element)
+ {
+ var value = element.GetValue(VirtualizationInfoProperty);
+ return value;
+ }
+
+ internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element)
+ {
+ if (TryGetVirtualizationInfo(element) != null)
+ {
+ throw new InvalidOperationException("VirtualizationInfo already created.");
+ }
+
+ var result = new VirtualizationInfo();
+ element.SetValue(VirtualizationInfoProperty, result);
+ return result;
+ }
+
+ internal static VirtualizationInfo GetVirtualizationInfo(IControl element)
+ {
+ var result = element.GetValue(VirtualizationInfoProperty);
+
+ if (result == null)
+ {
+ result = new VirtualizationInfo();
+ element.SetValue(VirtualizationInfoProperty, result);
+ }
+
+ return result;
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ if (_isLayoutInProgress)
+ {
+ throw new AvaloniaInternalException("Reentrancy detected during layout.");
+ }
+
+ if (IsProcessingCollectionChange)
+ {
+ throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
+ }
+
+ _viewportManager.OnOwnerMeasuring();
+
+ _isLayoutInProgress = true;
+
+ try
+ {
+ _viewManager.PrunePinnedElements();
+ var extent = new Rect();
+ var desiredSize = new Size();
+ var layout = Layout;
+
+ if (layout != null)
+ {
+ var layoutContext = GetLayoutContext();
+
+ desiredSize = layout.Measure(layoutContext, availableSize);
+ extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height);
+
+ // Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not
+ // call GetElementAt(index).
+ foreach (var element in Children)
+ {
+ var virtInfo = GetVirtualizationInfo(element);
+
+ if (virtInfo.Owner == ElementOwner.Layout &&
+ virtInfo.AutoRecycleCandidate &&
+ !virtInfo.KeepAlive)
+ {
+ ClearElementImpl(element);
+ }
+ }
+ }
+
+ _viewportManager.SetLayoutExtent(extent);
+ _lastAvailableSize = availableSize;
+ return desiredSize;
+ }
+ finally
+ {
+ _isLayoutInProgress = false;
+ }
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ if (_isLayoutInProgress)
+ {
+ throw new AvaloniaInternalException("Reentrancy detected during layout.");
+ }
+
+ if (IsProcessingCollectionChange)
+ {
+ throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
+ }
+
+ _isLayoutInProgress = true;
+
+ try
+ {
+ var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default;
+
+ // The view manager might clear elements during this call.
+ // That's why we call it before arranging cleared elements
+ // off screen.
+ _viewManager.OnOwnerArranged();
+
+ foreach (var element in Children)
+ {
+ var virtInfo = GetVirtualizationInfo(element);
+ virtInfo.KeepAlive = false;
+
+ if (virtInfo.Owner == ElementOwner.ElementFactory ||
+ virtInfo.Owner == ElementOwner.PinnedPool)
+ {
+ // Toss it away. And arrange it with size 0 so that XYFocus won't use it.
+ element.Arrange(new Rect(
+ ClearedElementsArrangePosition.X - element.DesiredSize.Width,
+ ClearedElementsArrangePosition.Y - element.DesiredSize.Height,
+ 0,
+ 0));
+ }
+ else
+ {
+ var newBounds = element.Bounds;
+
+ //if (virtInfo.ArrangeBounds != ItemsRepeater.InvalidRect &&
+ // newBounds != virtInfo.ArrangeBounds)
+ //{
+ // _animationManager.OnElementBoundsChanged(element, virtInfo.ArrangeBounds, newBounds);
+ //}
+
+ virtInfo.ArrangeBounds = newBounds;
+ }
+ }
+
+ _viewportManager.OnOwnerArranged();
+ //_animationManager.OnOwnerArranged();
+
+ return arrangeSize;
+ }
+ finally
+ {
+ _isLayoutInProgress = false;
+ }
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ InvalidateMeasure();
+ _viewportManager.ResetScrollers();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _viewportManager.ResetScrollers();
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+ {
+ var property = args.Property;
+
+ if (property == ItemsProperty)
+ {
+ var newValue = (IEnumerable)args.NewValue;
+ var newDataSource = newValue as ItemsSourceView;
+ if (newValue != null && newDataSource == null)
+ {
+ newDataSource = new ItemsSourceView(newValue);
+ }
+
+ OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
+ }
+ else if (property == ItemTemplateProperty)
+ {
+ OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue);
+ }
+ else if (property == LayoutProperty)
+ {
+ OnLayoutChanged((Layout)args.OldValue, (Layout)args.NewValue);
+ }
+ //else if (property == AnimatorProperty)
+ //{
+ // OnAnimatorChanged((ElementAnimator)args.OldValue, (ElementAnimator)args.NewValue);
+ //}
+ else if (property == HorizontalCacheLengthProperty)
+ {
+ _viewportManager.HorizontalCacheLength = (double)args.NewValue;
+ }
+ else if (property == VerticalCacheLengthProperty)
+ {
+ _viewportManager.VerticalCacheLength = (double)args.NewValue;
+ }
+ else
+ {
+ base.OnPropertyChanged(args);
+ }
+ }
+
+ internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
+ {
+ var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle);
+ return element;
+ }
+
+ internal void ClearElementImpl(IControl element)
+ {
+ // Clearing an element due to a collection change
+ // is more strict in that pinned elements will be forcibly
+ // unpinned and sent back to the view generator.
+ var isClearedDueToCollectionChange =
+ _processingItemsSourceChange != null &&
+ (_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove ||
+ _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace ||
+ _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset);
+
+ _viewManager.ClearElement(element, isClearedDueToCollectionChange);
+ _viewportManager.OnElementCleared(element);
+ }
+
+ private int GetElementIndexImpl(IControl element)
+ {
+ var virtInfo = TryGetVirtualizationInfo(element);
+ return _viewManager.GetElementIndex(virtInfo);
+ }
+
+ private IControl GetElementFromIndexImpl(int index)
+ {
+ IControl result = null;
+
+ var children = Children;
+ for (var i = 0; i < children.Count && result == null; ++i)
+ {
+ var element = children[i];
+ var virtInfo = TryGetVirtualizationInfo(element);
+ if (virtInfo?.IsRealized == true && virtInfo.Index == index)
+ {
+ result = element;
+ }
+ }
+
+ return result;
+ }
+
+ private IControl GetOrCreateElementImpl(int index)
+ {
+ if (index >= 0 && index >= ItemsSourceView.Count)
+ {
+ throw new ArgumentException("Argument index is invalid.", "index");
+ }
+
+ if (_isLayoutInProgress)
+ {
+ throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout.");
+ }
+
+ var element = GetElementFromIndexImpl(index);
+ bool isAnchorOutsideRealizedRange = element == null;
+
+ if (isAnchorOutsideRealizedRange)
+ {
+ if (Layout == null)
+ {
+ throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout.");
+ }
+
+ element = GetLayoutContext().GetOrCreateElementAt(index);
+ element.Measure(Size.Infinity);
+ }
+
+ _viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange);
+ InvalidateMeasure();
+
+ return element;
+ }
+
+ internal void OnElementPrepared(IControl element, int index)
+ {
+ _viewportManager.OnElementPrepared(element);
+ if (ElementPrepared != null)
+ {
+ if (_elementPreparedArgs == null)
+ {
+ _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);
+ }
+ else
+ {
+ _elementPreparedArgs.Update(element, index);
+ }
+
+ ElementPrepared(this, _elementPreparedArgs);
+ }
+ }
+
+ internal void OnElementClearing(IControl element)
+ {
+ if (ElementClearing != null)
+ {
+ if (_elementClearingArgs == null)
+ {
+ _elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element);
+ }
+ else
+ {
+ _elementClearingArgs.Update(element);
+ }
+
+ ElementClearing(this, _elementClearingArgs);
+ }
+ }
+
+ internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
+ {
+ if (ElementIndexChanged != null)
+ {
+ if (_elementIndexChangedArgs == null)
+ {
+ _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex);
+ }
+ else
+ {
+ _elementIndexChangedArgs.Update(element, oldIndex, newIndex);
+ }
+
+ ElementIndexChanged(this, _elementIndexChangedArgs);
+ }
+ }
+
+ private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)
+ {
+ if (_isLayoutInProgress)
+ {
+ throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
+ }
+
+ ItemsSourceView?.Dispose();
+ ItemsSourceView = newValue;
+
+ if (oldValue != null)
+ {
+ oldValue.CollectionChanged -= OnItemsSourceViewChanged;
+ }
+
+ if (newValue != null)
+ {
+ newValue.CollectionChanged += OnItemsSourceViewChanged;
+ }
+
+ if (Layout != null)
+ {
+ if (Layout is VirtualizingLayout virtualLayout)
+ {
+ var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args);
+ }
+ else if (Layout is NonVirtualizingLayout nonVirtualLayout)
+ {
+ // Walk through all the elements and make sure they are cleared for
+ // non-virtualizing layouts.
+ foreach (var element in Children)
+ {
+ if (GetVirtualizationInfo(element).IsRealized)
+ {
+ ClearElementImpl(element);
+ }
+ }
+ }
+
+ InvalidateMeasure();
+ }
+ }
+
+ private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue)
+ {
+ if (_isLayoutInProgress && oldValue != null)
+ {
+ throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout.");
+ }
+
+ // Since the ItemTemplate has changed, we need to re-evaluate all the items that
+ // have already been created and are now in the tree. The easiest way to do that
+ // would be to do a reset.. Note that this has to be done before we change the template
+ // so that the cleared elements go back into the old template.
+ if (Layout != null)
+ {
+ if (Layout is VirtualizingLayout virtualLayout)
+ {
+ var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ _processingItemsSourceChange = args;
+
+ try
+ {
+ virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args);
+ }
+ finally
+ {
+ _processingItemsSourceChange = null;
+ }
+ }
+ else if (Layout is NonVirtualizingLayout nonVirtualLayout)
+ {
+ // Walk through all the elements and make sure they are cleared for
+ // non-virtualizing layouts.
+ foreach (var element in Children)
+ {
+ if (GetVirtualizationInfo(element).IsRealized)
+ {
+ ClearElementImpl(element);
+ }
+ }
+ }
+ }
+
+ ItemTemplateShim = new ItemTemplateWrapper(newValue);
+
+ InvalidateMeasure();
+ }
+
+ private void OnLayoutChanged(Layout oldValue, Layout newValue)
+ {
+ if (_isLayoutInProgress)
+ {
+ throw new InvalidOperationException("Layout cannot be changed during layout.");
+ }
+
+ _viewManager.OnLayoutChanging();
+
+ if (oldValue != null)
+ {
+ oldValue.UninitializeForContext(LayoutContext);
+ oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
+ oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout;
+
+ // Walk through all the elements and make sure they are cleared
+ foreach (var element in Children)
+ {
+ if (GetVirtualizationInfo(element).IsRealized)
+ {
+ ClearElementImpl(element);
+ }
+ }
+
+ LayoutState = null;
+ }
+
+ if (newValue != null)
+ {
+ newValue.InitializeForContext(LayoutContext);
+ newValue.MeasureInvalidated += InvalidateMeasureForLayout;
+ newValue.ArrangeInvalidated += InvalidateArrangeForLayout;
+ }
+
+ bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;
+ _viewportManager.OnLayoutChanged(isVirtualizingLayout);
+ InvalidateMeasure();
+ }
+
+ private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args)
+ {
+ if (_isLayoutInProgress)
+ {
+ // Bad things will follow if the data changes while we are in the middle of a layout pass.
+ throw new InvalidOperationException("Changes in data source are not allowed during layout.");
+ }
+
+ if (IsProcessingCollectionChange)
+ {
+ throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source.");
+ }
+
+ _processingItemsSourceChange = args;
+
+ try
+ {
+ //_animationManager.OnItemsSourceChanged(sender, args);
+ _viewManager.OnItemsSourceChanged(sender, args);
+
+ if (Layout != null)
+ {
+ if (Layout is VirtualizingLayout virtualLayout)
+ {
+ virtualLayout.OnItemsChangedCore(GetLayoutContext(), sender, args);
+ }
+ else
+ {
+ // NonVirtualizingLayout
+ InvalidateMeasure();
+ }
+ }
+ }
+ finally
+ {
+ _processingItemsSourceChange = null;
+ }
+ }
+
+ private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure();
+
+ private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange();
+
+ private VirtualizingLayoutContext GetLayoutContext()
+ {
+ if (_layoutContext == null)
+ {
+ _layoutContext = new RepeaterLayoutContext(this);
+ }
+
+ return _layoutContext;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs
new file mode 100644
index 0000000000..20e62bc3b8
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Avalonia.Controls.Repeaters
+{
+ public class ItemsRepeaterElementClearingEventArgs : EventArgs
+ {
+ internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element;
+ public IControl Element { get; private set; }
+ internal void Update(IControl element) => Element = element;
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs
new file mode 100644
index 0000000000..c81c997bd4
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Avalonia.Controls.Repeaters
+{
+ public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
+ {
+ internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex)
+ {
+ Element = element;
+ NewIndex = newIndex;
+ OldIndex = oldIndex;
+ }
+
+ public IControl Element { get; private set; }
+
+ public int NewIndex { get; private set; }
+
+ public int OldIndex { get; private set; }
+
+ internal void Update(IControl element, int newIndex, int oldIndex)
+ {
+ Element = element;
+ NewIndex = newIndex;
+ OldIndex = oldIndex;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs
new file mode 100644
index 0000000000..3180058fae
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls.Repeaters
+{
+ public class ItemsRepeaterElementPreparedEventArgs
+ {
+ internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index)
+ {
+ Element = element;
+ Index = index;
+ }
+
+ public IControl Element { get; private set; }
+
+ public int Index { get; private set; }
+
+ internal void Update(IControl element, int index)
+ {
+ Element = element;
+ Index = index;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs
new file mode 100644
index 0000000000..0d21af0975
--- /dev/null
+++ b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+
+namespace Avalonia.Controls.Repeaters
+{
+ public class ItemsSourceView : INotifyCollectionChanged, IDisposable
+ {
+ private readonly IList _inner;
+ private INotifyCollectionChanged _notifyCollectionChanged;
+ private int _cachedSize = -1;
+
+ public ItemsSourceView(IEnumerable source)
+ {
+ Contract.Requires(source != null);
+
+ _inner = source as IList;
+
+ if (_inner == null && source is IEnumerable