diff --git a/.editorconfig b/.editorconfig
index 5f08d1e940..f6bce9cb76 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
+space_within_single_line_array_initializer_braces = true
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false
# Xaml files
[*.xaml]
-indent_size = 4
+indent_size = 2
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 8699508320..5f24c8062e 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..dfe8be2cec
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
@@ -0,0 +1,25 @@
+
+
+
+ ItemsRepeater
+ A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization.
+
+
+
+ Stack - Vertical
+ Stack - Horizontal
+ UniformGrid - Vertical
+ UniformGrid - Horizontal
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
new file mode 100644
index 0000000000..214de89253
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
@@ -0,0 +1,71 @@
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public class ItemsRepeaterPage : UserControl
+ {
+ private ItemsRepeater _repeater;
+ private ScrollViewer _scroller;
+
+ public ItemsRepeaterPage()
+ {
+ this.InitializeComponent();
+ _repeater = this.FindControl("repeater");
+ _scroller = this.FindControl("scroller");
+ DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void LayoutChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_repeater == null)
+ {
+ return;
+ }
+
+ var comboBox = (ComboBox)sender;
+
+ switch (comboBox.SelectedIndex)
+ {
+ case 0:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _repeater.Layout = new StackLayout { Orientation = Orientation.Vertical };
+ break;
+ case 1:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal };
+ break;
+ case 2:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _repeater.Layout = new UniformGridLayout
+ {
+ Orientation = Orientation.Vertical,
+ MinItemWidth = 200,
+ MinItemHeight = 200,
+ };
+ break;
+ case 3:
+ _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
+ _repeater.Layout = new UniformGridLayout
+ {
+ Orientation = Orientation.Horizontal,
+ MinItemWidth = 200,
+ MinItemHeight = 200,
+ };
+ break;
+ }
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
index 28cb84dad0..adf0345a70 100644
--- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
@@ -1,11 +1,10 @@
using System.Reactive;
using Avalonia.Controls.Notifications;
-using Avalonia.Diagnostics.ViewModels;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
- class MainWindowViewModel : ViewModelBase
+ class MainWindowViewModel : ReactiveObject
{
private IManagedNotificationManager _notificationManager;
diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
index 80e0fb2586..93fe09a156 100644
--- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
+++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
@@ -9,6 +9,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI.Legacy;
using ReactiveUI;
+using Avalonia.Layout;
namespace VirtualizationDemo.ViewModels
{
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index a3d5803ab0..0e2f0feada 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -163,6 +163,37 @@ namespace Avalonia
SetValue(property, AvaloniaProperty.UnsetValue);
}
+ ///
+ /// Compares two objects using reference equality.
+ ///
+ /// The object to compare.
+ ///
+ /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
+ ///
+ /// - AvaloniaObjects are by their nature mutable
+ /// - The presence of attached properties means that the semantics of equality are
+ /// difficult to define
+ ///
+ /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
+ /// this.
+ ///
+ public sealed override bool Equals(object obj) => base.Equals(obj);
+
+ ///
+ /// Gets the hash code for the object.
+ ///
+ ///
+ /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
+ ///
+ /// - AvaloniaObjects are by their nature mutable
+ /// - The presence of attached properties means that the semantics of equality are
+ /// difficult to define
+ ///
+ /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
+ /// this.
+ ///
+ public sealed override int GetHashCode() => base.GetHashCode();
+
///
/// Gets a value.
///
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index bcd12fbfbb..490a724eda 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -24,6 +24,7 @@ using System.Linq;
using Avalonia.Input.Platform;
using System.ComponentModel.DataAnnotations;
using Avalonia.Controls.Utils;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 58b4324a3e..d0804107b3 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs
index 304a760216..28b9b3a38f 100644
--- a/src/Avalonia.Controls/GridSplitter.cs
+++ b/src/Avalonia.Controls/GridSplitter.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs
new file mode 100644
index 0000000000..6b5cb2ee25
--- /dev/null
+++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs
@@ -0,0 +1,9 @@
+namespace Avalonia.Controls
+{
+ public interface IScrollAnchorProvider
+ {
+ IControl CurrentAnchor { get; }
+ void RegisterAnchorCandidate(IControl element);
+ void UnregisterAnchorCandidate(IControl element);
+ }
+}
diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs
index b60a97e1c8..6ec97aa04e 100644
--- a/src/Avalonia.Controls/Menu.cs
+++ b/src/Avalonia.Controls/Menu.cs
@@ -5,6 +5,7 @@ using Avalonia.Controls.Platform;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
index 46da8fe3f8..ae52e733b7 100644
--- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
@@ -8,6 +8,7 @@ using System.Reactive.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
+using Avalonia.Layout;
namespace Avalonia.Controls.Presenters
{
diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs
index e1b3061b54..c6119e89dc 100644
--- a/src/Avalonia.Controls/Primitives/ScrollBar.cs
+++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs
@@ -7,6 +7,7 @@ using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Input;
+using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 188685f796..c8c15bc079 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -333,6 +333,11 @@ namespace Avalonia.Controls.Primitives
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(Items, SelectedItem);
+
+ if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
+ {
+ SelectedIndex = 0;
+ }
break;
}
}
diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs
index a61757e628..ec0dbd124c 100644
--- a/src/Avalonia.Controls/Primitives/TabStrip.cs
+++ b/src/Avalonia.Controls/Primitives/TabStrip.cs
@@ -4,6 +4,7 @@
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Input;
+using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{
diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs
index c96fea6c25..21a7dd68f8 100644
--- a/src/Avalonia.Controls/Primitives/Track.cs
+++ b/src/Avalonia.Controls/Primitives/Track.cs
@@ -3,6 +3,7 @@
using System;
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.Metadata;
namespace Avalonia.Controls.Primitives
diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs
index a0f51099cd..29e3a17f74 100644
--- a/src/Avalonia.Controls/ProgressBar.cs
+++ b/src/Avalonia.Controls/ProgressBar.cs
@@ -3,6 +3,7 @@
using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
@@ -33,8 +34,8 @@ namespace Avalonia.Controls
static ProgressBar()
{
- PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
- PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
+ PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
+ PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged);
@@ -120,4 +121,4 @@ namespace Avalonia.Controls
UpdateIndicator(Bounds.Size);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
new file mode 100644
index 0000000000..04d859c742
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
@@ -0,0 +1,54 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using Avalonia.Controls.Templates;
+
+namespace Avalonia.Controls
+{
+ internal class ItemTemplateWrapper
+ {
+ private readonly IDataTemplate _dataTemplate;
+
+ public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
+
+ public IControl GetElement(IControl parent, object data)
+ {
+ 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, parent);
+ }
+
+ if (element == null)
+ {
+ // no element was found in recycle pool, create a new element
+ element = selectedTemplate.Build(data);
+
+ // Associate template with element
+ element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate);
+ }
+
+ return element;
+ }
+
+ public void RecycleElement(IControl parent, IControl 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(element, "" /* key */, parent);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
new file mode 100644
index 0000000000..44783e2c97
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
@@ -0,0 +1,724 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+using Avalonia.Layout;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Represents a data-driven collection control that incorporates a flexible layout system,
+ /// custom views, and virtualization.
+ ///
+ public class ItemsRepeater : Panel
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty HorizontalCacheLengthProperty =
+ AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ItemTemplateProperty =
+ ItemsControl.ItemTemplateProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ItemsProperty =
+ ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AvaloniaProperty LayoutProperty =
+ AvaloniaProperty.Register(nameof(Layout), new StackLayout());
+
+ ///
+ /// Defines the property.
+ ///
+ 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 bool _isLayoutInProgress;
+ private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
+ private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
+ private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ItemsRepeater()
+ {
+ _viewManager = new ViewManager(this);
+ _viewportManager = new ViewportManager(this);
+ KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once);
+ OnLayoutChanged(null, Layout);
+ }
+
+ static ItemsRepeater()
+ {
+ ClipToBoundsProperty.OverrideDefaultValue(true);
+ }
+
+ ///
+ /// Gets or sets the layout used to size and position elements in the ItemsRepeater.
+ ///
+ ///
+ /// The layout used to size and position elements. The default is a StackLayout with
+ /// vertical orientation.
+ ///
+ public AttachedLayout Layout
+ {
+ get => GetValue(LayoutProperty);
+ set => SetValue(LayoutProperty, value);
+ }
+
+ ///
+ /// Gets or sets an object source used to generate the content of the ItemsRepeater.
+ ///
+ public IEnumerable Items
+ {
+ get => _items;
+ set => SetAndRaise(ItemsProperty, ref _items, value);
+ }
+
+ ///
+ /// Gets or sets the template used to display each item.
+ ///
+ public IDataTemplate ItemTemplate
+ {
+ get => GetValue(ItemTemplateProperty);
+ set => SetValue(ItemTemplateProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value that indicates the size of the buffer used to realize items when
+ /// panning or scrolling horizontally.
+ ///
+ public double HorizontalCacheLength
+ {
+ get => GetValue(HorizontalCacheLengthProperty);
+ set => SetValue(HorizontalCacheLengthProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value that indicates the size of the buffer used to realize items when
+ /// panning or scrolling vertically.
+ ///
+ public double VerticalCacheLength
+ {
+ get => GetValue(VerticalCacheLengthProperty);
+ set => SetValue(VerticalCacheLengthProperty, value);
+ }
+
+ ///
+ /// Gets a standardized view of the supported interactions between a given Items object and
+ /// the ItemsRepeater control and its associated components.
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Occurs each time an element is cleared and made available to be re-used.
+ ///
+ ///
+ /// This event is raised immediately each time an element is cleared, such as when it falls
+ /// outside the range of realized items. Elements are cleared when they become available
+ /// for re-use.
+ ///
+ public event EventHandler ElementClearing;
+
+ ///
+ /// Occurs for each realized when the index for the item it
+ /// represents has changed.
+ ///
+ ///
+ /// When you use ItemsRepeater to build a more complex control that supports specific
+ /// interactions on the child elements (such as selection or click), it is useful to be
+ /// able to keep an up-to-date identifier for the backing data item.
+ ///
+ /// This event is raised for each realized IControl where the index for the item it
+ /// represents has changed. For example, when another item is added or removed in the data
+ /// source, the index for items that come after in the ordering will be impacted.
+ ///
+ public event EventHandler ElementIndexChanged;
+
+ ///
+ /// Occurs each time an element is prepared for use.
+ ///
+ ///
+ /// The prepared element might be newly created or an existing element that is being re-
+ /// used.
+ ///
+ public event EventHandler ElementPrepared;
+
+ ///
+ /// Retrieves the index of the item from the data source that corresponds to the specified
+ /// .
+ ///
+ ///
+ /// The element that corresponds to the item to get the index of.
+ ///
+ ///
+ /// The index of the item from the data source that corresponds to the specified UIElement,
+ /// or -1 if the element is not supported.
+ ///
+ public int GetElementIndex(IControl element) => GetElementIndexImpl(element);
+
+ ///
+ /// Retrieves the realized UIElement that corresponds to the item at the specified index in
+ /// the data source.
+ ///
+ /// The index of the item.
+ ///
+ /// he UIElement that corresponds to the item at the specified index if the item is
+ /// realized, or null if the item is not realized.
+ ///
+ public IControl TryGetElement(int index) => GetElementFromIndexImpl(index);
+
+ internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true);
+
+ internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false);
+
+ internal 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);
+ 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;
+ virtInfo.ArrangeBounds = newBounds;
+ }
+ }
+
+ _viewportManager.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((AttachedLayout)args.OldValue, (AttachedLayout)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 = (IControl)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.OnItemsChanged(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.OnItemsChanged(GetLayoutContext(), newValue, args);
+ }
+ finally
+ {
+ _processingItemsSourceChange = null;
+ }
+ }
+ else if (Layout is NonVirtualizingLayout)
+ {
+ // 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(AttachedLayout oldValue, AttachedLayout 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
+ {
+ _viewManager.OnItemsSourceChanged(sender, args);
+
+ if (Layout != null)
+ {
+ if (Layout is VirtualizingLayout virtualLayout)
+ {
+ virtualLayout.OnItemsChanged(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/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs
new file mode 100644
index 0000000000..75d50e52a6
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs
@@ -0,0 +1,24 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ItemsRepeaterElementClearingEventArgs : EventArgs
+ {
+ internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element;
+
+ ///
+ /// Gets the element that is being cleared for re-use.
+ ///
+ public IControl Element { get; private set; }
+
+ internal void Update(IControl element) => Element = element;
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
new file mode 100644
index 0000000000..7ca68140b2
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
@@ -0,0 +1,44 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
+ {
+ internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex)
+ {
+ Element = element;
+ NewIndex = newIndex;
+ OldIndex = oldIndex;
+ }
+
+ ///
+ /// Get the element for which the index changed.
+ ///
+ public IControl Element { get; private set; }
+
+ ///
+ /// Gets the index of the element after the change.
+ ///
+ public int NewIndex { get; private set; }
+
+ ///
+ /// Gets the index of the element before the change.
+ ///
+ 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/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs
new file mode 100644
index 0000000000..5a30dbcf2a
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs
@@ -0,0 +1,35 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the event.
+ ///
+ public class ItemsRepeaterElementPreparedEventArgs
+ {
+ internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index)
+ {
+ Element = element;
+ Index = index;
+ }
+
+ ///
+ /// Gets the prepared element.
+ ///
+ public IControl Element { get; private set; }
+
+ ///
+ /// Gets the index of the item the element was prepared for.
+ ///
+ public int Index { get; private set; }
+
+ internal void Update(IControl element, int index)
+ {
+ Element = element;
+ Index = index;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
new file mode 100644
index 0000000000..732ba8501c
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs
@@ -0,0 +1,143 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Represents a standardized view of the supported interactions between a given ItemsSource
+ /// object and an control.
+ ///
+ ///
+ /// Components written to work with ItemsRepeater should consume the
+ /// via ItemsSourceView since this provides a normalized
+ /// view of the Items. That way, each component does not need to know if the source is an
+ /// IEnumerable, an IList, or something else.
+ ///
+ public class ItemsSourceView : INotifyCollectionChanged, IDisposable
+ {
+ private readonly IList _inner;
+ private INotifyCollectionChanged _notifyCollectionChanged;
+ private int _cachedSize = -1;
+
+ ///
+ /// Initializes a new instance of the ItemsSourceView class for the specified data source.
+ ///
+ /// The data source.
+ public ItemsSourceView(IEnumerable source)
+ {
+ Contract.Requires(source != null);
+
+ _inner = source as IList;
+
+ if (_inner == null && source is IEnumerable objectEnumerable)
+ {
+ _inner = new List(objectEnumerable);
+ }
+ else
+ {
+ _inner = new List(source.Cast());
+ }
+
+ ListenToCollectionChanges();
+ }
+
+ ///
+ /// Gets the number of items in the collection.
+ ///
+ public int Count
+ {
+ get
+ {
+ if (_cachedSize == -1)
+ {
+ _cachedSize = _inner.Count;
+ }
+
+ return _cachedSize;
+ }
+ }
+
+ ///
+ /// Gets a value that indicates whether the items source can provide a unique key for each item.
+ ///
+ ///
+ /// TODO: Not yet implemented in Avalonia.
+ ///
+ public bool HasKeyIndexMapping => false;
+
+ ///
+ /// Occurs when the collection has changed to indicate the reason for the change and which items changed.
+ ///
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ ///
+ public void Dispose()
+ {
+ if (_notifyCollectionChanged != null)
+ {
+ _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
+ }
+ }
+
+ ///
+ /// Retrieves the item at the specified index.
+ ///
+ /// The index.
+ /// the item.
+ public object GetAt(int index) => _inner[index];
+
+ ///
+ /// Retrieves the index of the item that has the specified unique identifier (key).
+ ///
+ /// The index.
+ /// The key
+ ///
+ /// TODO: Not yet implemented in Avalonia.
+ ///
+ public string KeyFromIndex(int index)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Retrieves the unique identifier (key) for the item at the specified index.
+ ///
+ /// The key.
+ /// The index.
+ ///
+ /// TODO: Not yet implemented in Avalonia.
+ ///
+ public int IndexFromKey(string key)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
+ {
+ _cachedSize = _inner.Count;
+ CollectionChanged?.Invoke(this, args);
+ }
+
+ private void ListenToCollectionChanges()
+ {
+ if (_inner is INotifyCollectionChanged incc)
+ {
+ incc.CollectionChanged += OnCollectionChanged;
+ _notifyCollectionChanged = incc;
+ }
+ }
+
+ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ OnItemsSourceChanged(e);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs
new file mode 100644
index 0000000000..4e5950bdc5
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs
@@ -0,0 +1,106 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls.Templates;
+
+namespace Avalonia.Controls
+{
+ internal class RecyclePool
+ {
+ public static readonly AttachedProperty OriginTemplateProperty =
+ AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool));
+
+ private static ConditionalWeakTable s_pools = new ConditionalWeakTable();
+ private readonly Dictionary> _elements = new Dictionary>();
+
+ public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate)
+ {
+ s_pools.TryGetValue(dataTemplate, out var result);
+ return result;
+ }
+
+ public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value);
+
+ public void PutElement(IControl element, string key, IControl owner)
+ {
+ var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
+ var elementInfo = new ElementInfo(element, ownerAsPanel);
+
+ if (!_elements.TryGetValue(key, out var pool))
+ {
+ pool = new List();
+ _elements.Add(key, pool);
+ }
+
+ pool.Add(elementInfo);
+ }
+
+ public IControl TryGetElement(string key, IControl owner)
+ {
+ if (_elements.TryGetValue(key, out var elements))
+ {
+ if (elements.Count > 0)
+ {
+ // Prefer an element from the same owner or with no owner so that we don't incur
+ // the enter/leave cost during recycling.
+ // TODO: prioritize elements with the same owner to those without an owner.
+ var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault();
+ elements.Remove(elementInfo);
+
+ var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
+ if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel)
+ {
+ // Element is still under its parent. remove it from its parent.
+ var panel = elementInfo.Owner;
+ if (panel != null)
+ {
+ int childIndex = panel.Children.IndexOf(elementInfo.Element);
+ if (childIndex == -1)
+ {
+ throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection.");
+ }
+
+ panel.Children.RemoveAt(childIndex);
+ }
+ }
+
+ return elementInfo.Element;
+ }
+ }
+
+ return null;
+ }
+
+ private IPanel EnsureOwnerIsPanelOrNull(IControl owner)
+ {
+ if (owner is IPanel panel)
+ {
+ return panel;
+ }
+ else if (owner != null)
+ {
+ throw new InvalidOperationException("Owner must be IPanel or null.");
+ }
+
+ return null;
+ }
+
+ private class ElementInfo
+ {
+ public ElementInfo(IControl element, IPanel owner)
+ {
+ Element = element;
+ Owner = owner;
+ }
+
+ public IControl Element { get; }
+ public IPanel Owner { get;}
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
new file mode 100644
index 0000000000..977d9d794c
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
@@ -0,0 +1,65 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Layout;
+
+namespace Avalonia.Controls
+{
+ internal class RepeaterLayoutContext : VirtualizingLayoutContext
+ {
+ private readonly ItemsRepeater _owner;
+
+ public RepeaterLayoutContext(ItemsRepeater owner)
+ {
+ _owner = owner;
+ }
+
+ protected override Point LayoutOriginCore
+ {
+ get => _owner.LayoutOrigin;
+ set => _owner.LayoutOrigin = value;
+ }
+
+ protected override object LayoutStateCore
+ {
+ get => _owner.LayoutState;
+ set => _owner.LayoutState = value;
+ }
+
+ protected override int RecommendedAnchorIndexCore
+ {
+ get
+ {
+ int anchorIndex = -1;
+ var anchor = _owner.SuggestedAnchor;
+ if (anchor != null)
+ {
+ anchorIndex = _owner.GetElementIndex(anchor);
+ }
+
+ return anchorIndex;
+ }
+ }
+
+ protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0;
+
+ protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options)
+ {
+ return _owner.GetElementImpl(
+ index,
+ (options & ElementRealizationOptions.ForceCreate) != 0,
+ (options & ElementRealizationOptions.SuppressAutoRecycle) != 0);
+ }
+
+ protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);
+
+ protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element);
+
+ protected override Rect RealizationRectCore() => _owner.RealizationWindow;
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs
new file mode 100644
index 0000000000..775aa3f113
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs
@@ -0,0 +1,54 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Controls
+{
+ internal class UniqueIdElementPool : IEnumerable>
+ {
+ private readonly Dictionary _elementMap = new Dictionary();
+ private readonly ItemsRepeater _owner;
+
+ public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner;
+
+ public void Add(IControl element)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var key = virtInfo.UniqueId;
+
+ if (_elementMap.ContainsKey(key))
+ {
+ throw new InvalidOperationException($"The unique id provided ({key}) is not unique.");
+ }
+
+ _elementMap.Add(key, element);
+ }
+
+ public IControl Remove(int index)
+ {
+ // Check if there is already a element in the mapping and if so, use it.
+ string key = _owner.ItemsSourceView.KeyFromIndex(index);
+
+ if (_elementMap.TryGetValue(key, out var element))
+ {
+ _elementMap.Remove(key);
+ }
+
+ return element;
+ }
+
+ public void Clear()
+ {
+ _elementMap.Clear();
+ }
+
+ public IEnumerator> GetEnumerator() => _elementMap.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs
new file mode 100644
index 0000000000..833e708e9e
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ViewManager.cs
@@ -0,0 +1,682 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ internal sealed class ViewManager
+ {
+ private const int FirstRealizedElementIndexDefault = int.MaxValue;
+ private const int LastRealizedElementIndexDefault = int.MinValue;
+
+ private readonly ItemsRepeater _owner;
+ private readonly List _pinnedPool = new List();
+ private readonly UniqueIdElementPool _resetPool;
+ private IControl _lastFocusedElement;
+ private bool _isDataSourceStableResetPending;
+ private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
+ private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
+ private bool _eventsSubscribed;
+
+ public ViewManager(ItemsRepeater owner)
+ {
+ _owner = owner;
+ _resetPool = new UniqueIdElementPool(owner);
+ }
+
+ public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle)
+ {
+ var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index);
+ if (element == null)
+ {
+ // check if this is the anchor made through repeater in preparation
+ // for a bring into view.
+ var madeAnchor = _owner.MadeAnchor;
+ if (madeAnchor != null)
+ {
+ var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor);
+ if (anchorVirtInfo.Index == index)
+ {
+ element = madeAnchor;
+ }
+ }
+ }
+ if (element == null) { element = GetElementFromUniqueIdResetPool(index); };
+ if (element == null) { element = GetElementFromPinnedElements(index); }
+ if (element == null) { element = GetElementFromElementFactory(index); }
+
+ var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
+ if (suppressAutoRecycle)
+ {
+ virtInfo.AutoRecycleCandidate = false;
+ }
+ else
+ {
+ virtInfo.AutoRecycleCandidate = true;
+ virtInfo.KeepAlive = true;
+ }
+
+ return element;
+ }
+
+ public void ClearElement(IControl element, bool isClearedDueToCollectionChange)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var index = virtInfo.Index;
+ bool cleared =
+ ClearElementToUniqueIdResetPool(element, virtInfo) ||
+ ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange);
+
+ if (!cleared)
+ {
+ ClearElementToElementFactory(element);
+ }
+
+ // Both First and Last indices need to be valid or default.
+ if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout)
+ {
+ // First and last were pointing to the same element and that is going away.
+ InvalidateRealizedIndicesHeldByLayout();
+ }
+ else if (index == _firstRealizedElementIndexHeldByLayout)
+ {
+ // The FirstElement is going away, shrink the range by one.
+ ++_firstRealizedElementIndexHeldByLayout;
+ }
+ else if (index == _lastRealizedElementIndexHeldByLayout)
+ {
+ // Last element is going away, shrink the range by one at the end.
+ --_lastRealizedElementIndexHeldByLayout;
+ }
+ else
+ {
+ // Index is either outside the range we are keeping track of or inside the range.
+ // In both these cases, we just keep the range we have. If this clear was due to
+ // a collection change, then in the CollectionChanged event, we will invalidate these guys.
+ }
+ }
+
+ public void ClearElementToElementFactory(IControl element)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var clearedIndex = virtInfo.Index;
+ _owner.OnElementClearing(element);
+ _owner.ItemTemplateShim.RecycleElement(_owner, element);
+
+ virtInfo.MoveOwnershipToElementFactory();
+
+ if (_lastFocusedElement == element)
+ {
+ // Focused element is going away. Remove the tracked last focused element
+ // and pick a reasonable next focus if we can find one within the layout
+ // realized elements.
+ MoveFocusFromClearedIndex(clearedIndex);
+ }
+
+ }
+
+ private void MoveFocusFromClearedIndex(int clearedIndex)
+ {
+ IControl focusedChild = null;
+ var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild);
+ if (focusCandidate != null)
+ {
+ focusCandidate.Focus();
+ _lastFocusedElement = focusedChild;
+
+ // Add pin to hold the focused element.
+ UpdatePin(focusedChild, true /* addPin */);
+ }
+ else
+ {
+ // We could not find a candiate.
+ _lastFocusedElement = null;
+ }
+ }
+
+ IControl FindFocusCandidate(int clearedIndex, IControl focusedChild)
+ {
+ // Walk through all the children and find elements with index before and after the cleared index.
+ // Note that during a delete the next element would now have the same index.
+ int previousIndex = int.MinValue;
+ int nextIndex = int.MaxValue;
+ IControl nextElement = null;
+ IControl previousElement = null;
+
+ foreach (var child in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
+ if (virtInfo?.IsHeldByLayout == true)
+ {
+ int currentIndex = virtInfo.Index;
+ if (currentIndex < clearedIndex)
+ {
+ if (currentIndex > previousIndex)
+ {
+ previousIndex = currentIndex;
+ previousElement = child;
+ }
+ }
+ else if (currentIndex >= clearedIndex)
+ {
+ // Note that we use >= above because if we deleted the focused element,
+ // the next element would have the same index now.
+ if (currentIndex < nextIndex)
+ {
+ nextIndex = currentIndex;
+ nextElement = child;
+ }
+ }
+ }
+ }
+
+ // TODO: Find the next element if one exists, if not use the previous element.
+ // If the container itself is not focusable, find a descendent that is.
+
+ return nextElement;
+ }
+
+ public int GetElementIndex(VirtualizationInfo virtInfo)
+ {
+ if (virtInfo == null)
+ {
+ throw new ArgumentException("Element is not a child of this ItemsRepeater.");
+ }
+
+ return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
+ }
+
+ public void PrunePinnedElements()
+ {
+ EnsureEventSubscriptions();
+
+ // Go through pinned elements and make sure they still have
+ // a reason to be pinned.
+ for (var i = 0; i < _pinnedPool.Count; ++i)
+ {
+ var elementInfo = _pinnedPool[i];
+ var virtInfo = elementInfo.VirtualizationInfo;
+
+ if (!virtInfo.IsPinned)
+ {
+ _pinnedPool.RemoveAt(i);
+ --i;
+
+ // Pinning was the only thing keeping this element alive.
+ ClearElementToElementFactory(elementInfo.PinnedElement);
+ }
+ }
+ }
+
+ public void UpdatePin(IControl element, bool addPin)
+ {
+ var parent = element.VisualParent;
+ var child = (IVisual)element;
+
+ while (parent != null)
+ {
+ if (parent is ItemsRepeater repeater)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child);
+ if (virtInfo.IsRealized)
+ {
+ if (addPin)
+ {
+ virtInfo.AddPin();
+ }
+ else if (virtInfo.IsPinned)
+ {
+ if (virtInfo.RemovePin() == 0)
+ {
+ // ElementFactory is invoked during the measure pass.
+ // We will clear the element then.
+ repeater.InvalidateMeasure();
+ }
+ }
+ }
+ }
+
+ child = parent;
+ parent = child.VisualParent;
+ }
+ }
+
+ public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args)
+ {
+ // Note: For items that have been removed, the index will not be touched. It will hold
+ // the old index before it was removed. It is not valid anymore.
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ {
+ var newIndex = args.NewStartingIndex;
+ var newCount = args.NewItems.Count;
+ EnsureFirstLastRealizedIndices();
+ if (newIndex <= _lastRealizedElementIndexHeldByLayout)
+ {
+ _lastRealizedElementIndexHeldByLayout += newCount;
+ foreach (var element in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var dataIndex = virtInfo.Index;
+
+ if (virtInfo.IsRealized && dataIndex >= newIndex)
+ {
+ UpdateElementIndex(element, virtInfo, dataIndex + newCount);
+ }
+ }
+ }
+ else
+ {
+ // Indices held by layout are not affected
+ // We could still have items in the pinned elements that need updates. This is usually a very small vector.
+ for (var i = 0; i < _pinnedPool.Count; ++i)
+ {
+ var elementInfo = _pinnedPool[i];
+ var virtInfo = elementInfo.VirtualizationInfo;
+ var dataIndex = virtInfo.Index;
+
+ if (virtInfo.IsRealized && dataIndex >= newIndex)
+ {
+ var element = elementInfo.PinnedElement;
+ UpdateElementIndex(element, virtInfo, dataIndex + newCount);
+ }
+ }
+ }
+ break;
+ }
+
+ case NotifyCollectionChangedAction.Replace:
+ {
+ // Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true.
+ // Two cases here
+ // case 1: oldCount == newCount
+ // indices are not affected. nothing to do here.
+ // case 2: oldCount != newCount
+ // Replaced with less or more items. This is like an insert or remove
+ // depending on the counts.
+ var oldStartIndex = args.OldStartingIndex;
+ var newStartingIndex = args.NewStartingIndex;
+ var oldCount = args.OldItems.Count;
+ var newCount = args.NewItems.Count;
+ if (oldStartIndex != newStartingIndex)
+ {
+ throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex.");
+ }
+
+ if (oldCount == 0)
+ {
+ throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead.");
+ }
+
+ if (newCount == 0)
+ {
+ throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead.");
+ }
+
+ int countChange = newCount - oldCount;
+ if (countChange != 0)
+ {
+ // countChange > 0 : countChange items were added
+ // countChange < 0 : -countChange items were removed
+ foreach (var element in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var dataIndex = virtInfo.Index;
+
+ if (virtInfo.IsRealized)
+ {
+ if (dataIndex >= oldStartIndex + oldCount)
+ {
+ UpdateElementIndex(element, virtInfo, dataIndex + countChange);
+ }
+ }
+ }
+
+ EnsureFirstLastRealizedIndices();
+ _lastRealizedElementIndexHeldByLayout += countChange;
+ }
+ break;
+ }
+
+ case NotifyCollectionChangedAction.Remove:
+ {
+ var oldStartIndex = args.OldStartingIndex;
+ var oldCount = args.OldItems.Count;
+ foreach (var element in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ var dataIndex = virtInfo.Index;
+
+ if (virtInfo.IsRealized)
+ {
+ if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount)
+ {
+ // If we are doing the mapping, remove the element who's data was removed.
+ _owner.ClearElementImpl(element);
+ }
+ else if (dataIndex >= (oldStartIndex + oldCount))
+ {
+ UpdateElementIndex(element, virtInfo, dataIndex - oldCount);
+ }
+ }
+ }
+
+ InvalidateRealizedIndicesHeldByLayout();
+ break;
+ }
+
+ case NotifyCollectionChangedAction.Reset:
+ if (_owner.ItemsSourceView.HasKeyIndexMapping)
+ {
+ _isDataSourceStableResetPending = true;
+ }
+
+ // Walk through all the elements and make sure they are cleared, they will go into
+ // the stable id reset pool.
+ foreach (var element in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
+ {
+ _owner.ClearElementImpl(element);
+ }
+ }
+
+ InvalidateRealizedIndicesHeldByLayout();
+ break;
+ }
+ }
+
+ private void EnsureFirstLastRealizedIndices()
+ {
+ if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault)
+ {
+ // This will ensure that the indexes are updated.
+ GetElementIfAlreadyHeldByLayout(0);
+ }
+ }
+
+ public void OnLayoutChanging()
+ {
+ if (_owner.ItemsSourceView?.HasKeyIndexMapping == true)
+ {
+ _isDataSourceStableResetPending = true;
+ }
+ }
+
+ public void OnOwnerArranged()
+ {
+ if (_isDataSourceStableResetPending)
+ {
+ _isDataSourceStableResetPending = false;
+
+ foreach (var entry in _resetPool)
+ {
+ // TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource.
+ // Focused element is getting cleared. Need to figure out semantics on where
+ // focus should go when the focused element is removed from the data collection.
+ ClearElement(entry.Value, true /* isClearedDueToCollectionChange */);
+ }
+
+ _resetPool.Clear();
+ }
+ }
+
+ // We optimize for the case where index is not realized to return null as quickly as we can.
+ // Flow layouts manage containers on their own and will never ask for an index that is already realized.
+ // If an index that is realized is requested by the layout, we unfortunately have to walk the
+ // children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing
+ // and non-virtualizing hosts.
+ private IControl GetElementIfAlreadyHeldByLayout(int index)
+ {
+ IControl element = null;
+
+ bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault;
+ bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout);
+
+ if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange)
+ {
+ foreach (var child in _owner.Children)
+ {
+ var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
+ if (virtInfo?.IsHeldByLayout == true)
+ {
+ // Only give back elements held by layout. If someone else is holding it, they will be served by other methods.
+ int childIndex = virtInfo.Index;
+ _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex);
+ _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex);
+ if (virtInfo.Index == index)
+ {
+ element = child;
+ // If we have valid first/last indices, we don't have to walk the rest, but if we
+ // do not, then we keep walking through the entire children collection to get accurate
+ // indices once.
+ if (!cachedFirstLastIndicesInvalid)
+ {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return element;
+ }
+
+ private IControl GetElementFromUniqueIdResetPool(int index)
+ {
+ IControl element = null;
+ // See if you can get it from the reset pool.
+ if (_isDataSourceStableResetPending)
+ {
+ element = _resetPool.Remove(index);
+ if (element != null)
+ {
+ // Make sure that the index is updated to the current one
+ var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
+ UpdateElementIndex(element, virtInfo, index);
+ }
+ }
+
+ return element;
+ }
+
+ private IControl GetElementFromPinnedElements(int index)
+ {
+ IControl element = null;
+
+ // See if you can find something among the pinned elements.
+ for (var i = 0; i < _pinnedPool.Count; ++i)
+ {
+ var elementInfo = _pinnedPool[i];
+ var virtInfo = elementInfo.VirtualizationInfo;
+
+ if (virtInfo.Index == index)
+ {
+ _pinnedPool.RemoveAt(i);
+ element = elementInfo.PinnedElement;
+ elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
+ break;
+ }
+ }
+
+ return element;
+ }
+
+ private IControl GetElementFromElementFactory(int index)
+ {
+ // The view generator is the provider of last resort.
+
+ var itemTemplateFactory = _owner.ItemTemplateShim;
+ if (itemTemplateFactory == null)
+ {
+ // If no ItemTemplate was provided, use a default
+ var factory = FuncDataTemplate.Default;
+ _owner.ItemTemplate = factory;
+ itemTemplateFactory = _owner.ItemTemplateShim;
+ }
+
+ var data = _owner.ItemsSourceView.GetAt(index);
+ var element = itemTemplateFactory.GetElement(_owner, data);
+
+ var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
+ if (virtInfo == null)
+ {
+ virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
+ }
+
+ // Prepare the element
+ element.DataContext = data;
+
+ virtInfo.MoveOwnershipToLayoutFromElementFactory(
+ index,
+ /* uniqueId: */
+ _owner.ItemsSourceView.HasKeyIndexMapping ?
+ _owner.ItemsSourceView.KeyFromIndex(index) :
+ string.Empty);
+
+ // The view generator is the only provider that prepares the element.
+ var repeater = _owner;
+
+ // Add the element to the children collection here before raising OnElementPrepared so
+ // that handlers can walk up the tree in case they want to find their IndexPath in the
+ // nested case.
+ var children = repeater.Children;
+ if (element.VisualParent != repeater)
+ {
+ children.Add(element);
+ }
+
+ repeater.OnElementPrepared(element, index);
+
+ // Update realized indices
+ _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
+ _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
+
+ return element;
+ }
+
+ private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo)
+ {
+ if (_isDataSourceStableResetPending)
+ {
+ _resetPool.Add(element);
+ virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
+ }
+
+ return _isDataSourceStableResetPending;
+ }
+
+ private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
+ {
+ if (_isDataSourceStableResetPending)
+ {
+ _resetPool.Add(element);
+ virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
+ }
+
+ return _isDataSourceStableResetPending;
+ }
+
+ private void UpdateFocusedElement()
+ {
+ IControl focusedElement = null;
+
+ var child = FocusManager.Instance.Current;
+
+ if (child != null)
+ {
+ var parent = child.VisualParent;
+ var owner = _owner;
+
+ // Find out if the focused element belongs to one of our direct
+ // children.
+ while (parent != null)
+ {
+ if (parent is ItemsRepeater repeater)
+ {
+ var element = child as IControl;
+ if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized)
+ {
+ focusedElement = element;
+ }
+
+ break;
+ }
+
+ child = parent as IInputElement;
+ parent = child.VisualParent;
+ }
+ }
+
+ // If the focused element has changed,
+ // we need to unpin the old one and pin the new one.
+ if (_lastFocusedElement != focusedElement)
+ {
+ if (_lastFocusedElement != null)
+ {
+ UpdatePin(_lastFocusedElement, false /* addPin */);
+ }
+
+ if (focusedElement != null)
+ {
+ UpdatePin(focusedElement, true /* addPin */);
+ }
+
+ _lastFocusedElement = focusedElement;
+ }
+ }
+
+ private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement();
+
+ private void EnsureEventSubscriptions()
+ {
+ if (!_eventsSubscribed)
+ {
+ _owner.GotFocus += OnFocusChanged;
+ _owner.LostFocus += OnFocusChanged;
+ }
+ }
+
+ private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index)
+ {
+ var oldIndex = virtInfo.Index;
+ if (oldIndex != index)
+ {
+ virtInfo.UpdateIndex(index);
+ _owner.OnElementIndexChanged(element, oldIndex, index);
+ }
+ }
+
+ private void InvalidateRealizedIndicesHeldByLayout()
+ {
+ _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
+ _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
+ }
+
+ private struct PinnedElementInfo
+ {
+ public PinnedElementInfo(IControl element)
+ {
+ PinnedElement = element;
+ VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element);
+ }
+
+ public IControl PinnedElement { get; }
+ public VirtualizationInfo VirtualizationInfo { get; }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs
new file mode 100644
index 0000000000..10c11889d0
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs
@@ -0,0 +1,501 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using Avalonia.Layout;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ internal class ViewportManager
+ {
+ private const double CacheBufferPerSideInflationPixelDelta = 40.0;
+ private readonly ItemsRepeater _owner;
+ private bool _ensuredScroller;
+ private IScrollAnchorProvider _scroller;
+ private IControl _makeAnchorElement;
+ private bool _isAnchorOutsideRealizedRange;
+ private Task _cacheBuildAction;
+ private Rect _visibleWindow;
+ private Rect _layoutExtent;
+ // This is the expected shift by the layout.
+ private Point _expectedViewportShift;
+ // This is what is pending and not been accounted for.
+ // Sometimes the scrolling surface cannot service a shift (for example
+ // it is already at the top and cannot shift anymore.)
+ private Point _pendingViewportShift;
+ // Unshiftable shift amount that this view manager can
+ // handle on its own to fake it to the layout as if the shift
+ // actually happened. This can happen in cases where no scrollviewer
+ // in the parent chain can scroll in the shift direction.
+ private Point _unshiftableShift;
+ private double _maximumHorizontalCacheLength = 0.0;
+ private double _maximumVerticalCacheLength = 0.0;
+ private double _horizontalCacheBufferPerSide;
+ private double _verticalCacheBufferPerSide;
+ private bool _isBringIntoViewInProgress;
+ // For non-virtualizing layouts, we do not need to keep
+ // updating viewports and invalidating measure often. So when
+ // a non virtualizing layout is used, we stop doing all that work.
+ bool _managingViewportDisabled;
+ private IDisposable _effectiveViewportChangedRevoker;
+ private bool _layoutUpdatedSubscribed;
+
+ public ViewportManager(ItemsRepeater owner)
+ {
+ _owner = owner;
+ }
+
+ public IControl SuggestedAnchor
+ {
+ get
+ {
+ // The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick.
+ var suggestedAnchor = _makeAnchorElement;
+ var owner = _owner;
+
+ if (suggestedAnchor == null)
+ {
+ var anchorElement = _scroller?.CurrentAnchor;
+
+ if (anchorElement != null)
+ {
+ // We can't simply return anchorElement because, in case of nested Repeaters, it may not
+ // be a direct child of ours, or even an indirect child. We need to walk up the tree starting
+ // from anchorElement to figure out what child of ours (if any) to use as the suggested element.
+ var child = anchorElement;
+ var parent = child.VisualParent as IControl;
+
+ while (parent != null)
+ {
+ if (parent == owner)
+ {
+ suggestedAnchor = child;
+ break;
+ }
+
+ child = parent;
+ parent = parent.VisualParent as IControl;
+ }
+ }
+ }
+
+ return suggestedAnchor;
+ }
+ }
+
+ public bool HasScroller => _scroller != null;
+
+ public IControl MadeAnchor => _makeAnchorElement;
+
+ public double HorizontalCacheLength
+ {
+ get => _maximumHorizontalCacheLength;
+ set
+ {
+ if (_maximumHorizontalCacheLength != value)
+ {
+ ValidateCacheLength(value);
+ _maximumHorizontalCacheLength = value;
+ }
+ }
+ }
+
+ public double VerticalCacheLength
+ {
+ get => _maximumVerticalCacheLength;
+ set
+ {
+ if (_maximumVerticalCacheLength != value)
+ {
+ ValidateCacheLength(value);
+ _maximumVerticalCacheLength = value;
+ }
+ }
+ }
+
+ private Rect GetLayoutVisibleWindowDiscardAnchor()
+ {
+ var visibleWindow = _visibleWindow;
+
+ if (HasScroller)
+ {
+ visibleWindow = new Rect(
+ visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
+ visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
+ visibleWindow.Width,
+ visibleWindow.Height);
+ }
+
+ return visibleWindow;
+ }
+
+ public Rect GetLayoutVisibleWindow()
+ {
+ var visibleWindow = _visibleWindow;
+
+ if (_makeAnchorElement != null)
+ {
+ // The anchor is not necessarily laid out yet. Its position should default
+ // to zero and the layout origin is expected to change once layout is done.
+ // Until then, we need a window that's going to protect the anchor from
+ // getting recycled.
+ visibleWindow = visibleWindow.WithX(0).WithY(0);
+ }
+ else if (HasScroller)
+ {
+ visibleWindow = new Rect(
+ visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
+ visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
+ visibleWindow.Width,
+ visibleWindow.Height);
+ }
+
+ return visibleWindow;
+ }
+
+ public Rect GetLayoutRealizationWindow()
+ {
+ var realizationWindow = GetLayoutVisibleWindow();
+ if (HasScroller)
+ {
+ realizationWindow = new Rect(
+ realizationWindow.X - _horizontalCacheBufferPerSide,
+ realizationWindow.Y - _verticalCacheBufferPerSide,
+ realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0,
+ realizationWindow.Height + _verticalCacheBufferPerSide * 2.0);
+ }
+
+ return realizationWindow;
+ }
+
+ public void SetLayoutExtent(Rect extent)
+ {
+ _expectedViewportShift = new Point(
+ _expectedViewportShift.X + _layoutExtent.X - extent.X,
+ _expectedViewportShift.Y + _layoutExtent.Y - extent.Y);
+
+ // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
+ if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
+ {
+ // There are cases where we might be expecting a shift but not get it. We will
+ // be waiting for the effective viewport event but if the scroll viewer is not able
+ // to perform the shift (perhaps because it cannot scroll in negative offset),
+ // then we will end up not realizing elements in the visible
+ // window. To avoid this, we register to layout updated for this layout pass. If we
+ // get an effective viewport, we know we have a new viewport and we unregister from
+ // layout updated. If we get the layout updated handler, then we know that the
+ // scroller was unable to perform the shift and we invalidate measure and unregister
+ // from the layout updated event.
+ if (!_layoutUpdatedSubscribed)
+ {
+ _owner.LayoutUpdated += OnLayoutUpdated;
+ _layoutUpdatedSubscribed = true;
+ }
+ }
+
+ _layoutExtent = extent;
+ _pendingViewportShift = _expectedViewportShift;
+
+ // We just finished a measure pass and have a new extent.
+ // Let's make sure the scrollers will run its arrange so that they track the anchor.
+ ((IControl)_scroller)?.InvalidateArrange();
+ }
+
+ public Point GetOrigin() => _layoutExtent.TopLeft;
+
+ public void OnLayoutChanged(bool isVirtualizing)
+ {
+ _managingViewportDisabled = !isVirtualizing;
+
+ _layoutExtent = default;
+ _expectedViewportShift = default;
+ _pendingViewportShift = default;
+ _unshiftableShift = default;
+
+ _effectiveViewportChangedRevoker?.Dispose();
+
+ if (!_managingViewportDisabled)
+ {
+ _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
+ }
+ }
+
+ public void OnElementPrepared(IControl element)
+ {
+ // If we have an anchor element, we do not want the
+ // scroll anchor provider to start anchoring some other element.
+ ////element.CanBeScrollAnchor(true);
+ }
+
+ public void OnElementCleared(ILayoutable element)
+ {
+ ////element.CanBeScrollAnchor(false);
+ }
+
+ public void OnOwnerMeasuring()
+ {
+ // This is because of a bug that causes effective viewport to not
+ // fire if you register during arrange.
+ // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
+ EnsureScroller();
+ }
+
+ public void OnOwnerArranged()
+ {
+ _expectedViewportShift = default;
+
+ if (!_managingViewportDisabled)
+ {
+ // This is because of a bug that causes effective viewport to not
+ // fire if you register during arrange.
+ // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
+ // EnsureScroller();
+
+ if (HasScroller)
+ {
+ double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0;
+ double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0;
+
+ bool continueBuildingCache =
+ _horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide ||
+ _verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide;
+
+ if (continueBuildingCache)
+ {
+ _horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
+ _verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
+
+ _horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide);
+ _verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide);
+ }
+ }
+ }
+ }
+
+ private void OnLayoutUpdated(object sender, EventArgs args)
+ {
+ _owner.LayoutUpdated -= OnLayoutUpdated;
+ if (_managingViewportDisabled)
+ {
+ return;
+ }
+
+ // We were expecting a viewport shift but we never got one and we are not going to in this
+ // layout pass. We likely will never get this shift, so lets assume that we are never going to get it and
+ // adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer
+ // that can scroll in the direction where the shift is expected.
+ if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0)
+ {
+ // Assume this is never going to come.
+ _unshiftableShift = new Point(
+ _unshiftableShift.X + _pendingViewportShift.X,
+ _unshiftableShift.Y + _pendingViewportShift.Y);
+ _pendingViewportShift = default;
+ _expectedViewportShift = default;
+
+ TryInvalidateMeasure();
+ }
+ }
+
+ public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange)
+ {
+ _makeAnchorElement = anchor;
+ _isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
+ }
+
+ public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args)
+ {
+ if (!_managingViewportDisabled)
+ {
+ // During the time between a bring into view request and the element coming into view we do not
+ // want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the
+ // element that is being brought into view. We can do this by making just that element as a potential
+ // anchor candidate and ensure no other element of this repeater is an anchor candidate.
+ // Once the layout pass is done and we render the frame, the element will be in frame and we can
+ // switch back to letting the anchor provider pick a suitable anchor.
+
+ // get the targetChild - i.e the immediate child of this repeater that is being brought into view.
+ // Note that the element being brought into view could be a descendant.
+ var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject);
+
+ // Make sure that only the target child can be the anchor during the bring into view operation.
+ foreach (var child in _owner.Children)
+ {
+ ////if (child.CanBeScrollAnchor && child != targetChild)
+ ////{
+ //// child.CanBeScrollAnchor = false;
+ ////}
+ }
+
+ // Register to rendering event to go back to how things were before where any child can be the anchor.
+ _isBringIntoViewInProgress = true;
+ ////if (!m_renderingToken)
+ ////{
+ //// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr };
+ //// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering });
+ ////}
+ }
+ }
+
+ private IControl GetImmediateChildOfRepeater(IControl descendant)
+ {
+ var targetChild = descendant;
+ var parent = descendant.Parent;
+ while (parent != null && parent != _owner)
+ {
+ targetChild = parent;
+ parent = (IControl)parent.VisualParent;
+ }
+
+ if (parent == null)
+ {
+ throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call");
+ }
+
+ return targetChild;
+ }
+
+ public void ResetScrollers()
+ {
+ _scroller = null;
+ _effectiveViewportChangedRevoker?.Dispose();
+ _effectiveViewportChangedRevoker = null;
+ _ensuredScroller = false;
+ }
+
+ private void OnEffectiveViewportChanged(TransformedBounds? bounds)
+ {
+ if (!bounds.HasValue)
+ {
+ return;
+ }
+
+ var globalClip = bounds.Value.Clip;
+ var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value;
+ var clip = globalClip.TransformToAABB(transform);
+ var effectiveViewport = clip.Intersect(bounds.Value.Bounds);
+
+ UpdateViewport(effectiveViewport);
+
+ _pendingViewportShift = default;
+ _unshiftableShift = default;
+ if (_visibleWindow.IsEmpty)
+ {
+ // We got cleared.
+ _layoutExtent = default;
+ }
+
+ // We got a new viewport, we dont need to wait for layout updated anymore to
+ // see if our request for a pending shift was handled.
+ if (_layoutUpdatedSubscribed)
+ {
+ _owner.LayoutUpdated -= OnLayoutUpdated;
+ }
+ }
+
+ private void EnsureScroller()
+ {
+ if (!_ensuredScroller)
+ {
+ ResetScrollers();
+
+ var parent = _owner.GetVisualParent();
+ while (parent != null)
+ {
+ if (parent is IScrollAnchorProvider scroller)
+ {
+ _scroller = scroller;
+ break;
+ }
+
+ parent = parent.VisualParent;
+ }
+
+ if (_scroller == null)
+ {
+ // We usually update the viewport in the post arrange handler. But, since we don't have
+ // a scroller, let's do it now.
+ UpdateViewport(Rect.Empty);
+ }
+ else if (!_managingViewportDisabled)
+ {
+ _effectiveViewportChangedRevoker?.Dispose();
+ _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
+ }
+
+ _ensuredScroller = true;
+ }
+ }
+
+ private void UpdateViewport(Rect viewport)
+ {
+ var currentVisibleWindow = viewport;
+
+ if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
+ -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
+ {
+ // We got cleared.
+ _visibleWindow = default;
+ }
+ else
+ {
+ _visibleWindow = currentVisibleWindow;
+ }
+
+ TryInvalidateMeasure();
+ }
+
+ private static void ValidateCacheLength(double cacheLength)
+ {
+ if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength))
+ {
+ throw new ArgumentException("The maximum cache length must be equal or superior to zero.");
+ }
+ }
+
+ private void TryInvalidateMeasure()
+ {
+ // Don't invalidate measure if we have an invalid window.
+ if (!_visibleWindow.IsEmpty)
+ {
+ // We invalidate measure instead of just invalidating arrange because
+ // we don't invalidate measure in UpdateViewport if the view is changing to
+ // avoid layout cycles.
+ _owner.InvalidateMeasure();
+ }
+ }
+
+ private IDisposable SubscribeToEffectiveViewportChanged(IControl control)
+ {
+ // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
+ // we can get this from TransformedBounds, but this property is updated after layout has
+ // run, resulting in the UI being updated too late when scrolling quickly. We can
+ // partially remedey this by triggering also on Bounds changes, but this won't work so
+ // well for nested ItemsRepeaters.
+ //
+ // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially
+ // for this case): we need to implement that in Avalonia.
+ return control.GetObservable(Visual.TransformedBoundsProperty)
+ .Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds))
+ .Skip(1)
+ .Subscribe(OnEffectiveViewportChanged);
+ }
+
+ private class ScrollerInfo
+ {
+ public ScrollerInfo(ScrollViewer scroller)
+ {
+ Scroller = scroller;
+ }
+
+ public ScrollViewer Scroller { get; }
+ }
+ };
+}
diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
new file mode 100644
index 0000000000..eb30c1b7cf
--- /dev/null
+++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
@@ -0,0 +1,118 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Controls
+{
+ internal enum ElementOwner
+ {
+ // All elements are originally owned by the view generator.
+ ElementFactory,
+ // Ownership is transferred to the layout when it calls GetElement.
+ Layout,
+ // Ownership is transferred to the pinned pool if the element is cleared (outside of
+ // a 'remove' collection change of course).
+ PinnedPool,
+ // Ownership is transfered to the reset pool if the element is cleared by a reset and
+ // the data source supports unique ids.
+ UniqueIdResetPool,
+ // Ownership is transfered to the animator if the element is cleared due to a
+ // 'remove'-like collection change.
+ Animator
+ }
+
+ internal class VirtualizationInfo
+ {
+ private int _pinCounter;
+ private object _data;
+
+ public Rect ArrangeBounds { get; set; }
+ public bool AutoRecycleCandidate { get; set; }
+ public int Index { get; private set; }
+ public bool IsPinned => _pinCounter > 0;
+ public bool IsHeldByLayout => Owner == ElementOwner.Layout;
+ public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool;
+ public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
+ public bool KeepAlive { get; set; }
+ public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
+ public string UniqueId { get; private set; }
+
+ public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId)
+ {
+ Owner = ElementOwner.Layout;
+ Index = index;
+ UniqueId = uniqueId;
+ }
+
+ public void MoveOwnershipToLayoutFromUniqueIdResetPool()
+ {
+ Owner = ElementOwner.Layout;
+ }
+
+ public void MoveOwnershipToLayoutFromPinnedPool()
+ {
+ Owner = ElementOwner.Layout;
+ }
+
+ public void MoveOwnershipToElementFactory()
+ {
+ Owner = ElementOwner.ElementFactory;
+ _pinCounter = 0;
+ Index = -1;
+ UniqueId = string.Empty;
+ ArrangeBounds = ItemsRepeater.InvalidRect;
+ }
+
+ public void MoveOwnershipToUniqueIdResetPoolFromLayout()
+ {
+ Owner = ElementOwner.UniqueIdResetPool;
+ // Keep the pinCounter the same. If the container survives the reset
+ // it can go on being pinned as if nothing happened.
+ }
+
+ public void MoveOwnershipToAnimator()
+ {
+ // During a unique id reset, some elements might get removed.
+ // Their ownership will go from the UniqueIdResetPool to the Animator.
+ // The common path though is for ownership to go from Layout to Animator.
+ Owner = ElementOwner.Animator;
+ Index = -1;
+ _pinCounter = 0;
+ }
+
+ public void MoveOwnershipToPinnedPool()
+ {
+ Owner = ElementOwner.PinnedPool;
+ }
+
+ public int AddPin()
+ {
+ if (!IsRealized)
+ {
+ throw new InvalidOperationException("You can't pin an unrealized element.");
+ }
+
+ return ++_pinCounter;
+ }
+
+ public int RemovePin()
+ {
+ if (!IsRealized)
+ {
+ throw new InvalidOperationException("You can't unpin an unrealized element.");
+ }
+
+ if (!IsPinned)
+ {
+ throw new InvalidOperationException("UnpinElement was called more often than PinElement.");
+ }
+
+ return --_pinCounter;
+ }
+
+ public void UpdateIndex(int newIndex) => Index = newIndex;
+ }
+}
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index 264b1fd2ce..c9b5cbb75b 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -11,7 +11,7 @@ namespace Avalonia.Controls
///
/// A control scrolls its content if the content is bigger than the space available.
///
- public class ScrollViewer : ContentControl, IScrollable
+ public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{
///
/// Defines the property.
@@ -333,6 +333,9 @@ namespace Avalonia.Controls
get { return _viewport.Height; }
}
+ ///
+ IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
+
///
/// Gets the value of the HorizontalScrollBarVisibility attached property.
///
@@ -373,6 +376,16 @@ namespace Avalonia.Controls
control.SetValue(VerticalScrollBarVisibilityProperty, value);
}
+ void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
+ {
+ // TODO: Implement
+ }
+
+ void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
+ {
+ // TODO: Implement
+ }
+
internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset)
{
var maxX = Math.Max(extent.Width - viewport.Width, 0);
diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs
index bc4733296b..9eaa246434 100644
--- a/src/Avalonia.Controls/Slider.cs
+++ b/src/Avalonia.Controls/Slider.cs
@@ -5,6 +5,7 @@ using System;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs
index c29faa1b4d..8e3b4905cf 100644
--- a/src/Avalonia.Controls/StackPanel.cs
+++ b/src/Avalonia.Controls/StackPanel.cs
@@ -17,13 +17,13 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty SpacingProperty =
- AvaloniaProperty.Register(nameof(Spacing));
+ StackLayout.SpacingProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty OrientationProperty =
- AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical);
+ StackLayout.OrientationProperty.AddOwner();
///
/// Initializes static members of the class.
diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index 6f53d853c7..3acf341c35 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/src/Avalonia.Controls/WrapPanel.cs
@@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.Utilities;
using static System.Math;
diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml
index a538516c1a..1df0f3a097 100644
--- a/src/Avalonia.Diagnostics/DevTools.xaml
+++ b/src/Avalonia.Diagnostics/DevTools.xaml
@@ -1,24 +1,24 @@
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
- Hold Ctrl+Shift over a control to inspect.
-
- Focused:
-
-
- Pointer Over:
-
-
-
+
+ Hold Ctrl+Shift over a control to inspect.
+
+ Focused:
+
+
+ Pointer Over:
+
+
+
diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs
index ccb6151ada..cc3c545d84 100644
--- a/src/Avalonia.Diagnostics/DevTools.xaml.cs
+++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
@@ -18,22 +17,22 @@ using Avalonia.VisualTree;
namespace Avalonia
{
- public static class DevToolsExtensions
- {
- public static void AttachDevTools(this TopLevel control)
- {
- Avalonia.Diagnostics.DevTools.Attach(control);
- }
- }
+ public static class DevToolsExtensions
+ {
+ public static void AttachDevTools(this TopLevel control)
+ {
+ Diagnostics.DevTools.Attach(control);
+ }
+ }
}
namespace Avalonia.Diagnostics
{
- public class DevTools : UserControl
+ public class DevTools : UserControl
{
- private static Dictionary s_open = new Dictionary();
- private static HashSet s_visualTreeRoots = new HashSet();
- private IDisposable _keySubscription;
+ private static readonly Dictionary s_open = new Dictionary();
+ private static readonly HashSet s_visualTreeRoots = new HashSet();
+ private readonly IDisposable _keySubscription;
public DevTools(IControl root)
{
@@ -49,7 +48,6 @@ namespace Avalonia.Diagnostics
// HACK: needed for XAMLIL, will fix that later
public DevTools()
{
-
}
public IControl Root { get; }
@@ -67,9 +65,8 @@ namespace Avalonia.Diagnostics
if (e.Key == Key.F12)
{
var control = (TopLevel)sender;
- var devToolsWindow = default(Window);
- if (s_open.TryGetValue(control, out devToolsWindow))
+ if (s_open.TryGetValue(control, out var devToolsWindow))
{
devToolsWindow.Activate();
}
@@ -82,10 +79,7 @@ namespace Avalonia.Diagnostics
Width = 1024,
Height = 512,
Content = devTools,
- DataTemplates =
- {
- new ViewLocator(),
- },
+ DataTemplates = { new ViewLocator() },
Title = "Avalonia DevTools"
};
@@ -118,7 +112,6 @@ namespace Avalonia.Diagnostics
if ((e.Modifiers) == modifiers)
{
-
var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
.FirstOrDefault();
diff --git a/src/Avalonia.Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Models/EventChainLink.cs
index aab50a13dd..464187a048 100644
--- a/src/Avalonia.Diagnostics/Models/EventChainLink.cs
+++ b/src/Avalonia.Diagnostics/Models/EventChainLink.cs
@@ -12,9 +12,9 @@ namespace Avalonia.Diagnostics.Models
{
Contract.Requires(handler != null);
- this.Handler = handler;
- this.Handled = handled;
- this.Route = route;
+ Handler = handler;
+ Handled = handled;
+ Route = route;
}
public object Handler { get; }
@@ -27,6 +27,7 @@ namespace Avalonia.Diagnostics.Models
{
return named.Name + " (" + Handler.GetType().Name + ")";
}
+
return Handler.GetType().Name;
}
}
diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs
index cda511909a..a66703301d 100644
--- a/src/Avalonia.Diagnostics/ViewLocator.cs
+++ b/src/Avalonia.Diagnostics/ViewLocator.cs
@@ -7,7 +7,7 @@ using Avalonia.Controls.Templates;
namespace Avalonia.Diagnostics
{
- public class ViewLocator : IDataTemplate
+ internal class ViewLocator : IDataTemplate
{
public bool SupportsRecycling => false;
diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
index d723890196..4b832f7ce6 100644
--- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -20,16 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public IEnumerable Classes
- {
- get;
- private set;
- }
-
- public IEnumerable Properties
- {
- get;
- private set;
- }
+ public IEnumerable Properties { get; }
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
index bc80ab0550..9f524a21eb 100644
--- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
diff --git a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
index 0674918400..7e38749a6f 100644
--- a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
@@ -13,22 +13,18 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
- private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[]
+ private static readonly RoutedEvent[] s_defaultEvents =
{
- Button.ClickEvent,
- InputElement.KeyDownEvent,
- InputElement.KeyUpEvent,
- InputElement.TextInputEvent,
- InputElement.PointerReleasedEvent,
- InputElement.PointerPressedEvent,
+ Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
+ InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
};
public EventOwnerTreeNode(Type type, IEnumerable events, EventsViewModel vm)
: base(null, type.Name)
{
- this.Children = new AvaloniaList(events.OrderBy(e => e.Name)
+ Children = new AvaloniaList(events.OrderBy(e => e.Name)
.Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
- this.IsExpanded = true;
+ IsExpanded = true;
}
public override bool? IsEnabled
@@ -39,6 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (base.IsEnabled != value)
{
base.IsEnabled = value;
+
if (_updateChildren && value != null)
{
foreach (var child in Children)
diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
index 7ece790310..36f1904253 100644
--- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
using Avalonia.Threading;
@@ -12,8 +11,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
- private RoutedEvent _event;
- private EventsViewModel _parentViewModel;
+ private readonly RoutedEvent _event;
+ private readonly EventsViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
@@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires(@event != null);
Contract.Requires(vm != null);
- this._event = @event;
- this._parentViewModel = vm;
+ _event = @event;
+ _parentViewModel = vm;
}
public override bool? IsEnabled
diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
index 146a8cea8e..4be4d8f74e 100644
--- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
@@ -12,10 +12,10 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _isExpanded;
private bool? _isEnabled = false;
- public EventTreeNodeBase(EventTreeNodeBase parent, string text)
+ protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
{
- this.Parent = parent;
- this.Text = text;
+ Parent = parent;
+ Text = text;
}
public IAvaloniaReadOnlyList Children
@@ -26,14 +26,14 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
- get { return _isExpanded; }
- set { RaiseAndSetIfChanged(ref _isExpanded, value); }
+ get => _isExpanded;
+ set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public virtual bool? IsEnabled
{
- get { return _isEnabled; }
- set { RaiseAndSetIfChanged(ref _isEnabled, value); }
+ get => _isEnabled;
+ set => RaiseAndSetIfChanged(ref _isEnabled, value);
}
public EventTreeNodeBase Parent
@@ -44,7 +44,6 @@ namespace Avalonia.Diagnostics.ViewModels
public string Text
{
get;
- private set;
}
internal void UpdateChecked()
@@ -55,7 +54,9 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (Children == null)
return false;
+
bool? value = false;
+
for (int i = 0; i < Children.Count; i++)
{
if (i == 0)
diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
index 049280c390..daf8ebd0f6 100644
--- a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.ObjectModel;
-
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
@@ -11,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class FiredEvent : ViewModelBase
{
- private RoutedEventArgs _eventArgs;
+ private readonly RoutedEventArgs _eventArgs;
private EventChainLink _handledBy;
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
@@ -19,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires(eventArgs != null);
Contract.Requires(originator != null);
- this._eventArgs = eventArgs;
- this.Originator = originator;
+ _eventArgs = eventArgs;
+ Originator = originator;
AddToChain(originator);
}
@@ -42,8 +41,9 @@ namespace Avalonia.Diagnostics.ViewModels
if (IsHandled)
{
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
- $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
+ $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
}
+
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
}
}
@@ -52,7 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels
public EventChainLink HandledBy
{
- get { return _handledBy; }
+ get => _handledBy;
set
{
if (_handledBy != value)
diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
index 638cf6c88f..0b9bd85b4f 100644
--- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
@@ -17,8 +17,7 @@ namespace Avalonia.Diagnostics.ViewModels
public static LogicalTreeNode[] Create(object control)
{
- var logical = control as ILogical;
- return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
+ return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null;
}
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
index 2609b74ce0..523be406c8 100644
--- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
@@ -26,7 +26,9 @@ namespace Avalonia.Diagnostics.ViewModels
Value = diagnostic.Value ?? "(null)";
Priority = (diagnostic.Priority != BindingPriority.Unset) ?
diagnostic.Priority.ToString() :
- diagnostic.Property.Inherits ? "Inherited" : "Unset";
+ diagnostic.Property.Inherits ?
+ "Inherited" :
+ "Unset";
Diagnostic = diagnostic.Diagnostic;
});
}
@@ -37,20 +39,20 @@ namespace Avalonia.Diagnostics.ViewModels
public string Priority
{
- get { return _priority; }
- private set { RaiseAndSetIfChanged(ref _priority, value); }
+ get => _priority;
+ private set => RaiseAndSetIfChanged(ref _priority, value);
}
public string Diagnostic
{
- get { return _diagnostic; }
- private set { RaiseAndSetIfChanged(ref _diagnostic, value); }
+ get => _diagnostic;
+ private set => RaiseAndSetIfChanged(ref _diagnostic, value);
}
public object Value
{
- get { return _value; }
- private set { RaiseAndSetIfChanged(ref _value, value); }
+ get => _value;
+ private set => RaiseAndSetIfChanged(ref _value, value);
}
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
index 7c403e1b04..902eb81bd9 100644
--- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
@@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels
var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
- x => styleable.Classes.CollectionChanged += x,
- x => styleable.Classes.CollectionChanged -= x)
- .TakeUntil(((IStyleable)styleable).StyleDetach);
+ x => styleable.Classes.CollectionChanged += x,
+ x => styleable.Classes.CollectionChanged -= x)
+ .TakeUntil(styleable.StyleDetach);
classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default)
@@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
public string Classes
{
- get { return _classes; }
- private set { RaiseAndSetIfChanged(ref _classes, value); }
+ get => _classes;
+ private set => RaiseAndSetIfChanged(ref _classes, value);
}
public IVisual Visual
@@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
- get { return _isExpanded; }
- set { RaiseAndSetIfChanged(ref _isExpanded, value); }
+ get => _isExpanded;
+ set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public TreeNode Parent
@@ -78,7 +78,6 @@ namespace Avalonia.Diagnostics.ViewModels
public string Type
{
get;
- private set;
}
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
index 6b294c98bd..b2b1aaa723 100644
--- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
@@ -23,7 +23,7 @@ namespace Avalonia.Diagnostics.ViewModels
public TreeNode SelectedNode
{
- get { return _selected; }
+ get => _selected;
set
{
if (RaiseAndSetIfChanged(ref _selected, value))
@@ -35,8 +35,8 @@ namespace Avalonia.Diagnostics.ViewModels
public ControlDetailsViewModel Details
{
- get { return _details; }
- private set { RaiseAndSetIfChanged(ref _details, value); }
+ get => _details;
+ private set => RaiseAndSetIfChanged(ref _details, value);
}
public TreeNode FindNode(IControl control)
@@ -66,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
control = control.GetVisualParent();
}
- }
+ }
if (node != null)
{
@@ -90,16 +90,14 @@ namespace Avalonia.Diagnostics.ViewModels
{
return node;
}
- else
+
+ foreach (var child in node.Children)
{
- foreach (var child in node.Children)
- {
- var result = FindNode(child, control);
+ var result = FindNode(child, control);
- if (result != null)
- {
- return result;
- }
+ if (result != null)
+ {
+ return result;
}
}
diff --git a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
index 00660754c0..a6ff4dd853 100644
--- a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
@@ -1,11 +1,14 @@
-using System.Collections.Generic;
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Avalonia.Diagnostics.ViewModels
{
- public class ViewModelBase : INotifyPropertyChanged
+ internal class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
diff --git a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
index 8c070261d9..47ef91507a 100644
--- a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
+++ b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
@@ -29,12 +29,11 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
- public bool IsInTemplate { get; private set; }
+ public bool IsInTemplate { get; }
public static VisualTreeNode[] Create(object control)
{
- var visual = control as IVisual;
- return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
+ return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null;
}
}
}
diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
index 868bc774bb..fb867ab55e 100644
--- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
+++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
@@ -14,6 +14,7 @@ namespace Avalonia.Diagnostics.Views
{
private static readonly StyledProperty ViewModelProperty =
AvaloniaProperty.Register(nameof(ViewModel));
+
private SimpleGrid _grid;
public ControlDetailsView()
@@ -25,7 +26,7 @@ namespace Avalonia.Diagnostics.Views
public ControlDetailsViewModel ViewModel
{
- get { return GetValue(ViewModelProperty); }
+ get => GetValue(ViewModelProperty);
private set
{
SetValue(ViewModelProperty, value);
@@ -37,13 +38,7 @@ namespace Avalonia.Diagnostics.Views
{
Func> pt = PropertyTemplate;
- Content = new ScrollViewer
- {
- Content = _grid = new SimpleGrid
- {
- [GridRepeater.TemplateProperty] = pt,
- }
- };
+ Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } };
}
private IEnumerable PropertyTemplate(object i)
@@ -57,7 +52,7 @@ namespace Avalonia.Diagnostics.Views
Margin = margin,
Text = property.Name,
TextWrapping = TextWrapping.NoWrap,
- [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(),
+ [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding()
};
yield return new TextBlock
@@ -66,14 +61,14 @@ namespace Avalonia.Diagnostics.Views
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value))
.Select(v => v?.ToString())
- .ToBinding(),
+ .ToBinding()
};
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
- [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(),
+ [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding()
};
}
}
diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Views/EventsView.xaml
index 8d4d37f7b3..406dd433a2 100644
--- a/src/Avalonia.Diagnostics/Views/EventsView.xaml
+++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml
@@ -2,53 +2,57 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.EventsView">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs b/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
index a51cb4b7d9..41d74659ca 100644
--- a/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
-
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Markup.Xaml;
@@ -11,15 +10,16 @@ namespace Avalonia.Diagnostics.Views
{
public class EventsView : UserControl
{
- private ListBox _events;
+ private readonly ListBox _events;
public EventsView()
{
- this.InitializeComponent();
+ InitializeComponent();
_events = this.FindControl("events");
}
- private void RecordedEvents_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+ private void RecordedEvents_CollectionChanged(object sender,
+ System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
_events.ScrollIntoView(_events.Items.OfType().LastOrDefault());
}
diff --git a/src/Avalonia.Diagnostics/Views/GridRepeater.cs b/src/Avalonia.Diagnostics/Views/GridRepeater.cs
index ff12fde25b..b0ff26c7b6 100644
--- a/src/Avalonia.Diagnostics/Views/GridRepeater.cs
+++ b/src/Avalonia.Diagnostics/Views/GridRepeater.cs
@@ -14,7 +14,8 @@ namespace Avalonia.Diagnostics.Views
AvaloniaProperty.RegisterAttached("Items", typeof(GridRepeater));
public static readonly AttachedProperty>> TemplateProperty =
- AvaloniaProperty.RegisterAttached>>("Template", typeof(GridRepeater));
+ AvaloniaProperty.RegisterAttached>>("Template",
+ typeof(GridRepeater));
static GridRepeater()
{
diff --git a/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
index 0bd08929ad..24b2f29463 100644
--- a/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
+++ b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
@@ -23,11 +23,11 @@ namespace Avalonia.Diagnostics.Views
}
return Observable.FromEventPattern(
- e => source.PropertyChanged += e,
- e => source.PropertyChanged -= e)
- .Where(e => e.EventArgs.PropertyName == propertyName)
- .Select(_ => (T)property.GetValue(source))
- .StartWith((T)property.GetValue(source));
+ e => source.PropertyChanged += e,
+ e => source.PropertyChanged -= e)
+ .Where(e => e.EventArgs.PropertyName == propertyName)
+ .Select(_ => (T)property.GetValue(source))
+ .StartWith((T)property.GetValue(source));
}
}
}
diff --git a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs b/src/Avalonia.Diagnostics/Views/SimpleGrid.cs
index 33d9d81ed7..4fc77666e1 100644
--- a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs
+++ b/src/Avalonia.Diagnostics/Views/SimpleGrid.cs
@@ -15,8 +15,8 @@ namespace Avalonia.Diagnostics.Views
///
public class SimpleGrid : Panel
{
- private List _columnWidths = new List();
- private List _rowHeights = new List();
+ private readonly List _columnWidths = new List();
+ private readonly List _rowHeights = new List();
private double _totalWidth;
private double _totalHeight;
@@ -31,7 +31,7 @@ namespace Avalonia.Diagnostics.Views
///
public static readonly AttachedProperty RowProperty =
AvaloniaProperty.RegisterAttached("Row");
-
+
///
/// Gets the value of the Column attached property for a control.
///
@@ -62,7 +62,7 @@ namespace Avalonia.Diagnostics.Views
control.SetValue(ColumnProperty, value);
}
-
+
///
/// Sets the value of the Row attached property for a control.
///
diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml
index e927bcdf29..ca7314264a 100644
--- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml
+++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml
@@ -2,25 +2,25 @@
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.Views.TreePageView">
-
+
-
-
+
+
-
-
+
+
diff --git a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
similarity index 98%
rename from src/Avalonia.Diagnostics/Views/TreePage.xaml.cs
rename to src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
index 88cbb03c34..ddf1473b45 100644
--- a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs
+++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
@@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.Views
public TreePageView()
{
- this.InitializeComponent();
+ InitializeComponent();
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
}
@@ -39,7 +39,7 @@ namespace Avalonia.Diagnostics.Views
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),
- [AdornerLayer.AdornedElementProperty] = node.Visual,
+ [AdornerLayer.AdornedElementProperty] = node.Visual
};
layer.Children.Add(_adorner);
diff --git a/src/Avalonia.Diagnostics/Debug.cs b/src/Avalonia.Diagnostics/VisualTreeDebug.cs
similarity index 93%
rename from src/Avalonia.Diagnostics/Debug.cs
rename to src/Avalonia.Diagnostics/VisualTreeDebug.cs
index af3b5a8552..47511e0508 100644
--- a/src/Avalonia.Diagnostics/Debug.cs
+++ b/src/Avalonia.Diagnostics/VisualTreeDebug.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Linq;
using System.Text;
using Avalonia.Controls;
using Avalonia.Data;
@@ -10,7 +9,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics
{
- public static class Debug
+ public static class VisualTreeDebug
{
public static string PrintVisualTree(IVisual visual)
{
@@ -67,7 +66,7 @@ namespace Avalonia.Diagnostics
private static string Indent(int indent)
{
- return string.Join(string.Empty, Enumerable.Repeat(" ", Math.Max(indent, 0)));
+ return new string(' ' , Math.Max(indent, 0) * 4);
}
}
}
diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs
new file mode 100644
index 0000000000..5622731a7c
--- /dev/null
+++ b/src/Avalonia.Layout/AttachedLayout.cs
@@ -0,0 +1,106 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the base class for an object that sizes and arranges child elements for a host.
+ ///
+ public abstract class AttachedLayout : AvaloniaObject
+ {
+ internal string LayoutId { get; set; }
+
+ ///
+ /// Occurs when the measurement state (layout) has been invalidated.
+ ///
+ public event EventHandler MeasureInvalidated;
+
+ ///
+ /// Occurs when the arrange state (layout) has been invalidated.
+ ///
+ public event EventHandler ArrangeInvalidated;
+
+ ///
+ /// Initializes any per-container state the layout requires when it is attached to an
+ /// container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// Container elements that support attached layouts should call this method when a layout
+ /// instance is first assigned. The container is expected to give the attached layout
+ /// instance a way to store and retrieve any per-container state by way of the provided
+ /// context. It is also the responsibility of the container to not reuse the context, or
+ /// otherwise expose the state from one layout to another.
+ ///
+ /// When an attached layout is removed the container should release any reference to the
+ /// layout state it stored.
+ ///
+ /// Override or
+ /// to provide the behavior for
+ /// this method in a derived class.
+ ///
+ public abstract void InitializeForContext(LayoutContext context);
+
+ ///
+ /// Removes any state the layout previously stored on the ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ public abstract void UninitializeForContext(LayoutContext context);
+
+ ///
+ /// Suggests a DesiredSize for a container element. A container element that supports
+ /// attached layouts should call this method from their own MeasureOverride implementations
+ /// to form a recursive layout update. The attached layout is expected to call the Measure
+ /// for each of the container’s ILayoutable children.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The available space that a container can allocate to a child object. A child object can
+ /// request a larger space than what is available; the provided size might be accommodated
+ /// if scrolling or other resize behavior is possible in that particular container.
+ ///
+ ///
+ public abstract Size Measure(LayoutContext context, Size availableSize);
+
+ ///
+ /// Positions child elements and determines a size for a container UIElement. Container
+ /// elements that support attached layouts should call this method from their layout
+ /// override implementations to form a recursive layout update.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The final size that the container computes for the child in layout.
+ ///
+ /// The actual size that is used after the element is arranged in layout.
+ public abstract Size Arrange(LayoutContext context, Size finalSize);
+
+ ///
+ /// Invalidates the measurement state (layout) for all ILayoutable containers that reference
+ /// this layout.
+ ///
+ protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty);
+
+ ///
+ /// Invalidates the arrange state (layout) for all UIElement containers that reference this
+ /// layout. After the invalidation, the UIElement will have its layout updated, which
+ /// occurs asynchronously.
+ ///
+ protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs
new file mode 100644
index 0000000000..1748a3be03
--- /dev/null
+++ b/src/Avalonia.Layout/ElementManager.cs
@@ -0,0 +1,460 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Layout.Utils;
+
+namespace Avalonia.Layout
+{
+ 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(ScrollOrientation 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 ILayoutable GetAt(int realizedIndex)
+ {
+ ILayoutable 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(ILayoutable element, int dataIndex)
+ {
+ if (_realizedElements.Count == 0)
+ {
+ _firstRealizedDataIndex = dataIndex;
+ }
+
+ _realizedElements.Add(element);
+ _realizedElementLayoutBounds.Add(default);
+ }
+
+ public void Insert(int realizedIndex, int dataIndex, ILayoutable 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 ILayoutable 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, ScrollOrientation orientation, bool scrollOrientationSameAsFlow)
+ {
+ bool intersects = false;
+
+ if (_realizedElementLayoutBounds.Count > 0)
+ {
+ var firstElementBounds = GetLayoutBoundsForRealizedIndex(0);
+ var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1);
+
+ var effectiveOrientation = scrollOrientationSameAsFlow ?
+ (orientation == ScrollOrientation.Vertical ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical) :
+ orientation;
+
+
+ var windowStart = effectiveOrientation == ScrollOrientation.Vertical ? window.Y : window.X;
+ var windowEnd = effectiveOrientation == ScrollOrientation.Vertical ? window.Y + window.Height : window.X + window.Width;
+ var firstElementStart = effectiveOrientation == ScrollOrientation.Vertical ? firstElementBounds.Y : firstElementBounds.X;
+ var lastElementEnd = effectiveOrientation == ScrollOrientation.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:
+ {
+ int oldSize = args.OldItems.Count;
+ int newSize = args.NewItems.Count;
+ int oldStartIndex = args.OldStartingIndex;
+ int newStartIndex = args.NewStartingIndex;
+
+ if (oldSize == newSize &&
+ oldStartIndex == newStartIndex &&
+ IsDataIndexRealized(oldStartIndex) &&
+ IsDataIndexRealized(oldStartIndex + oldSize -1))
+ {
+ // Straight up replace of n items within the realization window.
+ // Removing and adding might causes us to lose the anchor causing us
+ // to throw away all containers and start from scratch.
+ // Instead, we can just clear those items and set the element to
+ // null (sentinel) and let the next measure get new containers for them.
+ var startRealizedIndex = GetRealizedRangeIndexFromDataIndex(oldStartIndex);
+ for (int realizedIndex = startRealizedIndex; realizedIndex < startRealizedIndex + oldSize; realizedIndex++)
+ {
+ var elementRef = _realizedElements[realizedIndex];
+
+ if (elementRef != null)
+ {
+ _context.RecycleElement(elementRef);
+ _realizedElements[realizedIndex] = null;
+ }
+ }
+ }
+ else
+ {
+ OnItemsRemoved(oldStartIndex, oldSize);
+ OnItemsAdded(newStartIndex, newSize);
+ }
+ }
+ 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(ILayoutable 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, ScrollOrientation 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, ScrollOrientation orientation)
+ {
+ var lhsStart = orientation == ScrollOrientation.Vertical ? lhs.Y : lhs.X;
+ var lhsEnd = orientation == ScrollOrientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width;
+ var rhsStart = orientation == ScrollOrientation.Vertical ? rhs.Y : rhs.X;
+ var rhsEnd = orientation == ScrollOrientation.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.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs
new file mode 100644
index 0000000000..615ce725bd
--- /dev/null
+++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs
@@ -0,0 +1,712 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Specialized;
+
+namespace Avalonia.Layout
+{
+ 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,
+ ScrollOrientation 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(
+ ILayoutable 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;
+ int count = 0;
+ 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);
+ ++count;
+
+ // 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 (count > 0)
+ {
+ 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);
+
+ // 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)
+ {
+ ILayoutable firstRealizedElement = null;
+ Rect firstBounds = new Rect();
+ ILayoutable 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)
+ {
+ 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);
+ }
+ }
+
+ private void SetLayoutOrigin()
+ {
+ if (IsVirtualizingContext)
+ {
+ _context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y);
+ }
+ }
+
+ public ILayoutable GetElementIfRealized(int dataIndex)
+ {
+ if (_elementManager.IsDataIndexRealized(dataIndex))
+ {
+ return _elementManager.GetRealizedElement(dataIndex);
+ }
+
+ return null;
+ }
+
+ public bool TryAddElement0(ILayoutable element)
+ {
+ if (_elementManager.GetRealizedElementCount() == 0)
+ {
+ _elementManager.Add(element, 0);
+ return true;
+ }
+
+ return false;
+ }
+
+ public enum LineAlignment
+ {
+ Start,
+ Center,
+ End,
+ SpaceAround,
+ SpaceBetween,
+ SpaceEvenly,
+ }
+
+ private enum GenerateDirection
+ {
+ Forward,
+ Backward,
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs
new file mode 100644
index 0000000000..907a3adf0f
--- /dev/null
+++ b/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs
@@ -0,0 +1,44 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+namespace Avalonia.Layout
+{
+ 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,
+ ILayoutable firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ ILayoutable lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds);
+ void Algorithm_OnElementMeasured(
+ ILayoutable 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.Layout/LayoutContext.cs b/src/Avalonia.Layout/LayoutContext.cs
new file mode 100644
index 0000000000..45a8048ea2
--- /dev/null
+++ b/src/Avalonia.Layout/LayoutContext.cs
@@ -0,0 +1,24 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the base class for an object that facilitates communication between an attached
+ /// layout and its host container.
+ ///
+ public class LayoutContext : AvaloniaObject
+ {
+ ///
+ /// Gets or sets an object that represents the state of a layout.
+ ///
+ public object LayoutState { get; set; }
+
+ ///
+ /// Implements the behavior of in a derived or custom LayoutContext.
+ ///
+ protected virtual object LayoutStateCore { get; set; }
+ }
+}
diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs
new file mode 100644
index 0000000000..fba91e66c7
--- /dev/null
+++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs
@@ -0,0 +1,103 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the base class for an object that sizes and arranges child elements for a host
+ /// and and does not support virtualization.
+ ///
+ ///
+ /// NonVirtualizingLayout is the base class for layouts that do not support virtualization. You
+ /// can inherit from it to create your own layout.
+ ///
+ /// A non-virtualizing layout can measure and arrange child elements.
+ ///
+ public abstract class NonVirtualizingLayout : AttachedLayout
+ {
+ ///
+ public sealed override void InitializeForContext(LayoutContext context)
+ {
+ InitializeForContextCore((VirtualizingLayoutContext)context);
+ }
+
+ ///
+ public sealed override void UninitializeForContext(LayoutContext context)
+ {
+ UninitializeForContextCore((VirtualizingLayoutContext)context);
+ }
+
+ ///
+ public sealed override Size Measure(LayoutContext context, Size availableSize)
+ {
+ return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
+ }
+
+ ///
+ public sealed override Size Arrange(LayoutContext context, Size finalSize)
+ {
+ return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
+ }
+
+ ///
+ /// When overridden in a derived class, initializes any per-container state the layout
+ /// requires when it is attached to an ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ }
+
+ ///
+ /// When overridden in a derived class, removes any state the layout previously stored on
+ /// the ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ }
+
+ ///
+ /// Provides the behavior for the "Measure" pass of the layout cycle. Classes can override
+ /// this method to define their own "Measure" pass behavior.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The available size that this object can give to child objects. Infinity can be
+ /// specified as a value to indicate that the object will size to whatever content is
+ /// available.
+ ///
+ ///
+ /// The size that this object determines it needs during layout, based on its calculations
+ /// of the allocated sizes for child objects or based on other considerations such as a
+ /// fixed container size.
+ ///
+ protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+
+ ///
+ /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
+ /// layout. Classes can override this method to define their own "Arrange" pass behavior.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The final area within the container that this object should use to arrange itself and
+ /// its children.
+ ///
+ /// The actual size that is used after the element is arranged in layout.
+ protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+ }
+}
diff --git a/src/Avalonia.Controls/Orientation.cs b/src/Avalonia.Layout/Orientation.cs
similarity index 94%
rename from src/Avalonia.Controls/Orientation.cs
rename to src/Avalonia.Layout/Orientation.cs
index fe998c024a..f03b087adc 100644
--- a/src/Avalonia.Controls/Orientation.cs
+++ b/src/Avalonia.Layout/Orientation.cs
@@ -1,7 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
-namespace Avalonia.Controls
+namespace Avalonia.Layout
{
///
/// Defines vertical or horizontal orientation.
diff --git a/src/Avalonia.Layout/OrientationBasedMeasures.cs b/src/Avalonia.Layout/OrientationBasedMeasures.cs
new file mode 100644
index 0000000000..23a8b0e168
--- /dev/null
+++ b/src/Avalonia.Layout/OrientationBasedMeasures.cs
@@ -0,0 +1,96 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+namespace Avalonia.Layout
+{
+ internal enum ScrollOrientation
+ {
+ Vertical,
+ Horizontal,
+ }
+
+ internal class OrientationBasedMeasures
+ {
+ public ScrollOrientation ScrollOrientation { get; set; } = ScrollOrientation.Vertical;
+
+ public double Major(in Size size) => ScrollOrientation == ScrollOrientation.Vertical ? size.Height : size.Width;
+ public double Minor(in Size size) => ScrollOrientation == ScrollOrientation.Vertical ? size.Width : size.Height;
+ public double MajorSize(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Height : rect.Width;
+ public double MinorSize(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Width : rect.Height;
+ public double MajorStart(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Y : rect.X;
+ public double MinorStart(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.X : rect.Y;
+ public double MajorEnd(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Bottom : rect.Right;
+ public double MinorEnd(in Rect rect) => ScrollOrientation == ScrollOrientation.Vertical ? rect.Right : rect.Bottom;
+
+ public void SetMajorSize(ref Rect rect, double value)
+ {
+ if (ScrollOrientation == ScrollOrientation.Vertical)
+ {
+ rect = rect.WithHeight(value);
+ }
+ else
+ {
+ rect = rect.WithWidth(value);
+ }
+ }
+
+ public void SetMinorSize(ref Rect rect, double value)
+ {
+ if (ScrollOrientation == ScrollOrientation.Vertical)
+ {
+ rect = rect.WithWidth(value);
+ }
+ else
+ {
+ rect = rect.WithHeight(value);
+ }
+ }
+
+ public void SetMajorStart(ref Rect rect, double value)
+ {
+ if (ScrollOrientation == ScrollOrientation.Vertical)
+ {
+ rect = rect.WithY(value);
+ }
+ else
+ {
+ rect = rect.WithX(value);
+ }
+ }
+
+ public void SetMinorStart(ref Rect rect, double value)
+ {
+ if (ScrollOrientation == ScrollOrientation.Vertical)
+ {
+ rect = rect.WithX(value);
+ }
+ else
+ {
+ rect = rect.WithY(value);
+ }
+ }
+
+ public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize)
+ {
+ return ScrollOrientation == ScrollOrientation.Vertical ?
+ new Rect(minor, major, minorSize, majorSize) :
+ new Rect(major, minor, majorSize, minorSize);
+ }
+
+ public Point MinorMajorPoint(double minor, double major)
+ {
+ return ScrollOrientation == ScrollOrientation.Vertical ?
+ new Point(minor, major) :
+ new Point(major, minor);
+ }
+
+ public Size MinorMajorSize(double minor, double major)
+ {
+ return ScrollOrientation == ScrollOrientation.Vertical ?
+ new Size(minor, major) :
+ new Size(major, minor);
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs
new file mode 100644
index 0000000000..e9735b9b31
--- /dev/null
+++ b/src/Avalonia.Layout/StackLayout.cs
@@ -0,0 +1,336 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically.
+ ///
+ public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OrientationProperty =
+ AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty SpacingProperty =
+ AvaloniaProperty.Register(nameof(Spacing));
+
+ private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
+
+ ///
+ /// Initializes a new instance of the StackLayout class.
+ ///
+ public StackLayout()
+ {
+ LayoutId = "StackLayout";
+ }
+
+ ///
+ /// Gets or sets the axis along which items are laid out.
+ ///
+ ///
+ /// One of the enumeration values that specifies the axis along which items are laid out.
+ /// The default is Vertical.
+ ///
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ ///
+ /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the
+ /// direction of the StackLayout's Orientation.
+ ///
+ public double Spacing
+ {
+ get => GetValue(SpacingProperty);
+ set => SetValue(SpacingProperty, value);
+ }
+
+ internal Rect GetExtent(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ ILayoutable firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ ILayoutable lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds)
+ {
+ var extent = new Rect();
+
+ // Constants
+ int itemsCount = context.ItemCount;
+ var stackState = (StackLayoutState)context.LayoutState;
+ double averageElementSize = GetAverageElementSize(availableSize, context, stackState) + Spacing;
+
+ _orientation.SetMinorSize(ref extent, stackState.MaxArrangeBounds);
+ _orientation.SetMajorSize(ref extent, Math.Max(0.0f, itemsCount * averageElementSize - Spacing));
+ if (itemsCount > 0)
+ {
+ if (firstRealized != null)
+ {
+ _orientation.SetMajorStart(
+ ref extent,
+ _orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize);
+ var remainingItems = itemsCount - lastRealizedItemIndex - 1;
+ _orientation.SetMajorSize(
+ ref extent,
+ _orientation.MajorEnd(lastRealizedLayoutBounds) -
+ _orientation.MajorStart(extent) +
+ (remainingItems * averageElementSize));
+ }
+ }
+
+ return extent;
+ }
+
+ internal void OnElementMeasured(
+ ILayoutable element,
+ int index,
+ Size availableSize,
+ Size measureSize,
+ Size desiredSize,
+ Size provisionalArrangeSize,
+ VirtualizingLayoutContext context)
+ {
+ if (context is VirtualizingLayoutContext virtualContext)
+ {
+ var stackState = (StackLayoutState)virtualContext.LayoutState;
+ var provisionalArrangeSizeWinRt = provisionalArrangeSize;
+ stackState.OnElementMeasured(
+ index,
+ _orientation.Major(provisionalArrangeSizeWinRt),
+ _orientation.Minor(provisionalArrangeSizeWinRt));
+ }
+ }
+
+ Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize(
+ int index,
+ Size availableSize,
+ VirtualizingLayoutContext context) => availableSize;
+
+ Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize(
+ int index,
+ Size measureSize,
+ Size desiredSize,
+ VirtualizingLayoutContext context)
+ {
+ var measureSizeMinor = _orientation.Minor(measureSize);
+ return _orientation.MinorMajorSize(
+ !double.IsInfinity(measureSizeMinor) ?
+ Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) :
+ _orientation.Minor(desiredSize),
+ _orientation.Major(desiredSize));
+ }
+
+ bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => true;
+
+ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect(
+ Size availableSize,
+ VirtualizingLayoutContext context) => GetAnchorForRealizationRect(availableSize, context);
+
+ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement(
+ int targetIndex,
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ double offset = double.NaN;
+ int index = -1;
+ int itemsCount = context.ItemCount;
+
+ if (targetIndex >= 0 && targetIndex < itemsCount)
+ {
+ index = targetIndex;
+ var state = (StackLayoutState)context.LayoutState;
+ double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing;
+ offset = index * averageElementSize + _orientation.MajorStart(state.FlowAlgorithm.LastExtent);
+ }
+
+ return new FlowLayoutAnchorInfo { Index = index, Offset = offset };
+ }
+
+ Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ ILayoutable firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ ILayoutable lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds)
+ {
+ return GetExtent(
+ availableSize,
+ context,
+ firstRealized,
+ firstRealizedItemIndex,
+ firstRealizedLayoutBounds,
+ lastRealized,
+ lastRealizedItemIndex,
+ lastRealizedLayoutBounds);
+ }
+
+ void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context)
+ {
+ OnElementMeasured(
+ element,
+ index,
+ availableSize,
+ measureSize,
+ desiredSize,
+ provisionalArrangeSize,
+ context);
+ }
+
+ void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context)
+ {
+ }
+
+ internal FlowLayoutAnchorInfo GetAnchorForRealizationRect(
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ int anchorIndex = -1;
+ double offset = double.NaN;
+
+ // Constants
+ int itemsCount = context.ItemCount;
+ if (itemsCount > 0)
+ {
+ var realizationRect = context.RealizationRect;
+ var state = (StackLayoutState)context.LayoutState;
+ var lastExtent = state.FlowAlgorithm.LastExtent;
+
+ double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing;
+ double realizationWindowOffsetInExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
+ double majorSize = _orientation.MajorSize(lastExtent) == 0 ? Math.Max(0.0, averageElementSize * itemsCount - Spacing) : _orientation.MajorSize(lastExtent);
+ if (itemsCount > 0 &&
+ _orientation.MajorSize(realizationRect) >= 0 &&
+ // MajorSize = 0 will account for when a nested repeater is outside the realization rect but still being measured. Also,
+ // note that if we are measuring this repeater, then we are already realizing an element to figure out the size, so we could
+ // just keep that element alive. It also helps in XYFocus scenarios to have an element realized for XYFocus to find a candidate
+ // in the navigating direction.
+ realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize)
+ {
+ anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize);
+ offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
+ anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex));
+ }
+ }
+
+ return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, };
+ }
+
+ protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var state = context.LayoutState;
+ var stackState = state as StackLayoutState;
+
+ if (stackState == null)
+ {
+ if (state != null)
+ {
+ throw new InvalidOperationException("LayoutState must derive from StackLayoutState.");
+ }
+
+ // Custom deriving layouts could potentially be stateful.
+ // If that is the case, we will just create the base state required by UniformGridLayout ourselves.
+ stackState = new StackLayoutState();
+ }
+
+ stackState.InitializeForContext(context, this);
+ }
+
+ protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var stackState = (StackLayoutState)context.LayoutState;
+ stackState.UninitializeForContext(context);
+ }
+
+ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ {
+ var desiredSize = GetFlowAlgorithm(context).Measure(
+ availableSize,
+ context,
+ false,
+ 0,
+ Spacing,
+ _orientation.ScrollOrientation,
+ LayoutId);
+
+ return new Size(desiredSize.Width, desiredSize.Height);
+ }
+
+ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ {
+ var value = GetFlowAlgorithm(context).Arrange(
+ finalSize,
+ context,
+ FlowLayoutAlgorithm.LineAlignment.Start,
+ LayoutId);
+
+ ((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
+
+ return new Size(value.Width, value.Height);
+ }
+
+ protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
+ {
+ GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context);
+ // Always invalidate layout to keep the view accurate.
+ InvalidateLayout();
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == OrientationProperty)
+ {
+ var orientation = (Orientation)e.NewValue;
+
+ //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
+ //Horizontal Orientation means we have a Horizontal ScrollOrientation.
+ _orientation.ScrollOrientation = orientation == Orientation.Horizontal ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical;
+ }
+
+ InvalidateLayout();
+ }
+
+ private double GetAverageElementSize(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ StackLayoutState stackLayoutState)
+ {
+ double averageElementSize = 0;
+
+ if (context.ItemCount > 0)
+ {
+ if (stackLayoutState.TotalElementsMeasured == 0)
+ {
+ var tmpElement = context.GetOrCreateElementAt(0, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
+ stackLayoutState.FlowAlgorithm.MeasureElement(tmpElement, 0, availableSize, context);
+ context.RecycleElement(tmpElement);
+ }
+
+ averageElementSize = Math.Round(stackLayoutState.TotalElementSize / stackLayoutState.TotalElementsMeasured);
+ }
+
+ return averageElementSize;
+ }
+
+ private void InvalidateLayout() => InvalidateMeasure();
+
+ private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState).FlowAlgorithm;
+ }
+}
diff --git a/src/Avalonia.Layout/StackLayoutState.cs b/src/Avalonia.Layout/StackLayoutState.cs
new file mode 100644
index 0000000000..05ad9bca8e
--- /dev/null
+++ b/src/Avalonia.Layout/StackLayoutState.cs
@@ -0,0 +1,61 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the state of a StackLayout.
+ ///
+ public class StackLayoutState
+ {
+ private const int BufferSize = 100;
+ private readonly List _estimationBuffer = new List();
+
+ internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm();
+ internal double MaxArrangeBounds { get; private set; }
+ internal int TotalElementsMeasured { get; private set; }
+ internal double TotalElementSize { get; private set; }
+
+ internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
+ {
+ FlowAlgorithm.InitializeForContext(context, callbacks);
+
+ if (_estimationBuffer.Count == 0)
+ {
+ _estimationBuffer.AddRange(Enumerable.Repeat(0.0, BufferSize));
+ }
+
+ context.LayoutState = this;
+ }
+
+ internal void UninitializeForContext(VirtualizingLayoutContext context)
+ {
+ FlowAlgorithm.UninitializeForContext(context);
+ }
+
+ internal void OnElementMeasured(int elementIndex, double majorSize, double minorSize)
+ {
+ int estimationBufferIndex = elementIndex % _estimationBuffer.Count;
+ bool alreadyMeasured = _estimationBuffer[estimationBufferIndex] != 0;
+
+ if (!alreadyMeasured)
+ {
+ TotalElementsMeasured++;
+ }
+
+ TotalElementSize -= _estimationBuffer[estimationBufferIndex];
+ TotalElementSize += majorSize;
+ _estimationBuffer[estimationBufferIndex] = majorSize;
+
+ MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize);
+ }
+
+ internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0;
+ }
+}
diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs
new file mode 100644
index 0000000000..edc2042922
--- /dev/null
+++ b/src/Avalonia.Layout/UniformGridLayout.cs
@@ -0,0 +1,520 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis.
+ ///
+ public enum UniformGridLayoutItemsJustification
+ {
+ ///
+ /// Items are aligned with the start of the row or column, with extra space at the end.
+ /// Spacing between items does not change.
+ ///
+ Start = 0,
+
+ ///
+ /// Items are aligned in the center of the row or column, with extra space at the start and
+ /// end. Spacing between items does not change.
+ ///
+ Center = 1,
+
+ ///
+ /// Items are aligned with the end of the row or column, with extra space at the start.
+ /// Spacing between items does not change.
+ ///
+ End = 2,
+
+ ///
+ /// Items are aligned so that extra space is added evenly before and after each item.
+ ///
+ SpaceAround = 3,
+
+ ///
+ /// Items are aligned so that extra space is added evenly between adjacent items. No space
+ /// is added at the start or end.
+ ///
+ SpaceBetween = 4,
+
+ SpaceEvenly = 5,
+ };
+
+ ///
+ /// Defines constants that specify how items are sized to fill the available space.
+ ///
+ public enum UniformGridLayoutItemsStretch
+ {
+ ///
+ /// The item retains its natural size. Use of extra space is determined by the
+ /// property.
+ ///
+ None = 0,
+
+ ///
+ /// The item is sized to fill the available space in the non-scrolling direction. Item size
+ /// in the scrolling direction is not changed.
+ ///
+ Fill = 1,
+
+ ///
+ /// The item is sized to both fill the available space in the non-scrolling direction and
+ /// maintain its aspect ratio.
+ ///
+ Uniform = 2,
+ };
+
+ ///
+ /// Positions elements sequentially from left to right or top to bottom in a wrapping layout.
+ ///
+ public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ItemsJustificationProperty =
+ AvaloniaProperty.Register(nameof(ItemsJustification));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ItemsStretchProperty =
+ AvaloniaProperty.Register(nameof(ItemsStretch));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinColumnSpacingProperty =
+ AvaloniaProperty.Register(nameof(MinColumnSpacing));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinItemHeightProperty =
+ AvaloniaProperty.Register(nameof(MinItemHeight));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinItemWidthProperty =
+ AvaloniaProperty.Register(nameof(MinItemWidth));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinRowSpacingProperty =
+ AvaloniaProperty.Register(nameof(MinRowSpacing));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty OrientationProperty =
+ StackLayout.OrientationProperty.AddOwner();
+
+ private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
+ private double _minItemWidth = double.NaN;
+ private double _minItemHeight = double.NaN;
+ private double _minRowSpacing;
+ private double _minColumnSpacing;
+ private UniformGridLayoutItemsJustification _itemsJustification;
+ private UniformGridLayoutItemsStretch _itemsStretch;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UniformGridLayout()
+ {
+ LayoutId = "UniformGridLayout";
+ }
+
+ static UniformGridLayout()
+ {
+ OrientationProperty.OverrideDefaultValue(Orientation.Horizontal);
+ }
+
+ ///
+ /// Gets or sets a value that indicates how items are aligned on the non-scrolling or non-
+ /// virtualizing axis.
+ ///
+ ///
+ /// An enumeration value that indicates how items are aligned. The default is Start.
+ ///
+ public UniformGridLayoutItemsJustification ItemsJustification
+ {
+ get => GetValue(ItemsJustificationProperty);
+ set => SetValue(ItemsJustificationProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value that indicates how items are sized to fill the available space.
+ ///
+ ///
+ /// An enumeration value that indicates how items are sized to fill the available space.
+ /// The default is None.
+ ///
+ ///
+ /// This property enables adaptive layout behavior where the items are sized to fill the
+ /// available space along the non-scrolling axis, and optionally maintain their aspect ratio.
+ ///
+ public UniformGridLayoutItemsStretch ItemsStretch
+ {
+ get => GetValue(ItemsStretchProperty);
+ set => SetValue(ItemsStretchProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum space between items on the horizontal axis.
+ ///
+ ///
+ /// The spacing may exceed this minimum value when is set
+ /// to SpaceEvenly, SpaceAround, or SpaceBetween.
+ ///
+ public double MinColumnSpacing
+ {
+ get => GetValue(MinColumnSpacingProperty);
+ set => SetValue(MinColumnSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum height of each item.
+ ///
+ ///
+ /// The minimum height (in pixels) of each item. The default is NaN, in which case the
+ /// height of the first item is used as the minimum.
+ ///
+ public double MinItemHeight
+ {
+ get => GetValue(MinItemHeightProperty);
+ set => SetValue(MinItemHeightProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum width of each item.
+ ///
+ ///
+ /// The minimum width (in pixels) of each item. The default is NaN, in which case the width
+ /// of the first item is used as the minimum.
+ ///
+ public double MinItemWidth
+ {
+ get => GetValue(MinItemWidthProperty);
+ set => SetValue(MinItemWidthProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum space between items on the vertical axis.
+ ///
+ ///
+ /// The spacing may exceed this minimum value when is set
+ /// to SpaceEvenly, SpaceAround, or SpaceBetween.
+ ///
+ public double MinRowSpacing
+ {
+ get => GetValue(MinRowSpacingProperty);
+ set => SetValue(MinRowSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the axis along which items are laid out.
+ ///
+ ///
+ /// One of the enumeration values that specifies the axis along which items are laid out.
+ /// The default is Vertical.
+ ///
+ public Orientation Orientation
+ {
+ get => GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ internal double LineSpacing => Orientation == Orientation.Horizontal ? _minRowSpacing : _minColumnSpacing;
+ internal double MinItemSpacing => Orientation == Orientation.Horizontal ? _minColumnSpacing : _minRowSpacing;
+
+ Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize(
+ int index,
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight);
+ }
+
+ Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize(
+ int index,
+ Size measureSize,
+ Size desiredSize,
+ VirtualizingLayoutContext context)
+ {
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight);
+ }
+
+ bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => remainingSpace < 0;
+
+ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect(
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ Rect bounds = new Rect(double.NaN, double.NaN, double.NaN, double.NaN);
+ int anchorIndex = -1;
+
+ int itemsCount = context.ItemCount;
+ var realizationRect = context.RealizationRect;
+ if (itemsCount > 0 && _orientation.MajorSize(realizationRect) > 0)
+ {
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ var lastExtent = gridState.FlowAlgorithm.LastExtent;
+ int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+ double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context);
+ double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent);
+ if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize)
+ {
+ double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent));
+ int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context));
+
+ anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
+ bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context);
+ }
+ }
+
+ return new FlowLayoutAnchorInfo
+ {
+ Index = anchorIndex,
+ Offset = _orientation.MajorStart(bounds)
+ };
+ }
+
+ FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement(
+ int targetIndex,
+ Size availableSize,
+ VirtualizingLayoutContext context)
+ {
+ int index = -1;
+ double offset = double.NaN;
+ int count = context.ItemCount;
+ if (targetIndex >= 0 && targetIndex < count)
+ {
+ int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+ int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
+ index = indexOfFirstInLine;
+ var state = context.LayoutState as UniformGridLayoutState;
+ offset = _orientation.MajorStart(GetLayoutRectForDataIndex(availableSize, indexOfFirstInLine, state.FlowAlgorithm.LastExtent, context));
+ }
+
+ return new FlowLayoutAnchorInfo
+ {
+ Index = index,
+ Offset = offset
+ };
+ }
+
+ Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ ILayoutable firstRealized,
+ int firstRealizedItemIndex,
+ Rect firstRealizedLayoutBounds,
+ ILayoutable lastRealized,
+ int lastRealizedItemIndex,
+ Rect lastRealizedLayoutBounds)
+ {
+ var extent = new Rect();
+
+
+ // Constants
+ int itemsCount = context.ItemCount;
+ double availableSizeMinor = _orientation.Minor(availableSize);
+ int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ?
+ (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount);
+ double lineSize = GetMajorSizeWithSpacing(context);
+
+ if (itemsCount > 0)
+ {
+ _orientation.SetMinorSize(
+ ref extent,
+ !double.IsInfinity(availableSizeMinor) ?
+ availableSizeMinor :
+ Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
+ _orientation.SetMajorSize(
+ ref extent,
+ Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing));
+
+ if (firstRealized != null)
+ {
+ _orientation.SetMajorStart(
+ ref extent,
+ _orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize);
+ int remainingItems = itemsCount - lastRealizedItemIndex - 1;
+ _orientation.SetMajorSize(
+ ref extent,
+ _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize);
+ }
+ }
+
+ return extent;
+ }
+
+ void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context)
+ {
+ }
+
+ void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context)
+ {
+ }
+
+ protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var state = context.LayoutState;
+ var gridState = state as UniformGridLayoutState;
+
+ if (gridState == null)
+ {
+ if (state != null)
+ {
+ throw new InvalidOperationException("LayoutState must derive from UniformGridLayoutState.");
+ }
+
+ // Custom deriving layouts could potentially be stateful.
+ // If that is the case, we will just create the base state required by UniformGridLayout ourselves.
+ gridState = new UniformGridLayoutState();
+ }
+
+ gridState.InitializeForContext(context, this);
+ }
+
+ protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ gridState.UninitializeForContext(context);
+ }
+
+ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ {
+ // Set the width and height on the grid state. If the user already set them then use the preset.
+ // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items.
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing);
+
+ var desiredSize = GetFlowAlgorithm(context).Measure(
+ availableSize,
+ context,
+ true,
+ MinItemSpacing,
+ LineSpacing,
+ _orientation.ScrollOrientation,
+ LayoutId);
+
+ // If after Measure the first item is in the realization rect, then we revoke grid state's ownership,
+ // and only use the layout when to clear it when it's done.
+ gridState.EnsureFirstElementOwnership(context);
+
+ return new Size(desiredSize.Width, desiredSize.Height);
+ }
+
+ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ {
+ var value = GetFlowAlgorithm(context).Arrange(
+ finalSize,
+ context,
+ (FlowLayoutAlgorithm.LineAlignment)_itemsJustification,
+ LayoutId);
+ return new Size(value.Width, value.Height);
+ }
+
+ protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
+ {
+ GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context);
+ // Always invalidate layout to keep the view accurate.
+ InvalidateLayout();
+
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ gridState.ClearElementOnDataSourceChange(context, args);
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+ {
+ if (args.Property == OrientationProperty)
+ {
+ var orientation = (Orientation)args.NewValue;
+
+ //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation.
+ //i.e. the properties are the inverse of each other.
+ var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal;
+ _orientation.ScrollOrientation = scrollOrientation;
+ }
+ else if (args.Property == MinColumnSpacingProperty)
+ {
+ _minColumnSpacing = (double)args.NewValue;
+ }
+ else if (args.Property == MinRowSpacingProperty)
+ {
+ _minRowSpacing = (double)args.NewValue;
+ }
+ else if (args.Property == ItemsJustificationProperty)
+ {
+ _itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue;
+ }
+ else if (args.Property == ItemsStretchProperty)
+ {
+ _itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue;
+ }
+ else if (args.Property == MinItemWidthProperty)
+ {
+ _minItemWidth = (double)args.NewValue;
+ }
+ else if (args.Property == MinItemHeightProperty)
+ {
+ _minItemHeight = (double)args.NewValue;
+ }
+
+ InvalidateLayout();
+ }
+
+ private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context)
+ {
+ var minItemSpacing = MinItemSpacing;
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ return _orientation.ScrollOrientation == ScrollOrientation.Vertical?
+ gridState.EffectiveItemWidth + minItemSpacing :
+ gridState.EffectiveItemHeight + minItemSpacing;
+ }
+
+ private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context)
+ {
+ var lineSpacing = LineSpacing;
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ return _orientation.ScrollOrientation == ScrollOrientation.Vertical ?
+ gridState.EffectiveItemHeight + lineSpacing :
+ gridState.EffectiveItemWidth + lineSpacing;
+ }
+
+ Rect GetLayoutRectForDataIndex(
+ Size availableSize,
+ int index,
+ Rect lastExtent,
+ VirtualizingLayoutContext context)
+ {
+ int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
+ int rowIndex = (int)(index / itemsPerLine);
+ int indexInRow = index - (rowIndex * itemsPerLine);
+
+ var gridState = (UniformGridLayoutState)context.LayoutState;
+ Rect bounds = _orientation.MinorMajorRect(
+ indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent),
+ rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent),
+ _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight,
+ _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth);
+
+ return bounds;
+ }
+
+ private void InvalidateLayout() => InvalidateMeasure();
+
+ private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState).FlowAlgorithm;
+ }
+}
diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs
new file mode 100644
index 0000000000..4557a78d37
--- /dev/null
+++ b/src/Avalonia.Layout/UniformGridLayoutState.cs
@@ -0,0 +1,192 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Collections.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the state of a .
+ ///
+ public class UniformGridLayoutState
+ {
+ // We need to measure the element at index 0 to know what size to measure all other items.
+ // If FlowlayoutAlgorithm has already realized element 0 then we can use that.
+ // If it does not, then we need to do context.GetElement(0) at which point we have requested an element and are on point to clear it.
+ // If we are responsible for clearing element 0 we keep m_cachedFirstElement valid.
+ // If we are not (because FlowLayoutAlgorithm is holding it for us) then we just null out this field and use the one from FlowLayoutAlgorithm.
+ private ILayoutable _cachedFirstElement;
+
+ internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm();
+ internal double EffectiveItemWidth { get; private set; }
+ internal double EffectiveItemHeight { get; private set; }
+
+ internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
+ {
+ FlowAlgorithm.InitializeForContext(context, callbacks);
+ context.LayoutState = this;
+ }
+
+ internal void UninitializeForContext(VirtualizingLayoutContext context)
+ {
+ FlowAlgorithm.UninitializeForContext(context);
+
+ if (_cachedFirstElement != null)
+ {
+ context.RecycleElement(_cachedFirstElement);
+ }
+ }
+
+ internal void EnsureElementSize(
+ Size availableSize,
+ VirtualizingLayoutContext context,
+ double layoutItemWidth,
+ double LayoutItemHeight,
+ UniformGridLayoutItemsStretch stretch,
+ Orientation orientation,
+ double minRowSpacing,
+ double minColumnSpacing)
+ {
+ if (context.ItemCount > 0)
+ {
+ // If the first element is realized we don't need to cache it or to get it from the context
+ var realizedElement = FlowAlgorithm.GetElementIfRealized(0);
+ if (realizedElement != null)
+ {
+ realizedElement.Measure(availableSize);
+ SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
+ _cachedFirstElement = null;
+ }
+ else
+ {
+ if (_cachedFirstElement == null)
+ {
+ // we only cache if we aren't realizing it
+ _cachedFirstElement = context.GetOrCreateElementAt(
+ 0,
+ ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); // expensive
+ }
+
+ _cachedFirstElement.Measure(availableSize);
+
+ // This doesn't need to be done in the UWP version and I'm not sure why. If we
+ // don't do this here, and we receive a recycled element then it will be shown
+ // at its previous arrange point, but we don't want it shown at all until its
+ // arranged.
+ _cachedFirstElement.Arrange(new Rect(-10000.0, -10000.0, 0, 0));
+
+ SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
+
+ // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.
+ bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement);
+ if (added)
+ {
+ _cachedFirstElement = null;
+ }
+ }
+ }
+ }
+
+ private void SetSize(
+ ILayoutable element,
+ double layoutItemWidth,
+ double LayoutItemHeight,
+ Size availableSize,
+ UniformGridLayoutItemsStretch stretch,
+ Orientation orientation,
+ double minRowSpacing,
+ double minColumnSpacing)
+ {
+ EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
+ EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight);
+
+ var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height;
+ var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing;
+
+ var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight;
+ itemSizeMinor += minorItemSpacing;
+
+ var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor));
+ var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor);
+ var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn;
+
+ if (stretch == UniformGridLayoutItemsStretch.Fill)
+ {
+ if (orientation == Orientation.Horizontal)
+ {
+ EffectiveItemWidth += extraMinorPixelsForEachItem;
+ }
+ else
+ {
+ EffectiveItemHeight += extraMinorPixelsForEachItem;
+ }
+ }
+ else if (stretch == UniformGridLayoutItemsStretch.Uniform)
+ {
+ var itemSizeMajor = orientation == Orientation.Horizontal ? EffectiveItemHeight : EffectiveItemWidth;
+ var extraMajorPixelsForEachItem = itemSizeMajor * (extraMinorPixelsForEachItem / itemSizeMinor);
+ if (orientation == Orientation.Horizontal)
+ {
+ EffectiveItemWidth += extraMinorPixelsForEachItem;
+ EffectiveItemHeight += extraMajorPixelsForEachItem;
+ }
+ else
+ {
+ EffectiveItemHeight += extraMinorPixelsForEachItem;
+ EffectiveItemWidth += extraMajorPixelsForEachItem;
+ }
+ }
+ }
+
+ internal void EnsureFirstElementOwnership(VirtualizingLayoutContext context)
+ {
+ if (_cachedFirstElement != null && FlowAlgorithm.GetElementIfRealized(0) != null)
+ {
+ // We created the element, but then flowlayout algorithm took ownership, so we can clear it and
+ // let flowlayout algorithm do its thing.
+ context.RecycleElement(_cachedFirstElement);
+ _cachedFirstElement = null;
+ }
+ }
+
+ internal void ClearElementOnDataSourceChange(
+ VirtualizingLayoutContext context,
+ NotifyCollectionChangedEventArgs args)
+ {
+ if (_cachedFirstElement != null)
+ {
+ bool shouldClear = false;
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ shouldClear = args.NewStartingIndex == 0;
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ shouldClear = args.NewStartingIndex == 0 || args.OldStartingIndex == 0;
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ shouldClear = args.OldStartingIndex == 0;
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ shouldClear = true;
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ throw new NotImplementedException();
+ }
+
+ if (shouldClear)
+ {
+ context.RecycleElement(_cachedFirstElement);
+ _cachedFirstElement = null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/Utils/ListUtils.cs b/src/Avalonia.Layout/Utils/ListUtils.cs
new file mode 100644
index 0000000000..eb2480acd3
--- /dev/null
+++ b/src/Avalonia.Layout/Utils/ListUtils.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Layout.Utils
+{
+ internal static class ListUtils
+ {
+ public static void Resize(this List list, int size, T value)
+ {
+ int cur = list.Count;
+
+ if (size < cur)
+ {
+ list.RemoveRange(size, cur - size);
+ }
+ else if (size > cur)
+ {
+ if (size > list.Capacity)
+ {
+ list.Capacity = size;
+ }
+
+ list.AddRange(Enumerable.Repeat(value, size - cur));
+ }
+ }
+
+ public static void Resize(this List list, int count)
+ {
+ Resize(list, count, default);
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs
new file mode 100644
index 0000000000..4c601175f3
--- /dev/null
+++ b/src/Avalonia.Layout/VirtualizingLayout.cs
@@ -0,0 +1,139 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System.Collections.Specialized;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Represents the base class for an object that sizes and arranges child elements for a host
+ /// and supports virtualization.
+ ///
+ ///
+ /// is the base class for layouts that support virtualization.
+ /// You can use one of the provided derived class, or inherit from it to create your own layout.
+ /// Provided concrete virtualizing layout classes are and
+ /// .
+ ///
+ public abstract class VirtualizingLayout : AttachedLayout
+ {
+ ///
+ public sealed override void InitializeForContext(LayoutContext context)
+ {
+ InitializeForContextCore((VirtualizingLayoutContext)context);
+ }
+
+ ///
+ public sealed override void UninitializeForContext(LayoutContext context)
+ {
+ UninitializeForContextCore((VirtualizingLayoutContext)context);
+ }
+
+ ///
+ public sealed override Size Measure(LayoutContext context, Size availableSize)
+ {
+ return MeasureOverride((VirtualizingLayoutContext)context, availableSize);
+ }
+
+ ///
+ public sealed override Size Arrange(LayoutContext context, Size finalSize)
+ {
+ return ArrangeOverride((VirtualizingLayoutContext)context, finalSize);
+ }
+
+ ///
+ /// Notifies the layout when the data collection assigned to the container element (Items)
+ /// has changed.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ /// The data source.
+ /// Data about the collection change.
+ ///
+ /// Override
+ /// to provide the behavior for this method in a derived class.
+ ///
+ public void OnItemsChanged(
+ VirtualizingLayoutContext context,
+ object source,
+ NotifyCollectionChangedEventArgs args) => OnItemsChangedCore(context, source, args);
+
+ ///
+ /// When overridden in a derived class, initializes any per-container state the layout
+ /// requires when it is attached to an ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ protected virtual void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ }
+
+ ///
+ /// When overridden in a derived class, removes any state the layout previously stored on
+ /// the ILayoutable container.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ }
+
+ ///
+ /// Provides the behavior for the "Measure" pass of the layout cycle. Classes can override
+ /// this method to define their own "Measure" pass behavior.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The available size that this object can give to child objects. Infinity can be
+ /// specified as a value to indicate that the object will size to whatever content is
+ /// available.
+ ///
+ ///
+ /// The size that this object determines it needs during layout, based on its calculations
+ /// of the allocated sizes for child objects or based on other considerations such as a
+ /// fixed container size.
+ ///
+ protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize);
+
+ ///
+ /// When implemented in a derived class, provides the behavior for the "Arrange" pass of
+ /// layout. Classes can override this method to define their own "Arrange" pass behavior.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ ///
+ /// The final area within the container that this object should use to arrange itself and
+ /// its children.
+ ///
+ /// The actual size that is used after the element is arranged in layout.
+ protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize;
+
+ ///
+ /// Notifies the layout when the data collection assigned to the container element (Items)
+ /// has changed.
+ ///
+ ///
+ /// The context object that facilitates communication between the layout and its host
+ /// container.
+ ///
+ /// The data source.
+ /// Data about the collection change.
+ protected internal virtual void OnItemsChangedCore(
+ VirtualizingLayoutContext context,
+ object source,
+ NotifyCollectionChangedEventArgs args) => InvalidateMeasure();
+ }
+}
diff --git a/src/Avalonia.Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Layout/VirtualizingLayoutContext.cs
new file mode 100644
index 0000000000..980daec2eb
--- /dev/null
+++ b/src/Avalonia.Layout/VirtualizingLayoutContext.cs
@@ -0,0 +1,190 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+
+namespace Avalonia.Layout
+{
+ ///
+ /// Defines constants that specify whether to suppress automatic recycling of the retrieved
+ /// element or force creation of a new element.
+ ///
+ ///
+ /// When you call ,
+ /// you can specify whether to suppress automatic recycling of the retrieved element or force
+ /// creation of a new element. Elements retrieved with automatic recycling suppressed
+ /// (SuppressAutoRecycle) are ignored by the automatic recycling logic that clears realized
+ /// elements that were not retrieved as part of the current layout pass. You must explicitly
+ /// recycle these elements by passing them to the RecycleElement method to avoid memory leaks.
+ ///
+ [Flags]
+ public enum ElementRealizationOptions
+ {
+ ///
+ /// No option is specified.
+ ///
+ None = 0x0,
+
+ ///
+ /// Creation of a new element is forced.
+ ///
+ ForceCreate = 0x1,
+
+ ///
+ /// The element is ignored by the automatic recycling logic.
+ ///
+ SuppressAutoRecycle = 0x2,
+ };
+
+ ///
+ /// Represents the base class for layout context types that support virtualization.
+ ///
+ public abstract class VirtualizingLayoutContext : LayoutContext
+ {
+ ///
+ /// Gets the number of items in the data.
+ ///
+ ///
+ /// This property gets the value returned by ItemCountCore, which must be implemented in
+ /// a derived class.
+ ///
+ public int ItemCount => ItemCountCore();
+
+ ///
+ /// Gets or sets the origin point for the estimated content size.
+ ///
+ ///
+ /// LayoutOrigin is used by virtualizing layouts that rely on estimations when determining
+ /// the size and position of content. It allows the layout to fix-up the estimated origin
+ /// of the content as it changes due to on-going estimation or potentially identifying the
+ /// actual size to use. For example, it’s possible that as a user is scrolling back to the
+ /// top of the content that the layout's estimates for the content size that it reports as
+ /// part of its MeasureOverride become increasingly accurate. If the predicted position of
+ /// the content does not already match the previously predicted position (for example, if
+ /// the size of the elements ends up being smaller than previously thought), then the
+ /// layout can indicate a new origin. The viewport provided to the layout on subsequent
+ /// passes will take into account the adjusted origin.
+ ///
+ public Point LayoutOrigin { get => LayoutOriginCore; set => LayoutOriginCore = value; }
+
+ ///
+ /// Gets an area that represents the viewport and buffer that the layout should fill with
+ /// realized elements.
+ ///
+ public Rect RealizationRect => RealizationRectCore();
+
+ ///
+ /// Gets the recommended index from which to start the generation and layout of elements.
+ ///
+ ///
+ /// The recommended index might be the result of programmatically realizing an element and
+ /// requesting that it be brought into view. Or, it may be that a user drags the scrollbar
+ /// thumb so quickly that the new viewport and the viewport and buffer previously given to
+ /// the layout do not intersect, so a new index is suggested as the anchor from which to
+ /// generate and layout other elements.
+ ///
+ public int RecommendedAnchorIndex => RecommendedAnchorIndexCore;
+
+ ///
+ /// Implements the behavior of LayoutOrigin in a derived or custom VirtualizingLayoutContext.
+ ///
+ protected abstract Point LayoutOriginCore { get; set; }
+
+ ///
+ /// Implements the behavior for getting the return value of RecommendedAnchorIndex in a
+ /// derived or custom .
+ ///
+ protected virtual int RecommendedAnchorIndexCore { get; }
+
+ ///
+ /// Retrieves the data item in the source found at the specified index.
+ ///
+ /// The index of the data item to retrieve.
+ public object GetItemAt(int index) => GetItemAtCore(index);
+
+ ///
+ /// Retrieves a UIElement that represents the data item in the source found at the
+ /// specified index. By default, if an element already exists, it is returned; otherwise,
+ /// a new element is created.
+ ///
+ /// The index of the data item to retrieve a UIElement for.
+ ///
+ /// This method calls
+ /// with options set to None. GetElementAtCore must be implemented in a derived class.
+ ///
+ public ILayoutable GetOrCreateElementAt(int index)
+ => GetOrCreateElementAtCore(index, ElementRealizationOptions.None);
+
+ ///
+ /// Retrieves a UIElement that represents the data item in the source found at the
+ /// specified index using the specified options.
+ ///
+ /// The index of the data item to retrieve a UIElement for.
+ ///
+ /// A value of that specifies whether to suppress
+ /// automatic recycling of the retrieved element or force creation of a new element.
+ ///
+ ///
+ /// This method calls ,
+ /// which must be implemented in a derived class. When you request an element for the
+ /// specified index, you can optionally specify whether to suppress automatic recycling of
+ /// the retrieved element or force creation of a new element.Elements retrieved with
+ /// automatic recycling suppressed(SuppressAutoRecycle) are ignored by the automatic
+ /// recycling logic that clears realized elements that were not retrieved as part of the
+ /// current layout pass.You must explicitly recycle these elements by passing them to the
+ /// RecycleElement method to avoid memory leaks. These options are intended for more
+ /// advanced layouts that choose to explicitly manage the realization and recycling of
+ /// elements as a performance optimization.
+ ///
+ public ILayoutable GetOrCreateElementAt(int index, ElementRealizationOptions options)
+ => GetOrCreateElementAtCore(index, options);
+
+ ///
+ /// Clears the specified UIElement and allows it to be either re-used or released.
+ ///
+ /// The element to clear.
+ ///
+ /// This method calls , which must be implemented
+ /// in a derived class.
+ ///
+ public void RecycleElement(ILayoutable element) => RecycleElementCore(element);
+
+ ///
+ /// When implemented in a derived class, retrieves the number of items in the data.
+ ///
+ protected abstract int ItemCountCore();
+
+ ///
+ /// When implemented in a derived class, retrieves the data item in the source found at the
+ /// specified index.
+ ///
+ /// The index of the data item to retrieve.
+ protected abstract object GetItemAtCore(int index);
+
+ ///
+ /// When implemented in a derived class, retrieves an area that represents the viewport and
+ /// buffer that the layout should fill with realized elements.
+ ///
+ protected abstract Rect RealizationRectCore();
+
+ ///
+ /// When implemented in a derived class, retrieves a UIElement that represents the data item
+ /// in the source found at the specified index using the specified options.
+ ///
+ /// The index of the data item to retrieve a UIElement for.
+ ///
+ /// A value of that specifies whether to suppress
+ /// automatic recycling of the retrieved element or force creation of a new element.
+ ///
+ protected abstract ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options);
+
+ ///
+ /// When implemented in a derived class, clears the specified UIElement and allows it to be
+ /// either re-used or released.
+ ///
+ /// The element to clear.
+ protected abstract void RecycleElementCore(ILayoutable element);
+ }
+}
diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
index 0d077d2a3a..ebaf62b2c0 100644
--- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
@@ -547,7 +547,6 @@ namespace Avalonia.Rendering
}
}
- System.Diagnostics.Debug.WriteLine("Invalidated " + rect);
SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect));
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
index 1c2fa17643..83d70122b3 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
@@ -149,11 +149,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
[Obsolete("Don't use", true)]
public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider(null);
- [DebuggerStepThrough]
+ // Don't emit debug symbols for this code so debugger will be forced to step into XAML instead
+ #line hidden
public static IServiceProvider CreateRootServiceProviderV2()
{
return new RootServiceProvider(new NameScope());
}
+ #line default
class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider
{
diff --git a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
index eeb502d730..ecf559951a 100644
--- a/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Data/DefaultValueConverterTests.cs
@@ -8,6 +8,7 @@ using Xunit;
using System.Windows.Input;
using System;
using Avalonia.Data.Converters;
+using Avalonia.Layout;
namespace Avalonia.Base.UnitTests.Data.Converters
{
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
index 2eeff4cdf9..e2eb628512 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
@@ -6,6 +6,7 @@ using System.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
+using Avalonia.Layout;
using Avalonia.Markup.Data;
using Avalonia.Styling;
using Avalonia.UnitTests;
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
index 8618387150..72f2b8022f 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
@@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System.Collections.Generic;
+using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@@ -43,6 +45,24 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal("foo", target.SelectedItem);
}
+
+ [Fact]
+ public void First_Item_Should_Be_Selected_When_Reset()
+ {
+ var items = new ResetOnAdd();
+ var target = new TestSelector
+ {
+ Items = items,
+ Template = Template(),
+ };
+
+ target.ApplyTemplate();
+ items.Add("foo");
+
+ Assert.Equal(0, target.SelectedIndex);
+ Assert.Equal("foo", target.SelectedItem);
+ }
+
[Fact]
public void Item_Should_Be_Selected_When_Selection_Removed()
{
@@ -100,5 +120,18 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected);
}
}
+
+ private class ResetOnAdd : List, INotifyCollectionChanged
+ {
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ public new void Add(string item)
+ {
+ base.Add(item);
+ CollectionChanged?.Invoke(
+ this,
+ new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs
index 5396a43f3a..dde0e0000d 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
using Xunit;
diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
index 75a2f4178b..2238175a4a 100644
--- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
+using Avalonia.Layout;
using Xunit;
namespace Avalonia.Controls.UnitTests
diff --git a/tests/Avalonia.Controls.UnitTests/SliderTests.cs b/tests/Avalonia.Controls.UnitTests/SliderTests.cs
index dc47d9eb89..1c3c052144 100644
--- a/tests/Avalonia.Controls.UnitTests/SliderTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/SliderTests.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
+using Avalonia.Layout;
using Xunit;
namespace Avalonia.Controls.UnitTests
diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
index cd35627064..a0511761e4 100644
--- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Layout;
using Xunit;
namespace Avalonia.Controls.UnitTests
@@ -93,4 +94,4 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds);
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
index abe6fa84b0..bb44d069b5 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs
@@ -1,4 +1,5 @@
using Avalonia.Controls;
+using Avalonia.Layout;
using Avalonia.UnitTests;
using Xunit;
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs
index 867d4d7450..139a7925b1 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using Avalonia.Controls;
+using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;