// 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; using Avalonia.Data; 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 internal 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 internal override void UninitializeForContextCore(VirtualizingLayoutContext context) { var stackState = (StackLayoutState)context.LayoutState; stackState.UninitializeForContext(context); } protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) { var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, context, false, 0, Spacing, int.MaxValue, _orientation.ScrollOrientation, LayoutId); return new Size(desiredSize.Width, desiredSize.Height); } protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) { var value = GetFlowAlgorithm(context).Arrange( finalSize, context, false, 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(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) { if (property == OrientationProperty) { var orientation = newValue.GetValueOrDefault(); //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; } }