using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
///
/// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically.
///
public class VirtualizingStackPanel : VirtualizingPanel, IScrollSnapPointsInfo
{
///
/// Defines the property.
///
public static readonly StyledProperty OrientationProperty =
StackPanel.OrientationProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular));
///
/// Defines the property.
///
public static readonly StyledProperty AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular));
///
/// Defines the event.
///
public static readonly RoutedEvent HorizontalSnapPointsChangedEvent =
RoutedEvent.Register(
nameof(HorizontalSnapPointsChanged),
RoutingStrategies.Bubble);
///
/// Defines the event.
///
public static readonly RoutedEvent VerticalSnapPointsChangedEvent =
RoutedEvent.Register(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
private static readonly AttachedProperty ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached("ItemIsOwnContainer");
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
private readonly Action _recycleElement;
private readonly Action _recycleElementOnItemRemoved;
private readonly Action _updateElementIndex;
private int _scrollToIndex = -1;
private Control? _scrollToElement;
private bool _isInLayout;
private bool _isWaitingForViewportUpdate;
private double _lastEstimatedElementSizeU = 25;
private RealizedStackElements? _measureElements;
private RealizedStackElements? _realizedElements;
private ScrollViewer? _scrollViewer;
private Rect _viewport = s_invalidViewport;
private Stack? _recyclePool;
private Control? _unrealizedFocusedElement;
private int _unrealizedFocusedIndex = -1;
public VirtualizingStackPanel()
{
_recycleElement = RecycleElement;
_recycleElementOnItemRemoved = RecycleElementOnItemRemoved;
_updateElementIndex = UpdateElementIndex;
EffectiveViewportChanged += OnEffectiveViewportChanged;
}
///
/// 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);
}
///
/// Occurs when the measurements for horizontal snap points change.
///
public event EventHandler? HorizontalSnapPointsChanged
{
add => AddHandler(HorizontalSnapPointsChangedEvent, value);
remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value);
}
///
/// Occurs when the measurements for vertical snap points change.
///
public event EventHandler? VerticalSnapPointsChanged
{
add => AddHandler(VerticalSnapPointsChangedEvent, value);
remove => RemoveHandler(VerticalSnapPointsChangedEvent, value);
}
///
/// Gets or sets whether the horizontal snap points for the are equidistant from each other.
///
public bool AreHorizontalSnapPointsRegular
{
get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
}
///
/// Gets or sets whether the vertical snap points for the are equidistant from each other.
///
public bool AreVerticalSnapPointsRegular
{
get { return GetValue(AreVerticalSnapPointsRegularProperty); }
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
}
///
/// Gets the index of the first realized element, or -1 if no elements are realized.
///
public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1;
///
/// Gets the index of the last realized element, or -1 if no elements are realized.
///
public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1;
protected override Size MeasureOverride(Size availableSize)
{
var items = Items;
if (items.Count == 0)
return default;
// If we're bringing an item into view, ignore any layout passes until we receive a new
// effective viewport.
if (_isWaitingForViewportUpdate)
return DesiredSize;
_isInLayout = true;
try
{
var orientation = Orientation;
_realizedElements ??= new();
_measureElements ??= new();
// We handle horizontal and vertical layouts here so X and Y are abstracted to:
// - Horizontal layouts: U = horizontal, V = vertical
// - Vertical layouts: U = vertical, V = horizontal
var viewport = CalculateMeasureViewport(items);
// If the viewport is disjunct then we can recycle everything.
if (viewport.viewportIsDisjunct)
_realizedElements.RecycleAllElements(_recycleElement);
// Do the measure, creating/recycling elements as necessary to fill the viewport. Don't
// write to _realizedElements yet, only _measureElements.
RealizeElements(items, availableSize, ref viewport);
// Now swap the measureElements and realizedElements collection.
(_measureElements, _realizedElements) = (_realizedElements, _measureElements);
_measureElements.ResetForReuse();
return CalculateDesiredSize(orientation, items.Count, viewport);
}
finally
{
_isInLayout = false;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
if (_realizedElements is null)
return default;
_isInLayout = true;
try
{
var orientation = Orientation;
var u = _realizedElements!.StartU;
for (var i = 0; i < _realizedElements.Count; ++i)
{
var e = _realizedElements.Elements[i];
if (e is not null)
{
var sizeU = _realizedElements.SizeU[i];
var rect = orientation == Orientation.Horizontal ?
new Rect(u, 0, sizeU, finalSize.Height) :
new Rect(0, u, finalSize.Width, sizeU);
e.Arrange(rect);
_scrollViewer?.RegisterAnchorCandidate(e);
u += orientation == Orientation.Horizontal ? rect.Width : rect.Height;
}
}
return finalSize;
}
finally
{
_isInLayout = false;
RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_scrollViewer = this.FindAncestorOfType();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_scrollViewer = null;
}
protected override void OnItemsChanged(IReadOnlyList