From ece1b3e334c4c77acb1092e64604e4d285a7ce8a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 May 2019 22:45:14 +0200 Subject: [PATCH 01/30] Initial import of ItemsRepeater from the WinUI library. https://github.com/microsoft/microsoft-ui-xaml --- samples/ControlCatalog/MainView.xaml | 1 + .../Pages/ItemsRepeaterPage.xaml | 7 + .../Pages/ItemsRepeaterPage.xaml.cs | 25 + .../Properties/AssemblyInfo.cs | 1 + .../Repeaters/ElementFactoryGetArgs.cs | 11 + .../Repeaters/ElementFactoryRecycleArgs.cs | 12 + .../Repeaters/ElementManager.cs | 426 ++++++++++ .../Repeaters/FlowLayoutAlgorithm.cs | 689 +++++++++++++++ .../IFlowLayoutAlgorithmDelegates.cs | 43 + .../Repeaters/ITrackerHandleManager.cs | 10 + .../Repeaters/ItemTemplateWrapper.cs | 53 ++ .../Repeaters/ItemsRepeater.cs | 631 ++++++++++++++ .../ItemsRepeaterElementClearingEventArgs.cs | 11 + ...emsRepeaterElementIndexChangedEventArgs.cs | 27 + .../ItemsRepeaterElementPreparedEventArgs.cs | 25 + .../Repeaters/ItemsSourceView.cs | 91 ++ src/Avalonia.Controls/Repeaters/Layout.cs | 24 + .../Repeaters/LayoutContext.cs | 13 + .../Repeaters/NonVirtualizingLayout.cs | 29 + .../Repeaters/OrientationBasedMeasures.cs | 78 ++ .../Repeaters/RecyclePool.cs | 101 +++ .../Repeaters/RepeaterLayoutContext.cs | 59 ++ .../Repeaters/StackLayout.cs | 298 +++++++ .../Repeaters/StackLayoutState.cs | 53 ++ .../Repeaters/UniqueIdElementPool.cs | 54 ++ .../Repeaters/ViewManager.cs | 787 ++++++++++++++++++ .../Repeaters/ViewportManager.cs | 463 +++++++++++ .../Repeaters/VirtualizationInfo.cs | 121 +++ .../Repeaters/VirtualizingLayout.cs | 47 ++ .../Repeaters/VirtualizingLayoutContext.cs | 190 +++++ src/Avalonia.Controls/StackPanel.cs | 4 +- src/Avalonia.Controls/Utils/ListUtils.cs | 32 + .../Rendering/DeferredRenderer.cs | 1 - 33 files changed, 4414 insertions(+), 3 deletions(-) create mode 100644 samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml create mode 100644 samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs create mode 100644 src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ElementManager.cs create mode 100644 src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs create mode 100644 src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs create mode 100644 src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsRepeater.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs create mode 100644 src/Avalonia.Controls/Repeaters/ItemsSourceView.cs create mode 100644 src/Avalonia.Controls/Repeaters/Layout.cs create mode 100644 src/Avalonia.Controls/Repeaters/LayoutContext.cs create mode 100644 src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs create mode 100644 src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs create mode 100644 src/Avalonia.Controls/Repeaters/RecyclePool.cs create mode 100644 src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs create mode 100644 src/Avalonia.Controls/Repeaters/StackLayout.cs create mode 100644 src/Avalonia.Controls/Repeaters/StackLayoutState.cs create mode 100644 src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs create mode 100644 src/Avalonia.Controls/Repeaters/ViewManager.cs create mode 100644 src/Avalonia.Controls/Repeaters/ViewportManager.cs create mode 100644 src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs create mode 100644 src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs create mode 100644 src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs create mode 100644 src/Avalonia.Controls/Utils/ListUtils.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 1cddb9d295..4c9b4eaed5 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -26,6 +26,7 @@ + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml new file mode 100644 index 0000000000..a0cbefa4d9 --- /dev/null +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -0,0 +1,7 @@ + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs new file mode 100644 index 0000000000..a6ca27cb67 --- /dev/null +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ItemsRepeaterPage : UserControl + { + public ItemsRepeaterPage() + { + this.InitializeComponent(); + DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ) + .ToArray(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + } +} diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index c04c66a77f..8fa80e55d2 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -13,6 +13,7 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Repeaters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")] diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs new file mode 100644 index 0000000000..173388b68e --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + public sealed class ElementFactoryGetArgs : EventArgs + { + public object Data { get; set; } + public IControl Parent { get; set; } + internal int Index { get; set; } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs new file mode 100644 index 0000000000..3025546f67 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public sealed class ElementFactoryRecycleArgs : EventArgs + { + public IControl Element { get; set; } + public IControl Parent { get; set; } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ElementManager.cs b/src/Avalonia.Controls/Repeaters/ElementManager.cs new file mode 100644 index 0000000000..2ce0ad24fa --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ElementManager.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls.Repeaters +{ + internal class ElementManager + { + private readonly List _realizedElements = new List(); + private readonly List _realizedElementLayoutBounds = new List(); + private int _firstRealizedDataIndex; + private VirtualizingLayoutContext _context; + + private bool IsVirtualizingContext + { + get + { + if (_context != null) + { + var rect = _context.RealizationRect; + bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width); + return !hasInfiniteSize; + } + return false; + } + } + + public void SetContext(VirtualizingLayoutContext virtualContext) => _context = virtualContext; + + public void OnBeginMeasure(Orientation orientation) + { + if (_context != null) + { + if (IsVirtualizingContext) + { + // We proactively clear elements laid out outside of the realizaton + // rect so that they are available for reuse during the current + // measure pass. + // This is useful during fast panning scenarios in which the realization + // window is constantly changing and we want to reuse elements from + // the end that's opposite to the panning direction. + DiscardElementsOutsideWindow(_context.RealizationRect, orientation); + } + else + { + // If we are initialized with a non-virtualizing context, make sure that + // we have enough space to hold the bounds for all the elements. + int count = _context.ItemCount; + if (_realizedElementLayoutBounds.Count != count) + { + // Make sure there is enough space for the bounds. + // Note: We could optimize when the count becomes smaller, but keeping + // it always up to date is the simplest option for now. + _realizedElementLayoutBounds.Resize(count); + } + } + } + } + + public int GetRealizedElementCount() + { + return IsVirtualizingContext ? _realizedElements.Count : _context.ItemCount; + } + + public IControl GetAt(int realizedIndex) + { + IControl element; + + if (IsVirtualizingContext) + { + if (_realizedElements[realizedIndex] == null) + { + // Sentinel. Create the element now since we need it. + int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex); + element = _context.GetOrCreateElementAt( + dataIndex, + ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + _realizedElements[realizedIndex] = element; + } + else + { + element = _realizedElements[realizedIndex]; + } + } + else + { + // realizedIndex and dataIndex are the same (everything is realized) + element = _context.GetOrCreateElementAt( + realizedIndex, + ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + } + + return element; + } + + public void Add(IControl element, int dataIndex) + { + if (_realizedElements.Count == 0) + { + _firstRealizedDataIndex = dataIndex; + } + + _realizedElements.Add(element); + _realizedElementLayoutBounds.Add(default); + } + + public void Insert(int realizedIndex, int dataIndex, IControl element) + { + if (realizedIndex == 0) + { + _firstRealizedDataIndex = dataIndex; + } + + _realizedElements.Insert(realizedIndex, element); + + // Set bounds to an invalid rect since we do not know it yet. + _realizedElementLayoutBounds.Insert(realizedIndex, new Rect(-1, -1, -1, -1)); + } + + public void ClearRealizedRange(int realizedIndex, int count) + { + for (int i = 0; i < count; i++) + { + // Clear from the edges so that ItemsRepeater can optimize on maintaining + // realized indices without walking through all the children every time. + int index = realizedIndex == 0 ? realizedIndex + i : (realizedIndex + count - 1) - i; + var elementRef = _realizedElements[index]; + + if (elementRef != null) + { + _context.RecycleElement(elementRef); + } + } + + int endIndex = realizedIndex + count; + _realizedElements.RemoveRange(realizedIndex, endIndex - realizedIndex); + _realizedElementLayoutBounds.RemoveRange(realizedIndex, endIndex - realizedIndex); + + if (realizedIndex == 0) + { + _firstRealizedDataIndex = _realizedElements.Count == 0 ? + -1 : _firstRealizedDataIndex + count; + } + } + + public void DiscardElementsOutsideWindow(bool forward, int startIndex) + { + // Remove layout elements that are outside the realized range. + if (IsDataIndexRealized(startIndex)) + { + int rangeIndex = GetRealizedRangeIndexFromDataIndex(startIndex); + + if (forward) + { + ClearRealizedRange(rangeIndex, GetRealizedElementCount() - rangeIndex); + } + else + { + ClearRealizedRange(0, rangeIndex + 1); + } + } + } + + public void ClearRealizedRange() => ClearRealizedRange(0, GetRealizedElementCount()); + + public Rect GetLayoutBoundsForDataIndex(int dataIndex) + { + int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex); + return _realizedElementLayoutBounds[realizedIndex]; + } + + public void SetLayoutBoundsForDataIndex(int dataIndex, in Rect bounds) + { + int realizedIndex = GetRealizedRangeIndexFromDataIndex(dataIndex); + _realizedElementLayoutBounds[realizedIndex] = bounds; + } + + public Rect GetLayoutBoundsForRealizedIndex(int realizedIndex) => _realizedElementLayoutBounds[realizedIndex]; + + public void SetLayoutBoundsForRealizedIndex(int realizedIndex, in Rect bounds) + { + _realizedElementLayoutBounds[realizedIndex] = bounds; + } + + public bool IsDataIndexRealized(int index) + { + if (IsVirtualizingContext) + { + int realizedCount = GetRealizedElementCount(); + return + realizedCount > 0 && + GetDataIndexFromRealizedRangeIndex(0) <= index && + GetDataIndexFromRealizedRangeIndex(realizedCount - 1) >= index; + } + else + { + // Non virtualized - everything is realized + return index >= 0 && index < _context.ItemCount; + } + } + + public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount; + + public IControl GetRealizedElement(int dataIndex) + { + return IsVirtualizingContext ? + GetAt(GetRealizedRangeIndexFromDataIndex(dataIndex)) : + _context.GetOrCreateElementAt( + dataIndex, + ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + } + + public void EnsureElementRealized(bool forward, int dataIndex, string layoutId) + { + if (IsDataIndexRealized(dataIndex) == false) + { + var element = _context.GetOrCreateElementAt( + dataIndex, + ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + + if (forward) + { + Add(element, dataIndex); + } + else + { + Insert(0, dataIndex, element); + } + } + } + + public bool IsWindowConnected(in Rect window, Orientation orientation, bool scrollOrientationSameAsFlow) + { + bool intersects = false; + + if (_realizedElementLayoutBounds.Count > 0) + { + var firstElementBounds = GetLayoutBoundsForRealizedIndex(0); + var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1); + + var effectiveOrientation = scrollOrientationSameAsFlow ? + (orientation == Orientation.Vertical ? Orientation.Horizontal : Orientation.Vertical) : + orientation; + + + var windowStart = effectiveOrientation == Orientation.Vertical ? window.Y : window.X; + var windowEnd = effectiveOrientation == Orientation.Vertical ? window.Y + window.Height : window.X + window.Width; + var firstElementStart = effectiveOrientation == Orientation.Vertical ? firstElementBounds.Y : firstElementBounds.X; + var lastElementEnd = effectiveOrientation == Orientation.Vertical ? lastElementBounds.Y + lastElementBounds.Height : lastElementBounds.X + lastElementBounds.Width; + + intersects = + firstElementStart <= windowEnd && + lastElementEnd >= windowStart; + } + + return intersects; + } + + public void DataSourceChanged(object source, NotifyCollectionChangedEventArgs args) + { + if (_realizedElements.Count > 0) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + { + OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); + } + break; + + case NotifyCollectionChangedAction.Replace: + { + OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count); + OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); + } + break; + + case NotifyCollectionChangedAction.Remove: + { + OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count); + } + break; + + case NotifyCollectionChangedAction.Reset: + ClearRealizedRange(); + break; + + case NotifyCollectionChangedAction.Move: + throw new NotImplementedException(); + } + } + } + + public int GetElementDataIndex(IControl suggestedAnchor) + { + var it = _realizedElements.IndexOf(suggestedAnchor); + return it != -1 ? GetDataIndexFromRealizedRangeIndex(it) : -1; + } + + public int GetDataIndexFromRealizedRangeIndex(int rangeIndex) + { + return IsVirtualizingContext ? rangeIndex + _firstRealizedDataIndex : rangeIndex; + } + + private int GetRealizedRangeIndexFromDataIndex(int dataIndex) + { + return IsVirtualizingContext ? dataIndex - _firstRealizedDataIndex : dataIndex; + } + + private void DiscardElementsOutsideWindow(in Rect window, Orientation orientation) + { + // The following illustration explains the cutoff indices. + // We will clear all the realized elements from both ends + // up to the corresponding cutoff index. + // '-' means the element is outside the cutoff range. + // '*' means the element is inside the cutoff range and will be cleared. + // + // Window: + // |______________________________| + // Realization range: + // |*****----------------------------------*********| + // | | + // frontCutoffIndex backCutoffIndex + // + // Note that we tolerate at most one element outside of the window + // because the FlowLayoutAlgorithm.Generate routine stops *after* + // it laid out an element outside the realization window. + // This is also convenient because it protects the anchor + // during a BringIntoView operation during which the anchor may + // not be in the realization window (in fact, the realization window + // might be empty if the BringIntoView is issued before the first + // layout pass). + + int realizedRangeSize = GetRealizedElementCount(); + int frontCutoffIndex = -1; + int backCutoffIndex = realizedRangeSize; + + for (int i = 0; + i= 0 && + !Intersects(window, _realizedElementLayoutBounds[i], orientation); + --i) + { + --backCutoffIndex; + } + + if (backCutoffIndex 0) + { + ClearRealizedRange(0, Math.Min(frontCutoffIndex, GetRealizedElementCount())); + } + } + + private static bool Intersects(in Rect lhs, in Rect rhs, Orientation orientation) + { + var lhsStart = orientation == Orientation.Vertical ? lhs.Y : lhs.X; + var lhsEnd = orientation == Orientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width; + var rhsStart = orientation == Orientation.Vertical ? rhs.Y : rhs.X; + var rhsEnd = orientation == Orientation.Vertical ? rhs.Y + rhs.Height : rhs.X + rhs.Width; + + return lhsEnd >= rhsStart && lhsStart <= rhsEnd; + } + + private void OnItemsAdded(int index, int count) + { + // Using the old indices here (before it was updated by the collection change) + // if the insert data index is between the first and last realized data index, we need + // to insert items. + int lastRealizedDataIndex = _firstRealizedDataIndex + GetRealizedElementCount() - 1; + int newStartingIndex = index; + if (newStartingIndex > _firstRealizedDataIndex && + newStartingIndex <= lastRealizedDataIndex) + { + // Inserted within the realized range + int insertRangeStartIndex = newStartingIndex - _firstRealizedDataIndex; + for (int i = 0; i < count; i++) + { + // Insert null (sentinel) here instead of an element, that way we dont + // end up creating a lot of elements only to be thrown out in the next layout. + int insertRangeIndex = insertRangeStartIndex + i; + int dataIndex = newStartingIndex + i; + // This is to keep the contiguousness of the mapping + Insert(insertRangeIndex, dataIndex, null); + } + } + else if (index <= _firstRealizedDataIndex) + { + // Items were inserted before the realized range. + // We need to update m_firstRealizedDataIndex; + _firstRealizedDataIndex += count; + } + } + + private void OnItemsRemoved(int index, int count) + { + int lastRealizedDataIndex = _firstRealizedDataIndex + _realizedElements.Count - 1; + int startIndex = Math.Max(_firstRealizedDataIndex, index); + int endIndex = Math.Min(lastRealizedDataIndex, index + count - 1); + bool removeAffectsFirstRealizedDataIndex = (index <= _firstRealizedDataIndex); + + if (endIndex >= startIndex) + { + ClearRealizedRange(GetRealizedRangeIndexFromDataIndex(startIndex), endIndex - startIndex + 1); + } + + if (removeAffectsFirstRealizedDataIndex && + _firstRealizedDataIndex != -1) + { + _firstRealizedDataIndex -= count; + } + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs new file mode 100644 index 0000000000..a0ced65076 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -0,0 +1,689 @@ +using System; +using System.Collections.Specialized; + +namespace Avalonia.Controls.Repeaters +{ + internal class FlowLayoutAlgorithm + { + private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + private readonly ElementManager _elementManager = new ElementManager(); + private Size _lastAvailableSize; + private double _lastItemSpacing; + private bool _collectionChangePending; + private VirtualizingLayoutContext _context; + private IFlowLayoutAlgorithmDelegates _algorithmCallbacks; + private Rect _lastExtent; + private int _firstRealizedDataIndexInsideRealizationWindow = -1; + private int _lastRealizedDataIndexInsideRealizationWindow = -1; + + // If the scroll orientation is the same as the folow orientation + // we will only have one line since we will never wrap. In that case + // we do not want to align the line. We could potentially switch the + // meaning of line alignment in this case, but I'll hold off on that + // feature until someone asks for it - This is not a common scenario + // anyway. + private bool _scrollOrientationSameAsFlow; + + public Rect LastExtent => _lastExtent; + + private bool IsVirtualizingContext + { + get + { + if (_context != null) + { + var rect = _context.RealizationRect; + bool hasInfiniteSize = double.IsInfinity(rect.Height) || double.IsInfinity(rect.Width); + return !hasInfiniteSize; + } + return false; + } + } + + private Rect RealizationRect => IsVirtualizingContext ? _context.RealizationRect : new Rect(Size.Infinity); + + public void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks) + { + _algorithmCallbacks = callbacks; + _context = context; + _elementManager.SetContext(context); + } + + public void UninitializeForContext(VirtualizingLayoutContext context) + { + if (IsVirtualizingContext) + { + // This layout is about to be detached. Let go of all elements + // being held and remove the layout state from the context. + _elementManager.ClearRealizedRange(); + } + + context.LayoutState = null; + } + + public Size Measure( + Size availableSize, + VirtualizingLayoutContext context, + bool isWrapping, + double minItemSpacing, + double lineSpacing, + Orientation orientation, + string layoutId) + { + _orientation.ScrollOrientation = orientation; + + // If minor size is infinity, there is only one line and no need to align that line. + _scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize)); + var realizationRect = RealizationRect; + + var suggestedAnchorIndex = _context.RecommendedAnchorIndex; + if (_elementManager.IsIndexValidInData(suggestedAnchorIndex)) + { + var anchorRealized = _elementManager.IsDataIndexRealized(suggestedAnchorIndex); + if (!anchorRealized) + { + MakeAnchor(_context, suggestedAnchorIndex, availableSize); + } + } + + _elementManager.OnBeginMeasure(orientation); + + int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + if (isWrapping && IsReflowRequired()) + { + var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); + _orientation.SetMinorStart(ref firstElementBounds, 0); + _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId); + } + + RaiseLineArranged(); + _collectionChangePending = false; + _lastExtent = EstimateExtent(availableSize, layoutId); + SetLayoutOrigin(); + + return new Size(_lastExtent.Width, _lastExtent.Height); + } + + public Size Arrange( + Size finalSize, + VirtualizingLayoutContext context, + LineAlignment lineAlignment, + string layoutId) + { + ArrangeVirtualizingLayout(finalSize, lineAlignment, layoutId); + + return new Size( + Math.Max(finalSize.Width, _lastExtent.Width), + Math.Max(finalSize.Height, _lastExtent.Height)); + } + + public void OnItemsSourceChanged( + object source, + NotifyCollectionChangedEventArgs args, + VirtualizingLayoutContext context) + { + _elementManager.DataSourceChanged(source, args); + _collectionChangePending = true; + } + + public Size MeasureElement( + IControl element, + int index, + Size availableSize, + VirtualizingLayoutContext context) + { + var measureSize = _algorithmCallbacks.Algorithm_GetMeasureSize(index, availableSize, context); + element.Measure(measureSize); + var provisionalArrangeSize = _algorithmCallbacks.Algorithm_GetProvisionalArrangeSize(index, measureSize, element.DesiredSize, context); + _algorithmCallbacks.Algorithm_OnElementMeasured(element, index, availableSize, measureSize, element.DesiredSize, provisionalArrangeSize, context); + + return provisionalArrangeSize; + } + + private int GetAnchorIndex( + Size availableSize, + bool isWrapping, + double minItemSpacing, + string layoutId) + { + int anchorIndex = -1; + var anchorPosition= new Point(); + var context = _context; + + if (!IsVirtualizingContext) + { + // Non virtualizing host, start generating from the element 0 + anchorIndex = context.ItemCount > 0 ? 0 : -1; + } + else + { + bool isRealizationWindowConnected = _elementManager.IsWindowConnected(RealizationRect, _orientation.ScrollOrientation, _scrollOrientationSameAsFlow); + // Item spacing and size in non-virtualizing direction change can cause elements to reflow + // and get a new column position. In that case we need the anchor to be positioned in the + // correct column. + bool needAnchorColumnRevaluation = isWrapping && ( + _orientation.Minor(_lastAvailableSize) != _orientation.Minor(availableSize) || + _lastItemSpacing != minItemSpacing || + _collectionChangePending); + + var suggestedAnchorIndex = _context.RecommendedAnchorIndex; + + var isAnchorSuggestionValid = suggestedAnchorIndex >= 0 && + _elementManager.IsDataIndexRealized(suggestedAnchorIndex); + + if (isAnchorSuggestionValid) + { + anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement( + suggestedAnchorIndex, + availableSize, + context).Index; + + if (_elementManager.IsDataIndexRealized(anchorIndex)) + { + var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); + if (needAnchorColumnRevaluation) + { + // We were provided a valid anchor, but its position might be incorrect because for example it is in + // the wrong column. We do know that the anchor is the first element in the row, so we can force the minor position + // to start at 0. + anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds)); + } + else + { + anchorPosition = new Point(anchorBounds.X, anchorBounds.Y); + } + } + else + { + // It is possible to end up in a situation during a collection change where GetAnchorForTargetElement returns an index + // which is not in the realized range. Eg. insert one item at index 0 for a grid layout. + // SuggestedAnchor will be 1 (used to be 0) and GetAnchorForTargetElement will return 0 (left most item in row). However 0 is not in the + // realized range yet. In this case we realize the gap between the target anchor and the suggested anchor. + int firstRealizedDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); + + for (int i = firstRealizedDataIndex - 1; i >= anchorIndex; --i) + { + _elementManager.EnsureElementRealized(false /*forward*/, i, layoutId); + } + + var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(suggestedAnchorIndex); + anchorPosition = _orientation.MinorMajorPoint(0, _orientation.MajorStart(anchorBounds)); + } + } + else if (needAnchorColumnRevaluation || !isRealizationWindowConnected) + { + // The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window + // but not the visible window. In that situation, we still need to produce a valid anchor. + var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context); + anchorIndex = anchorInfo.Index; + anchorPosition = _orientation.MinorMajorPoint(0, anchorInfo.Offset); + } + else + { + // No suggestion - just pick first in realized range + anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); + var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); + anchorPosition = new Point(firstElementBounds.X, firstElementBounds.Y); + } + } + + _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex; + if (_elementManager.IsIndexValidInData(anchorIndex)) + { + if (!_elementManager.IsDataIndexRealized(anchorIndex)) + { + // Disconnected, throw everything and create new anchor + _elementManager.ClearRealizedRange(); + + var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + _elementManager.Add(anchor, anchorIndex); + } + + var anchorElement = _elementManager.GetRealizedElement(anchorIndex); + var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context); + var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height); + _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds); + } + else + { + _elementManager.ClearRealizedRange(); + } + + // TODO: Perhaps we can track changes in the property setter + _lastAvailableSize = availableSize; + _lastItemSpacing = minItemSpacing; + + return anchorIndex; + } + + private void Generate( + GenerateDirection direction, + int anchorIndex, + Size availableSize, + double minItemSpacing, + double lineSpacing, + string layoutId) + { + if (anchorIndex != -1) + { + int step = (direction == GenerateDirection.Forward) ? 1 : -1; + int previousIndex = anchorIndex; + int currentIndex = anchorIndex + step; + var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); + var lineOffset = _orientation.MajorStart(anchorBounds); + var lineMajorSize = _orientation.MajorSize(anchorBounds); + int countInLine = 1; + bool lineNeedsReposition = false; + + while (_elementManager.IsIndexValidInData(currentIndex) && + ShouldContinueFillingUpSpace(previousIndex, direction)) + { + // Ensure layout element. + _elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId); + var currentElement = _elementManager.GetRealizedElement(currentIndex); + var desiredSize = MeasureElement(currentElement, currentIndex, availableSize, _context); + + // Lay it out. + var previousElement = _elementManager.GetRealizedElement(previousIndex); + var currentBounds = new Rect(0, 0, desiredSize.Width, desiredSize.Height); + var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(previousIndex); + + if (direction == GenerateDirection.Forward) + { + double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize)); + if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + { + // No more space in this row. wrap to next row. + _orientation.SetMinorStart(ref currentBounds, 0); + _orientation.SetMajorStart(ref currentBounds, _orientation.MajorStart(previousElementBounds) + lineMajorSize + lineSpacing); + + if (lineNeedsReposition) + { + // reposition the previous line (countInLine items) + for (int i = 0; i < countInLine; i++) + { + var dataIndex = currentIndex - 1 - i; + var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex); + _orientation.SetMajorSize(ref bounds, lineMajorSize); + _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds); + } + } + + // Setup for next line. + lineMajorSize = _orientation.MajorSize(currentBounds); + lineOffset = _orientation.MajorStart(currentBounds); + lineNeedsReposition = false; + countInLine = 1; + } + else + { + // More space is available in this row. + _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing); + _orientation.SetMajorStart(ref currentBounds, lineOffset); + lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds)); + lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds); + countInLine++; + } + } + else + { + // Backward + double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing); + if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + { + // Does not fit, wrap to the previous row + var availableSizeMinor = _orientation.Minor(availableSize); + + _orientation.SetMinorStart(ref currentBounds, double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0); + _orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing); + + if (lineNeedsReposition) + { + var previousLineOffset = _orientation.MajorStart(_elementManager.GetLayoutBoundsForDataIndex(currentIndex + countInLine + 1)); + // reposition the previous line (countInLine items) + for (int i = 0; i < countInLine; i++) + { + var dataIndex = currentIndex + 1 + i; + if (dataIndex != anchorIndex) + { + var bounds = _elementManager.GetLayoutBoundsForDataIndex(dataIndex); + _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing); + _orientation.SetMajorSize(ref bounds, lineMajorSize); + _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds); + } + } + } + + // Setup for next line. + lineMajorSize = _orientation.MajorSize(currentBounds); + lineOffset = _orientation.MajorStart(currentBounds); + lineNeedsReposition = false; + countInLine = 1; + } + else + { + // Fits in this row. put it in the previous position + _orientation.SetMinorStart(ref currentBounds, _orientation.MinorStart(previousElementBounds) - _orientation.Minor(desiredSize) - minItemSpacing); + _orientation.SetMajorStart(ref currentBounds, lineOffset); + lineMajorSize = Math.Max(lineMajorSize, _orientation.MajorSize(currentBounds)); + lineNeedsReposition = _orientation.MajorSize(previousElementBounds) != _orientation.MajorSize(currentBounds); + countInLine++; + } + } + + _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds); + previousIndex = currentIndex; + currentIndex += step; + } + + // If we did not reach the top or bottom of the extent, we realized one + // extra item before we knew we were outside the realization window. Do not + // account for that element in the indicies inside the realization window. + if (direction == GenerateDirection.Forward) + { + int dataCount = _context.ItemCount; + _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; + _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); + } + else + { + int dataCount = _context.ItemCount; + _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1; + _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow); + } + + _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex); + } + } + + private void MakeAnchor( + VirtualizingLayoutContext context, + int index, + Size availableSize) + { + _elementManager.ClearRealizedRange(); + // FlowLayout requires that the anchor is the first element in the row. + var internalAnchor = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(index, availableSize, context); + //MUX_ASSERT(internalAnchor.Index <= index); + + // No need to set the position of the anchor. + // (0,0) is fine for now since the extent can + // grow in any direction. + for (int dataIndex = internalAnchor.Index; dataIndex < index + 1; ++dataIndex) + { + var element = context.GetOrCreateElementAt(dataIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + element.Measure(_algorithmCallbacks.Algorithm_GetMeasureSize(dataIndex, availableSize, context)); + _elementManager.Add(element, dataIndex); + } + } + + private bool IsReflowRequired() + { + // If first element is realized and is not at the very beginning we need to reflow. + return + _elementManager.GetRealizedElementCount() > 0 && + _elementManager.GetDataIndexFromRealizedRangeIndex(0) == 0 && + _orientation.MinorStart(_elementManager.GetLayoutBoundsForRealizedIndex(0)) != 0; + } + + private bool ShouldContinueFillingUpSpace( + int index, + GenerateDirection direction) + { + bool shouldContinue = false; + if (!IsVirtualizingContext) + { + shouldContinue = true; + } + else + { + var realizationRect = _context.RealizationRect; + var elementBounds = _elementManager.GetLayoutBoundsForDataIndex(index); + + var elementMajorStart = _orientation.MajorStart(elementBounds); + var elementMajorEnd = _orientation.MajorEnd(elementBounds); + var rectMajorStart = _orientation.MajorStart(realizationRect); + var rectMajorEnd = _orientation.MajorEnd(realizationRect); + + var elementMinorStart = _orientation.MinorStart(elementBounds); + var elementMinorEnd = _orientation.MinorEnd(elementBounds); + var rectMinorStart = _orientation.MinorStart(realizationRect); + var rectMinorEnd = _orientation.MinorEnd(realizationRect); + + // Ensure that both minor and major directions are taken into consideration so that if the scrolling direction + // is the same as the flow direction we still stop at the end of the viewport rectangle. + shouldContinue = + (direction == GenerateDirection.Forward && elementMajorStart < rectMajorEnd && elementMinorStart < rectMinorEnd) || + (direction == GenerateDirection.Backward && elementMajorEnd > rectMajorStart && elementMinorEnd > rectMinorStart); + } + + return shouldContinue; + } + + private Rect EstimateExtent(Size availableSize, string layoutId) + { + IControl firstRealizedElement = null; + Rect firstBounds = new Rect(); + IControl lastRealizedElement = null; + Rect lastBounds = new Rect(); + int firstDataIndex = -1; + int lastDataIndex = -1; + + if (_elementManager.GetRealizedElementCount() > 0) + { + firstRealizedElement = _elementManager.GetAt(0); + firstBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); + firstDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);; + + int last = _elementManager.GetRealizedElementCount() - 1; + lastRealizedElement = _elementManager.GetAt(last); + lastDataIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(last); + lastBounds = _elementManager.GetLayoutBoundsForRealizedIndex(last); + } + + Rect extent = _algorithmCallbacks.Algorithm_GetExtent( + availableSize, + _context, + firstRealizedElement, + firstDataIndex, + firstBounds, + lastRealizedElement, + lastDataIndex, + lastBounds); + + return extent; + } + + private void RaiseLineArranged() + { + var realizationRect = RealizationRect; + if (realizationRect.Width != 0.0f || realizationRect.Height != 0.0f) + { + int realizedElementCount = _elementManager.GetRealizedElementCount(); + if (realizedElementCount > 0) + { + //MUX_ASSERT(_firstRealizedDataIndexInsideRealizationWindow != -1 && _lastRealizedDataIndexInsideRealizationWindow != -1); + int countInLine = 0; + var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow); + var currentLineOffset = _orientation.MajorStart(previousElementBounds); + var currentLineSize = _orientation.MajorSize(previousElementBounds); + for (int currentDataIndex = _firstRealizedDataIndexInsideRealizationWindow; currentDataIndex <= _lastRealizedDataIndexInsideRealizationWindow; currentDataIndex++) + { + var currentBounds = _elementManager.GetLayoutBoundsForDataIndex(currentDataIndex); + if (_orientation.MajorStart(currentBounds) != currentLineOffset) + { + // Staring a new line + _algorithmCallbacks.Algorithm_OnLineArranged(currentDataIndex - countInLine, countInLine, currentLineSize, _context); + countInLine = 0; + currentLineOffset = _orientation.MajorStart(currentBounds); + currentLineSize = 0; + } + + currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds)); + countInLine++; + previousElementBounds = currentBounds; + } + + // Raise for the last line. + _algorithmCallbacks.Algorithm_OnLineArranged(_lastRealizedDataIndexInsideRealizationWindow - countInLine + 1, countInLine, currentLineSize, _context); + } + } + } + + private void ArrangeVirtualizingLayout( + Size finalSize, + LineAlignment lineAlignment, + string layoutId) + { + // Walk through the realized elements one line at a time and + // align them, Then call element.Arrange with the arranged bounds. + int realizedElementCount = _elementManager.GetRealizedElementCount(); + if (realizedElementCount > 0) + { + var countInLine = 1; + var previousElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); + var currentLineOffset = _orientation.MajorStart(previousElementBounds); + var spaceAtLineStart = _orientation.MinorStart(previousElementBounds); + var spaceAtLineEnd = 0.0; + var currentLineSize = _orientation.MajorSize(previousElementBounds); + for (int i = 1; i < realizedElementCount; i++) + { + var currentBounds = _elementManager.GetLayoutBoundsForRealizedIndex(i); + if (_orientation.MajorStart(currentBounds) != currentLineOffset) + { + spaceAtLineEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); + PerformLineAlignment(i - countInLine, countInLine, spaceAtLineStart, spaceAtLineEnd, currentLineSize, lineAlignment, layoutId); + spaceAtLineStart = _orientation.MinorStart(currentBounds); + countInLine = 0; + currentLineOffset = _orientation.MajorStart(currentBounds); + currentLineSize = 0; + } + + countInLine++; // for current element + currentLineSize = Math.Max(currentLineSize, _orientation.MajorSize(currentBounds)); + previousElementBounds = currentBounds; + } + + // Last line - potentially have a property to customize + // aligning the last line or not. + if (countInLine > 0) + { + var spaceAtEnd = _orientation.Minor(finalSize) - _orientation.MinorStart(previousElementBounds) - _orientation.MinorSize(previousElementBounds); + PerformLineAlignment(realizedElementCount - countInLine, countInLine, spaceAtLineStart, spaceAtEnd, currentLineSize, lineAlignment, layoutId); + } + } + } + + // Align elements within a line. Note that this does not modify LayoutBounds. So if we get + // repeated measures, the LayoutBounds remain the same in each layout. + private void PerformLineAlignment( + int lineStartIndex, + int countInLine, + double spaceAtLineStart, + double spaceAtLineEnd, + double lineSize, + LineAlignment lineAlignment, + string layoutId) + { + for (int rangeIndex = lineStartIndex; rangeIndex < lineStartIndex + countInLine; ++rangeIndex) + { + var bounds = _elementManager.GetLayoutBoundsForRealizedIndex(rangeIndex); + _orientation.SetMajorSize(ref bounds, lineSize); + + if (!_scrollOrientationSameAsFlow) + { + // Note: Space at start could potentially be negative + if (spaceAtLineStart != 0 || spaceAtLineEnd != 0) + { + var totalSpace = spaceAtLineStart + spaceAtLineEnd; + var minorStart = _orientation.MinorStart(bounds); + switch (lineAlignment) + { + case LineAlignment.Start: + { + _orientation.SetMinorStart(ref bounds, minorStart - spaceAtLineStart); + break; + } + + case LineAlignment.End: + { + _orientation.SetMinorStart(ref bounds, minorStart + spaceAtLineEnd); + break; + } + + case LineAlignment.Center: + { + _orientation.SetMinorStart(ref bounds, (minorStart - spaceAtLineStart) + (totalSpace / 2)); + break; + } + + case LineAlignment.SpaceAround: + { + var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine * 2) : 0; + _orientation.SetMinorStart( + ref bounds, + (minorStart - spaceAtLineStart) + (interItemSpace * ((rangeIndex - lineStartIndex + 1) * 2 - 1))); + break; + } + + case LineAlignment.SpaceBetween: + { + var interItemSpace = countInLine > 1 ? totalSpace / (countInLine - 1) : 0; + _orientation.SetMinorStart( + ref bounds, + (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex))); + break; + } + + case LineAlignment.SpaceEvenly: + { + var interItemSpace = countInLine >= 1 ? totalSpace / (countInLine + 1) : 0; + _orientation.SetMinorStart( + ref bounds, + (minorStart - spaceAtLineStart) + (interItemSpace * (rangeIndex - lineStartIndex + 1))); + break; + } + } + } + } + + bounds = bounds.Translate(-_lastExtent.Position); + var element = _elementManager.GetAt(rangeIndex); + element.Arrange(bounds); + } + } + + void SetLayoutOrigin() + { + if (IsVirtualizingContext) + { + _context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y); + } + else + { + // Should have 0 origin for non-virtualizing layout since we always start from + // the first item + //MUX_ASSERT(m_lastExtent.X == 0 && m_lastExtent.Y == 0); + } + } + + public enum LineAlignment + { + Start, + Center, + End, + SpaceAround, + SpaceBetween, + SpaceEvenly, + } + + private enum GenerateDirection + { + Forward, + Backward, + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs new file mode 100644 index 0000000000..e27f00540c --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + internal struct FlowLayoutAnchorInfo + { + public int Index { get; set; } + public double Offset { get; set; } + } + + internal interface IFlowLayoutAlgorithmDelegates + { + Size Algorithm_GetMeasureSize(int index, Size availableSize, VirtualizingLayoutContext context); + Size Algorithm_GetProvisionalArrangeSize(int index, Size measureSize, Size desiredSize, VirtualizingLayoutContext context); + bool Algorithm_ShouldBreakLine(int index, double remainingSpace); + FlowLayoutAnchorInfo Algorithm_GetAnchorForRealizationRect(Size availableSize, VirtualizingLayoutContext context); + FlowLayoutAnchorInfo Algorithm_GetAnchorForTargetElement(int targetIndex, Size availableSize, VirtualizingLayoutContext context); + Rect Algorithm_GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + IControl firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + IControl lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds); + void Algorithm_OnElementMeasured( + IControl element, + int index, + Size availableSize, + Size measureSize, + Size desiredSize, + Size provisionalArrangeSize, + VirtualizingLayoutContext context); + void Algorithm_OnLineArranged( + int startIndex, + int countInLine, + double lineSize, + VirtualizingLayoutContext context); + } +} diff --git a/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs b/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs new file mode 100644 index 0000000000..55ce6c28ca --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + internal interface ITrackerHandleManager + { + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs new file mode 100644 index 0000000000..c26d73c02c --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls.Repeaters +{ + internal class ItemTemplateWrapper + { + private readonly IDataTemplate _dataTemplate; + + public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; + + public IControl GetElement(ElementFactoryGetArgs args) + { + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + IControl element = null; + + if (recyclePool != null) + { + // try to get an element from the recycle pool. + element = recyclePool.TryGetElement(string.Empty, args.Parent); + } + + if (element == null) + { + // no element was found in recycle pool, create a new element + element = selectedTemplate.Build(args.Data); + + // Associate template with element + element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate); + } + + return element; + } + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + var element = args.Element; + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + if (recyclePool == null) + { + // No Recycle pool in the template, create one. + recyclePool = new RecyclePool(); + RecyclePool.SetPoolInstance(selectedTemplate, recyclePool); + } + + recyclePool.PutElement(args.Element, "" /* key */, args.Parent); + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs new file mode 100644 index 0000000000..9d71685b11 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -0,0 +1,631 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using Avalonia.Controls.Templates; +using Avalonia.Input; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsRepeater : Panel + { + public static readonly AvaloniaProperty HorizontalCacheLengthProperty = + AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); + public static readonly StyledProperty ItemTemplateProperty = + ItemsControl.ItemTemplateProperty.AddOwner(); + public static readonly DirectProperty ItemsProperty = + ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + public static readonly AvaloniaProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + public static readonly AvaloniaProperty VerticalCacheLengthProperty = + AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); + private static readonly AttachedProperty VirtualizationInfoProperty = + AvaloniaProperty.RegisterAttached("VirtualizationInfo"); + + internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); + internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0); + + private readonly ViewManager _viewManager; + private readonly ViewportManager _viewportManager; + private IEnumerable _items; + private VirtualizingLayoutContext _layoutContext; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private Size _lastAvailableSize; + private bool _isLayoutInProgress; + private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; + private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; + private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + + public ItemsRepeater() + { + _viewManager = new ViewManager(this); + _viewportManager = new ViewportManager(this); + KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); + OnLayoutChanged(null, Layout); + } + + static ItemsRepeater() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + } + + public Layout Layout + { + get => GetValue(LayoutProperty); + set => SetValue(LayoutProperty, value); + } + + public IEnumerable Items + { + get => _items; + set => SetAndRaise(ItemsProperty, ref _items, value); + } + + public IDataTemplate ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + public double HorizontalCacheLength + { + get => GetValue(HorizontalCacheLengthProperty); + set => SetValue(HorizontalCacheLengthProperty, value); + } + + public double VerticalCacheLength + { + get => GetValue(VerticalCacheLengthProperty); + set => SetValue(VerticalCacheLengthProperty, value); + } + + public ItemsSourceView ItemsSourceView { get; private set; } + + internal ItemTemplateWrapper ItemTemplateShim { get; set; } + internal Point LayoutOrigin { get; set; } + internal object LayoutState { get; set; } + internal IControl MadeAnchor => _viewportManager.MadeAnchor; + internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow(); + internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor; + + private bool IsProcessingCollectionChange => _processingItemsSourceChange != null; + + private LayoutContext LayoutContext + { + get + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } + + public event EventHandler ElementClearing; + public event EventHandler ElementIndexChanged; + public event EventHandler ElementPrepared; + + public int GetElementIndex(IControl element) => GetElementIndexImpl(element); + + public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); + + public void PinElement(IControl element) => _viewManager.UpdatePin(element, true); + + public void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); + + public IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + + internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) + { + var value = element.GetValue(VirtualizationInfoProperty); + return value; + } + + internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element) + { + if (TryGetVirtualizationInfo(element) != null) + { + throw new InvalidOperationException("VirtualizationInfo already created."); + } + + var result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + return result; + } + + internal static VirtualizationInfo GetVirtualizationInfo(IControl element) + { + var result = element.GetValue(VirtualizationInfoProperty); + + if (result == null) + { + result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + } + + return result; + } + + protected override Size MeasureOverride(Size availableSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _viewportManager.OnOwnerMeasuring(); + + _isLayoutInProgress = true; + + try + { + _viewManager.PrunePinnedElements(); + var extent = new Rect(); + var desiredSize = new Size(); + var layout = Layout; + + if (layout != null) + { + var layoutContext = GetLayoutContext(); + + desiredSize = layout.Measure(layoutContext, availableSize); + extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height); + + // Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not + // call GetElementAt(index). + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + + if (virtInfo.Owner == ElementOwner.Layout && + virtInfo.AutoRecycleCandidate && + !virtInfo.KeepAlive) + { + ClearElementImpl(element); + } + } + } + + _viewportManager.SetLayoutExtent(extent); + _lastAvailableSize = availableSize; + return desiredSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _isLayoutInProgress = true; + + try + { + var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default; + + // The view manager might clear elements during this call. + // That's why we call it before arranging cleared elements + // off screen. + _viewManager.OnOwnerArranged(); + + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + virtInfo.KeepAlive = false; + + if (virtInfo.Owner == ElementOwner.ElementFactory || + virtInfo.Owner == ElementOwner.PinnedPool) + { + // Toss it away. And arrange it with size 0 so that XYFocus won't use it. + element.Arrange(new Rect( + ClearedElementsArrangePosition.X - element.DesiredSize.Width, + ClearedElementsArrangePosition.Y - element.DesiredSize.Height, + 0, + 0)); + } + else + { + var newBounds = element.Bounds; + + //if (virtInfo.ArrangeBounds != ItemsRepeater.InvalidRect && + // newBounds != virtInfo.ArrangeBounds) + //{ + // _animationManager.OnElementBoundsChanged(element, virtInfo.ArrangeBounds, newBounds); + //} + + virtInfo.ArrangeBounds = newBounds; + } + } + + _viewportManager.OnOwnerArranged(); + //_animationManager.OnOwnerArranged(); + + return arrangeSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + InvalidateMeasure(); + _viewportManager.ResetScrollers(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _viewportManager.ResetScrollers(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + var property = args.Property; + + if (property == ItemsProperty) + { + var newValue = (IEnumerable)args.NewValue; + var newDataSource = newValue as ItemsSourceView; + if (newValue != null && newDataSource == null) + { + newDataSource = new ItemsSourceView(newValue); + } + + OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); + } + else if (property == ItemTemplateProperty) + { + OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); + } + else if (property == LayoutProperty) + { + OnLayoutChanged((Layout)args.OldValue, (Layout)args.NewValue); + } + //else if (property == AnimatorProperty) + //{ + // OnAnimatorChanged((ElementAnimator)args.OldValue, (ElementAnimator)args.NewValue); + //} + else if (property == HorizontalCacheLengthProperty) + { + _viewportManager.HorizontalCacheLength = (double)args.NewValue; + } + else if (property == VerticalCacheLengthProperty) + { + _viewportManager.VerticalCacheLength = (double)args.NewValue; + } + else + { + base.OnPropertyChanged(args); + } + } + + internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) + { + var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); + return element; + } + + internal void ClearElementImpl(IControl element) + { + // Clearing an element due to a collection change + // is more strict in that pinned elements will be forcibly + // unpinned and sent back to the view generator. + var isClearedDueToCollectionChange = + _processingItemsSourceChange != null && + (_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); + + _viewManager.ClearElement(element, isClearedDueToCollectionChange); + _viewportManager.OnElementCleared(element); + } + + private int GetElementIndexImpl(IControl element) + { + var virtInfo = TryGetVirtualizationInfo(element); + return _viewManager.GetElementIndex(virtInfo); + } + + private IControl GetElementFromIndexImpl(int index) + { + IControl result = null; + + var children = Children; + for (var i = 0; i < children.Count && result == null; ++i) + { + var element = children[i]; + var virtInfo = TryGetVirtualizationInfo(element); + if (virtInfo?.IsRealized == true && virtInfo.Index == index) + { + result = element; + } + } + + return result; + } + + private IControl GetOrCreateElementImpl(int index) + { + if (index >= 0 && index >= ItemsSourceView.Count) + { + throw new ArgumentException("Argument index is invalid.", "index"); + } + + if (_isLayoutInProgress) + { + throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout."); + } + + var element = GetElementFromIndexImpl(index); + bool isAnchorOutsideRealizedRange = element == null; + + if (isAnchorOutsideRealizedRange) + { + if (Layout == null) + { + throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); + } + + element = GetLayoutContext().GetOrCreateElementAt(index); + element.Measure(Size.Infinity); + } + + _viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange); + InvalidateMeasure(); + + return element; + } + + internal void OnElementPrepared(IControl element, int index) + { + _viewportManager.OnElementPrepared(element); + if (ElementPrepared != null) + { + if (_elementPreparedArgs == null) + { + _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); + } + else + { + _elementPreparedArgs.Update(element, index); + } + + ElementPrepared(this, _elementPreparedArgs); + } + } + + internal void OnElementClearing(IControl element) + { + if (ElementClearing != null) + { + if (_elementClearingArgs == null) + { + _elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element); + } + else + { + _elementClearingArgs.Update(element); + } + + ElementClearing(this, _elementClearingArgs); + } + } + + internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) + { + if (ElementIndexChanged != null) + { + if (_elementIndexChangedArgs == null) + { + _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); + } + else + { + _elementIndexChangedArgs.Update(element, oldIndex, newIndex); + } + + ElementIndexChanged(this, _elementIndexChangedArgs); + } + } + + private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); + } + + ItemsSourceView?.Dispose(); + ItemsSourceView = newValue; + + if (oldValue != null) + { + oldValue.CollectionChanged -= OnItemsSourceViewChanged; + } + + if (newValue != null) + { + newValue.CollectionChanged += OnItemsSourceViewChanged; + } + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args); + } + else if (Layout is NonVirtualizingLayout nonVirtualLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + + InvalidateMeasure(); + } + } + + private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue) + { + if (_isLayoutInProgress && oldValue != null) + { + throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout."); + } + + // Since the ItemTemplate has changed, we need to re-evaluate all the items that + // have already been created and are now in the tree. The easiest way to do that + // would be to do a reset.. Note that this has to be done before we change the template + // so that the cleared elements go back into the old template. + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + _processingItemsSourceChange = args; + + try + { + virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args); + } + finally + { + _processingItemsSourceChange = null; + } + } + else if (Layout is NonVirtualizingLayout nonVirtualLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + } + + ItemTemplateShim = new ItemTemplateWrapper(newValue); + + InvalidateMeasure(); + } + + private void OnLayoutChanged(Layout oldValue, Layout newValue) + { + if (_isLayoutInProgress) + { + throw new InvalidOperationException("Layout cannot be changed during layout."); + } + + _viewManager.OnLayoutChanging(); + + if (oldValue != null) + { + oldValue.UninitializeForContext(LayoutContext); + oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; + oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; + + // Walk through all the elements and make sure they are cleared + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + + LayoutState = null; + } + + if (newValue != null) + { + newValue.InitializeForContext(LayoutContext); + newValue.MeasureInvalidated += InvalidateMeasureForLayout; + newValue.ArrangeInvalidated += InvalidateArrangeForLayout; + } + + bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; + _viewportManager.OnLayoutChanged(isVirtualizingLayout); + InvalidateMeasure(); + } + + private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (_isLayoutInProgress) + { + // Bad things will follow if the data changes while we are in the middle of a layout pass. + throw new InvalidOperationException("Changes in data source are not allowed during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source."); + } + + _processingItemsSourceChange = args; + + try + { + //_animationManager.OnItemsSourceChanged(sender, args); + _viewManager.OnItemsSourceChanged(sender, args); + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + virtualLayout.OnItemsChangedCore(GetLayoutContext(), sender, args); + } + else + { + // NonVirtualizingLayout + InvalidateMeasure(); + } + } + } + finally + { + _processingItemsSourceChange = null; + } + } + + private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure(); + + private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange(); + + private VirtualizingLayoutContext GetLayoutContext() + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs new file mode 100644 index 0000000000..20e62bc3b8 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs @@ -0,0 +1,11 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsRepeaterElementClearingEventArgs : EventArgs + { + internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element; + public IControl Element { get; private set; } + internal void Update(IControl element) => Element = element; + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs new file mode 100644 index 0000000000..c81c997bd4 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -0,0 +1,27 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs + { + internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + + public IControl Element { get; private set; } + + public int NewIndex { get; private set; } + + public int OldIndex { get; private set; } + + internal void Update(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs new file mode 100644 index 0000000000..3180058fae --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsRepeaterElementPreparedEventArgs + { + internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) + { + Element = element; + Index = index; + } + + public IControl Element { get; private set; } + + public int Index { get; private set; } + + internal void Update(IControl element, int index) + { + Element = element; + Index = index; + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs new file mode 100644 index 0000000000..0d21af0975 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace Avalonia.Controls.Repeaters +{ + public class ItemsSourceView : INotifyCollectionChanged, IDisposable + { + private readonly IList _inner; + private INotifyCollectionChanged _notifyCollectionChanged; + private int _cachedSize = -1; + + public ItemsSourceView(IEnumerable source) + { + Contract.Requires(source != null); + + _inner = source as IList; + + if (_inner == null && source is IEnumerable objectEnumerable) + { + _inner = new List(objectEnumerable); + } + else + { + _inner = new List(source.Cast()); + } + + ListenToCollectionChanges(); + } + + public int Count + { + get + { + if (_cachedSize == -1) + { + _cachedSize = _inner.Count; + } + + return _cachedSize; + } + } + + public bool HasKeyIndexMapping => false; + + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + public void Dispose() + { + if (_notifyCollectionChanged != null) + { + _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + } + } + + public object GetAt(int index) => _inner[index]; + + public string KeyFromIndex(int index) + { + throw new NotImplementedException(); + } + + 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/Repeaters/Layout.cs b/src/Avalonia.Controls/Repeaters/Layout.cs new file mode 100644 index 0000000000..a03e881616 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/Layout.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + public abstract class Layout : AvaloniaObject + { + public string LayoutId { get; set; } + + public event EventHandler MeasureInvalidated; + public event EventHandler ArrangeInvalidated; + + public abstract void InitializeForContext(LayoutContext context); + + public abstract void UninitializeForContext(LayoutContext context); + + public abstract Size Measure(LayoutContext context, Size availableSize); + + public abstract Size Arrange(LayoutContext context, Size finalSize); + + protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty); + + protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/Avalonia.Controls/Repeaters/LayoutContext.cs b/src/Avalonia.Controls/Repeaters/LayoutContext.cs new file mode 100644 index 0000000000..ce462f6a16 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/LayoutContext.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public class LayoutContext : AvaloniaObject + { + public object LayoutState { get; set; } + + protected virtual object LayoutStateCore { get; set; } + } +} diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs new file mode 100644 index 0000000000..c5ab27fabb --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public class NonVirtualizingLayout : Layout + { + public override Size Arrange(LayoutContext context, Size finalSize) + { + throw new NotImplementedException(); + } + + public override void InitializeForContext(LayoutContext context) + { + throw new NotImplementedException(); + } + + public override Size Measure(LayoutContext context, Size availableSize) + { + throw new NotImplementedException(); + } + + public override void UninitializeForContext(LayoutContext context) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs new file mode 100644 index 0000000000..173b62842b --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs @@ -0,0 +1,78 @@ +namespace Avalonia.Controls.Repeaters +{ + internal class OrientationBasedMeasures + { + public Orientation ScrollOrientation { get; set; } = Orientation.Vertical; + + public double Major(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Height : size.Width; + public double Minor(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Width : size.Height; + public double MajorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Height : rect.Width; + public double MinorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Width : rect.Height; + public double MajorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Y : rect.X; + public double MinorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.X : rect.Y; + public double MajorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Bottom : rect.Right; + public double MinorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Right : rect.Bottom; + + public void SetMajorSize(ref Rect rect, double value) + { + if (ScrollOrientation == Orientation.Vertical) + { + rect = rect.WithHeight(value); + } + else + { + rect = rect.WithWidth(value); + } + } + + public void SetMinorSize(ref Rect rect, double value) + { + if (ScrollOrientation == Orientation.Vertical) + { + rect = rect.WithWidth(value); + } + else + { + rect = rect.WithHeight(value); + } + } + + public void SetMajorStart(ref Rect rect, double value) + { + if (ScrollOrientation == Orientation.Vertical) + { + rect = rect.WithY(value); + } + else + { + rect = rect.WithX(value); + } + } + + public void SetMinorStart(ref Rect rect, double value) + { + if (ScrollOrientation == Orientation.Vertical) + { + rect = rect.WithX(value); + } + else + { + rect = rect.WithY(value); + } + } + + public Point MinorMajorPoint(double minor, double major) + { + return ScrollOrientation == Orientation.Vertical ? + new Point(minor, major) : + new Point(major, minor); + } + + public Size MinorMajorSize(double minor, double major) + { + return ScrollOrientation == Orientation.Vertical ? + new Size(minor, major) : + new Size(major, minor); + } + } +} diff --git a/src/Avalonia.Controls/Repeaters/RecyclePool.cs b/src/Avalonia.Controls/Repeaters/RecyclePool.cs new file mode 100644 index 0000000000..e602a6b674 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/RecyclePool.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls.Repeaters +{ + 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/Repeaters/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs new file mode 100644 index 0000000000..3f002c3121 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + 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 IControl 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(IControl element) => _owner.ClearElementImpl(element); + + protected override Rect RealizationRectCore() => _owner.RealizationWindow; + } +} diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs new file mode 100644 index 0000000000..79ebcef645 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Specialized; + +namespace Avalonia.Controls.Repeaters +{ + public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates + { + public static readonly AvaloniaProperty OrientationProperty + = StackPanel.OrientationProperty.AddOwner(); + + public static readonly AvaloniaProperty SpacingProperty + = StackPanel.SpacingProperty.AddOwner(); + + private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + public double Spacing + { + get => GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + public Rect GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + IControl firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + IControl 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) + { + //MUX_ASSERT(lastRealized); + _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; + } + + public void OnElementMeasured( + IControl 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, + IControl firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + IControl lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds) + { + return GetExtent( + availableSize, + context, + firstRealized, + firstRealizedItemIndex, + firstRealizedLayoutBounds, + lastRealized, + lastRealizedItemIndex, + lastRealizedLayoutBounds); + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl 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 && + 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) + { + //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. + //Horizontal Orientation means we have a Horizontal ScrollOrientation. + _orientation.ScrollOrientation = (Orientation)e.NewValue; + } + + 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.Controls/Repeaters/StackLayoutState.cs b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs new file mode 100644 index 0000000000..cefaf38f3a --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Controls.Repeaters +{ + 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.Controls/Repeaters/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs new file mode 100644 index 0000000000..4b3922b9c3 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + 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) + { + //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); + + 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) + { + //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); + + // 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() + { + //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); + _elementMap.Clear(); + } + + public IEnumerator> GetEnumerator() => _elementMap.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs new file mode 100644 index 0000000000..d7f8f224b8 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -0,0 +1,787 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Repeaters +{ + 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 ElementFactoryGetArgs _elementFactoryGetArgs; + private ElementFactoryRecycleArgs _elementFactoryRecycleArgs; + 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); + + if (_elementFactoryRecycleArgs == null) + { + // Create one. + _elementFactoryRecycleArgs = new ElementFactoryRecycleArgs(); + } + + var context = _elementFactoryRecycleArgs; + context.Element = element; + context.Parent = _owner; + + _owner.ItemTemplateShim.RecycleElement(context); + + context.Element = null; + context.Parent = null; + + virtInfo.MoveOwnershipToElementFactory(); + //_phaser.StopPhasing(element, virtInfo); + 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) + { + //var focusState = _lastFocusedElement?.FocusState ?? FocusState.Programmatic; + + // If the last focused element has focus, use its focus state, if not use programmatic. + //focusState = focusState == FocusState.Unfocused ? FocusState.Programmatic : focusState; + 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; + } + } + } + } + + // 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. + IControl focusCandidate = null; + if (nextElement != null) + { + focusCandidate = nextElement as IControl; + if (focusCandidate != null) + { + ////var firstFocus = FocusManager.FindFirstFocusableElement(nextElement); + + ////if (firstFocus != null) + ////{ + //// focusCandidate = firstFocus as IControl; + ////} + } + } + + if (focusCandidate == null && previousElement != null) + { + focusCandidate = previousElement as IControl; + if (previousElement != null) + { + ////var lastFocus = FocusManager.FindLastFocusableElement(previousElement); + + ////if (lastFocus != null) + ////{ + //// focusCandidate = lastFocus as IControl; + ////} + } + } + + return focusCandidate; + } + + 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; + + //MUX_ASSERT(virtInfo.Owner() == ElementOwner.PinnedPool); + + 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; + //MUX_ASSERT(!cachedFirstLastIndicesInvalid || m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault); + + bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout); + + if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange) + { + // Both First and Last indices need to be valid or default. + //MUX_ASSERT((m_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault) || + // (m_firstRealizedElementIndexHeldByLayout != FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout != LastRealizedElementIndexDefault)); + + 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); + + if (_elementFactoryGetArgs == null) + { + // Create one. + _elementFactoryGetArgs = new ElementFactoryGetArgs(); + } + + var args = _elementFactoryGetArgs; + args.Data = data; + args.Parent = _owner; + args.Index= index; + + var element = itemTemplateFactory.GetElement(args); + + args.Data = null; + args.Parent = null; + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (virtInfo == null) + { + virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); + } + + // Prepare the element + // If we are phasing, run phase 0 before setting DataContext. If phase 0 is not + // run before setting DataContext, when setting DataContext all the phases will be + // run in the OnDataContextChanged handler in code generated by the xaml compiler (code-gen). + var extension = false; ////CachedVisualTreeHelpers.GetDataTemplateComponent(element); + if (extension) + { + ////// Clear out old data. + ////extension.Recycle(); + ////int nextPhase = VirtualizationInfo.PhaseReachedEnd; + ////// Run Phase 0 + ////extension.ProcessBindings(data, index, 0 /* currentPhase */, nextPhase); + + ////// Setup phasing information, so that Phaser can pick up any pending phases left. + ////// Update phase on virtInfo. Set data and templateComponent only if x:Phase was used. + ////virtInfo.UpdatePhasingInfo(nextPhase, nextPhase > 0 ? data : null, nextPhase > 0 ? extension : null); + } + else + { + // Set data context only if no x:Bind was used. ie. No data template component on the root. + 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.AnimationManager.OnElementPrepared(element); + repeater.OnElementPrepared(element, index); + ////_phaser.PhaseElement(element, virtInfo); + + // 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 ClearElementToAnimator(IControl element, VirtualizationInfo virtInfo) + { + return false; + ////bool cleared = _owner.AnimationManager.ClearElement(element); + ////if (cleared) + ////{ + //// int clearedIndex = virtInfo.Index; + //// virtInfo.MoveOwnershipToAnimator(); + //// 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); + //// } + ////} + ////return cleared; + } + + 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/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs new file mode 100644 index 0000000000..5986c75579 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -0,0 +1,463 @@ +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.Repeaters +{ + internal class ViewportManager + { + private const double CacheBufferPerSideInflationPixelDelta = 40.0; + private readonly ItemsRepeater _owner; + 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 = 2.0; + private double _maximumVerticalCacheLength = 2.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; + } + + // TODO: Implement + public IControl SuggestedAnchor => null; + + // TODO: Implement + public bool HasScroller => false; + + public IControl MadeAnchor => _makeAnchorElement; + + public double HorizontalCacheLength + { + get => _maximumHorizontalCacheLength; + set + { + if (_maximumHorizontalCacheLength != value) + { + ValidateCacheLength(value); + _maximumHorizontalCacheLength = value; + ResetCacheBuffer(); + } + } + } + + public double VerticalCacheLength + { + get => _maximumVerticalCacheLength; + set + { + if (_maximumVerticalCacheLength != value) + { + ValidateCacheLength(value); + _maximumVerticalCacheLength = value; + ResetCacheBuffer(); + } + } + } + + 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. + ////if (_scroller != null) + ////{ + //// ((IControl)_scroller).InvalidateArrange(); + ////} + } + + public Point GetOrigin() => throw new NotImplementedException(); + + public void OnLayoutChanged(bool isVirtualizing) + { + _managingViewportDisabled = !isVirtualizing; + + _layoutExtent = default; + _expectedViewportShift = default; + _pendingViewportShift = default; + _unshiftableShift = default; + ResetCacheBuffer(); + + _effectiveViewportChangedRevoker?.Dispose(); + + if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker = _owner.GetObservable(Visual.TransformedBoundsProperty) + .Skip(1) + .Subscribe(OnEffectiveViewportChanged); + } + } + + 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(IControl 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); + + // Since we grow the cache buffer at the end of the arrange pass, + // we need to register work even if we just reached cache potential. + RegisterCacheBuildWork(); + } + } + } + } + + 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) + { + // We do not animate bring-into-view operations where the anchor is disconnected because + // it doesn't look good (the blank space is obvious because the layout can't keep track + // of two realized ranges while the animation is going on). + if (_isAnchorOutsideRealizedRange) + { + ////args.AnimationDesired(false); + } + + // 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(); + ////m_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 UpdateViewport(Rect viewport) + { + //assert(!m_managingViewportDisabled); + var previousVisibleWindow = _visibleWindow; + var currentVisibleWindow = viewport; + + if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && + -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) + { + // We got cleared. + _visibleWindow = default; + } + else + { + _visibleWindow = currentVisibleWindow; + } + + TryInvalidateMeasure(); + } + + private void ResetCacheBuffer() + { + _horizontalCacheBufferPerSide = 0.0; + _verticalCacheBufferPerSide = 0.0; + + if (!_managingViewportDisabled) + { + // We need to start building the realization buffer again. + RegisterCacheBuildWork(); + } + } + + 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 RegisterCacheBuildWork() + { + ////assert(!m_managingViewportDisabled); + if (_owner.Layout != null && + _cacheBuildAction == null) + { + // We capture 'owner' (a strong refernce on ItemsRepeater) to make sure ItemsRepeater is still around + // when the async action completes. By protecting ItemsRepeater, we also ensure that this instance + // of ViewportManager (referenced by 'this' pointer) is valid because the lifetime of ItemsRepeater + // and ViewportManager is the same (see ItemsRepeater::m_viewportManager). + // We can't simply hold a strong reference on ViewportManager because it's not a COM object. + ////auto strongOwner = m_owner->get_strong(); + ////m_cacheBuildAction.set( + //// m_owner->Dispatcher().RunIdleAsync([this, strongOwner](const winrt::IdleDispatchedHandlerArgs&) + ////{ + //// OnCacheBuildActionCompleted(); + ////})); + } + } + + 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 class ScrollerInfo + { + public ScrollerInfo(ScrollViewer scroller) + { + Scroller = scroller; + } + + public ScrollViewer Scroller { get; } + } + }; +} diff --git a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs new file mode 100644 index 0000000000..5c6554be81 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs @@ -0,0 +1,121 @@ +using System; + +namespace Avalonia.Controls.Repeaters +{ + 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) + { + //MUX_ASSERT(_owner == ElementOwner.ElementFactory); + Owner = ElementOwner.Layout; + Index = index; + UniqueId = uniqueId; + } + + public void MoveOwnershipToLayoutFromUniqueIdResetPool() + { + //MUX_ASSERT(_owner == ElementOwner.UniqueIdResetPool); + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToLayoutFromPinnedPool() + { + //MUX_ASSERT(_owner == ElementOwner.PinnedPool); + //MUX_ASSERT(IsPinned()); + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToElementFactory() + { + //MUX_ASSERT(_owner != ElementOwner.ElementFactory); + Owner = ElementOwner.ElementFactory; + _pinCounter = 0; + Index = -1; + UniqueId = string.Empty; + ArrangeBounds = ItemsRepeater.InvalidRect; + } + + public void MoveOwnershipToUniqueIdResetPoolFromLayout() + { + //MUX_ASSERT(_owner == ElementOwner.Layout); + 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. + //MUX_ASSERT(_owner == ElementOwner.Layout || _owner == ElementOwner.UniqueIdResetPool); + Owner = ElementOwner.Animator; + Index = -1; + _pinCounter = 0; + } + + public void MoveOwnershipToPinnedPool() + { + //MUX_ASSERT(_owner == ElementOwner.Layout); + 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/Repeaters/VirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs new file mode 100644 index 0000000000..fa31095f9e --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + public abstract class VirtualizingLayout : Layout + { + 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); + } + + protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + { + } + + protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + { + } + + protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + + protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + + protected internal virtual void OnItemsChangedCore( + VirtualizingLayoutContext context, + object source, + NotifyCollectionChangedEventArgs args) => InvalidateMeasure(); + } +} diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs b/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs new file mode 100644 index 0000000000..da8d6f0f03 --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/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.Controls.Repeaters +{ + /// + /// 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 IControl 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 IControl 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(IControl 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 IControl 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(IControl element); + } +} diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c29faa1b4d..b391ff061d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -161,8 +161,8 @@ namespace Avalonia.Controls /// The desired size of the control. protected override Size MeasureOverride(Size availableSize) { - double childAvailableWidth = double.PositiveInfinity; - double childAvailableHeight = double.PositiveInfinity; + double childAvailableWidth = availableSize.Width; + double childAvailableHeight = availableSize.Height; if (Orientation == Orientation.Vertical) { diff --git a/src/Avalonia.Controls/Utils/ListUtils.cs b/src/Avalonia.Controls/Utils/ListUtils.cs new file mode 100644 index 0000000000..8599c53057 --- /dev/null +++ b/src/Avalonia.Controls/Utils/ListUtils.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Controls.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.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)); } } From 4850571a90d3de5f09738d7978a33fea76a4c570 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 May 2019 22:37:07 +0200 Subject: [PATCH 02/30] Fix quick scrolling. With a bit of a hack... --- src/Avalonia.Controls/Repeaters/ViewportManager.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index 5986c75579..cf69f9beb6 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -188,7 +188,16 @@ namespace Avalonia.Controls.Repeaters if (!_managingViewportDisabled) { + // 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. _effectiveViewportChangedRevoker = _owner.GetObservable(Visual.TransformedBoundsProperty) + .Merge(_owner.GetObservable(Visual.BoundsProperty).Select(_ => _owner.TransformedBounds)) .Skip(1) .Subscribe(OnEffectiveViewportChanged); } From 20a49c713d1f6e729981774b9bc4a805bcc6c923 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 1 Jun 2019 00:06:36 +0200 Subject: [PATCH 03/30] Fix infinity checks. Misread `std::isfinite` in C++ code as `std::infinite`, ha. --- src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs | 2 +- src/Avalonia.Controls/Repeaters/StackLayout.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs index a0ced65076..f8da998201 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -337,7 +337,7 @@ namespace Avalonia.Controls.Repeaters // 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.SetMinorStart(ref currentBounds, !double.IsInfinity(availableSizeMinor) ? availableSizeMinor - _orientation.Minor(desiredSize) : 0); _orientation.SetMajorStart(ref currentBounds, lineOffset - _orientation.Major(desiredSize) - lineSpacing); if (lineNeedsReposition) diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index 79ebcef645..379eab8675 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Repeaters { var measureSizeMinor = _orientation.Minor(measureSize); return _orientation.MinorMajorSize( - double.IsInfinity(measureSizeMinor) ? + !double.IsInfinity(measureSizeMinor) ? Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) : _orientation.Minor(desiredSize), _orientation.Major(desiredSize)); From aef3a2c9bd6c66d4c1e69eda8bb7f46c837cca28 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 1 Jun 2019 16:20:38 +0200 Subject: [PATCH 04/30] Added UniformGridLayout. --- .../Repeaters/FlowLayoutAlgorithm.cs | 23 +- .../Repeaters/OrientationBasedMeasures.cs | 7 + .../Repeaters/StackLayout.cs | 13 +- .../Repeaters/UniformGridLayout.cs | 389 ++++++++++++++++++ .../Repeaters/UniformGridLayoutState.cs | 176 ++++++++ 5 files changed, 603 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Controls/Repeaters/UniformGridLayout.cs create mode 100644 src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs index f8da998201..6aaad0bdb5 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -656,7 +656,7 @@ namespace Avalonia.Controls.Repeaters } } - void SetLayoutOrigin() + private void SetLayoutOrigin() { if (IsVirtualizingContext) { @@ -670,6 +670,27 @@ namespace Avalonia.Controls.Repeaters } } + public IControl GetElementIfRealized(int dataIndex) + { + if (_elementManager.IsDataIndexRealized(dataIndex)) + { + return _elementManager.GetRealizedElement(dataIndex); + } + + return null; + } + + public bool TryAddElement0(IControl element) + { + if (_elementManager.GetRealizedElementCount() == 0) + { + _elementManager.Add(element, 0); + return true; + } + + return false; + } + public enum LineAlignment { Start, diff --git a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs index 173b62842b..2f30f4c551 100644 --- a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs +++ b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs @@ -61,6 +61,13 @@ } } + public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize) + { + return ScrollOrientation == Orientation.Vertical ? + new Rect(minor, major, minorSize, majorSize) : + new Rect(major, minor, majorSize, minorSize); + } + public Point MinorMajorPoint(double minor, double major) { return ScrollOrientation == Orientation.Vertical ? diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index 379eab8675..db1900d03b 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -5,14 +5,19 @@ namespace Avalonia.Controls.Repeaters { public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates { - public static readonly AvaloniaProperty OrientationProperty - = StackPanel.OrientationProperty.AddOwner(); + public static readonly StyledProperty OrientationProperty = + StackPanel.OrientationProperty.AddOwner(); - public static readonly AvaloniaProperty SpacingProperty - = StackPanel.SpacingProperty.AddOwner(); + public static readonly StyledProperty SpacingProperty = + StackPanel.SpacingProperty.AddOwner(); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + public StackLayout() + { + LayoutId = "StackLayout"; + } + public Orientation Orientation { get => GetValue(OrientationProperty); diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs new file mode 100644 index 0000000000..4d062e23ab --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Specialized; + +namespace Avalonia.Controls.Repeaters +{ + public enum UniformGridLayoutItemsJustification + { + Start = 0, + Center = 1, + End = 2, + SpaceAround = 3, + SpaceBetween = 4, + SpaceEvenly = 5, + }; + + public enum UniformGridLayoutItemsStretch + { + None = 0, + Fill = 1, + Uniform = 2, + }; + + public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates + { + public static readonly StyledProperty ItemsJustificationProperty = + AvaloniaProperty.Register(nameof(ItemsJustification)); + + public static readonly StyledProperty ItemsStretchProperty = + AvaloniaProperty.Register(nameof(ItemsStretch)); + + public static readonly StyledProperty MinColumnSpacingProperty = + AvaloniaProperty.Register(nameof(MinColumnSpacing)); + + public static readonly StyledProperty MinItemHeightProperty = + AvaloniaProperty.Register(nameof(MinItemHeight)); + + public static readonly StyledProperty MinItemWidthProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + + public static readonly StyledProperty MinRowSpacingProperty = + AvaloniaProperty.Register(nameof(MinRowSpacing)); + + public static readonly StyledProperty OrientationProperty = + StackPanel.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; + + public UniformGridLayout() + { + LayoutId = "UniformGridLayout"; + } + + static UniformGridLayout() + { + OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); + } + + public UniformGridLayoutItemsJustification ItemsJustification + { + get => GetValue(ItemsJustificationProperty); + set => SetValue(ItemsJustificationProperty, value); + } + + public UniformGridLayoutItemsStretch ItemsStretch + { + get => GetValue(ItemsStretchProperty); + set => SetValue(ItemsStretchProperty, value); + } + + public double MinColumnSpacing + { + get => GetValue(MinColumnSpacingProperty); + set => SetValue(MinColumnSpacingProperty, value); + } + + public double MinItemHeight + { + get => GetValue(MinItemHeightProperty); + set => SetValue(MinItemHeightProperty, value); + } + + public double MinItemWidth + { + get => GetValue(MinItemWidthProperty); + set => SetValue(MinItemWidthProperty, value); + } + + public double MinRowSpacing + { + get => GetValue(MinRowSpacingProperty); + set => SetValue(MinRowSpacingProperty, value); + } + + 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, + IControl firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + IControl 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) + { + ////MUX_ASSERT(lastRealized); + + _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(IControl 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); + + 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) ? Orientation.Vertical : Orientation.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 == Orientation.Vertical? + gridState.EffectiveItemWidth + minItemSpacing : + gridState.EffectiveItemHeight + minItemSpacing; + } + + private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context) + { + var lineSpacing = LineSpacing; + var gridState = (UniformGridLayoutState)context.LayoutState; + return _orientation.ScrollOrientation == Orientation.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 == Orientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, + _orientation.ScrollOrientation == Orientation.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.Controls/Repeaters/UniformGridLayoutState.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs new file mode 100644 index 0000000000..e62f64192d --- /dev/null +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; + +namespace Avalonia.Controls.Repeaters +{ + 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 IControl _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); + 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( + IControl 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() + { + if (FlowAlgorithm.GetElementIfRealized(0) != null) + { + _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; + } + } + } + } +} From 89268e9df66da5fb6c71c805964a9a95717f8dd8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 12:21:28 +0200 Subject: [PATCH 05/30] Allow switching ItemsRepeater layout in ControlCatalog. --- .../Pages/ItemsRepeaterPage.xaml | 24 +++++++-- .../Pages/ItemsRepeaterPage.xaml.cs | 53 ++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index a0cbefa4d9..dfe8be2cec 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,7 +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 index a6ca27cb67..dccb242a78 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -3,17 +3,23 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Repeaters; using Avalonia.Markup.Xaml; namespace ControlCatalog.Pages { public class ItemsRepeaterPage : UserControl { + private ItemsRepeater _repeater; + private ScrollViewer _scroller; + public ItemsRepeaterPage() { this.InitializeComponent(); - DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ) - .ToArray(); + _repeater = this.FindControl("repeater"); + _scroller = this.FindControl("scroller"); + DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray(); } private void InitializeComponent() @@ -21,5 +27,48 @@ namespace ControlCatalog.Pages 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; + } + } } } From e09b1e4bea92da015954d3af2f3ccc7ff64f3b4b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 12:59:40 +0200 Subject: [PATCH 06/30] Removed the ElementFactory args stuff. We're just using `IDataTemplate` so it's not needed. --- .../Repeaters/ElementFactoryGetArgs.cs | 11 ------ .../Repeaters/ElementFactoryRecycleArgs.cs | 12 ------- .../Repeaters/ItemTemplateWrapper.cs | 11 +++--- .../Repeaters/ViewManager.cs | 35 ++----------------- 4 files changed, 7 insertions(+), 62 deletions(-) delete mode 100644 src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs delete mode 100644 src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs deleted file mode 100644 index 173388b68e..0000000000 --- a/src/Avalonia.Controls/Repeaters/ElementFactoryGetArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Avalonia.Controls.Repeaters -{ - public sealed class ElementFactoryGetArgs : EventArgs - { - public object Data { get; set; } - public IControl Parent { get; set; } - internal int Index { get; set; } - } -} diff --git a/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs b/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs deleted file mode 100644 index 3025546f67..0000000000 --- a/src/Avalonia.Controls/Repeaters/ElementFactoryRecycleArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Controls.Repeaters -{ - public sealed class ElementFactoryRecycleArgs : EventArgs - { - public IControl Element { get; set; } - public IControl Parent { get; set; } - } -} diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs index c26d73c02c..300fd11d1c 100644 --- a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls.Repeaters public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; - public IControl GetElement(ElementFactoryGetArgs args) + public IControl GetElement(IControl parent, object data) { var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); @@ -20,13 +20,13 @@ namespace Avalonia.Controls.Repeaters if (recyclePool != null) { // try to get an element from the recycle pool. - element = recyclePool.TryGetElement(string.Empty, args.Parent); + element = recyclePool.TryGetElement(string.Empty, parent); } if (element == null) { // no element was found in recycle pool, create a new element - element = selectedTemplate.Build(args.Data); + element = selectedTemplate.Build(data); // Associate template with element element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate); @@ -35,9 +35,8 @@ namespace Avalonia.Controls.Repeaters return element; } - public void RecycleElement(ElementFactoryRecycleArgs args) + public void RecycleElement(IControl parent, IControl element) { - var element = args.Element; var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); if (recyclePool == null) @@ -47,7 +46,7 @@ namespace Avalonia.Controls.Repeaters RecyclePool.SetPoolInstance(selectedTemplate, recyclePool); } - recyclePool.PutElement(args.Element, "" /* key */, args.Parent); + recyclePool.PutElement(element, "" /* key */, parent); } } } diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs index d7f8f224b8..5fccba5412 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -18,8 +18,6 @@ namespace Avalonia.Controls.Repeaters private readonly UniqueIdElementPool _resetPool; private IControl _lastFocusedElement; private bool _isDataSourceStableResetPending; - private ElementFactoryGetArgs _elementFactoryGetArgs; - private ElementFactoryRecycleArgs _elementFactoryRecycleArgs; private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; private bool _eventsSubscribed; @@ -107,21 +105,7 @@ namespace Avalonia.Controls.Repeaters var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var clearedIndex = virtInfo.Index; _owner.OnElementClearing(element); - - if (_elementFactoryRecycleArgs == null) - { - // Create one. - _elementFactoryRecycleArgs = new ElementFactoryRecycleArgs(); - } - - var context = _elementFactoryRecycleArgs; - context.Element = element; - context.Parent = _owner; - - _owner.ItemTemplateShim.RecycleElement(context); - - context.Element = null; - context.Parent = null; + _owner.ItemTemplateShim.RecycleElement(_owner, element); virtInfo.MoveOwnershipToElementFactory(); //_phaser.StopPhasing(element, virtInfo); @@ -579,22 +563,7 @@ namespace Avalonia.Controls.Repeaters } var data = _owner.ItemsSourceView.GetAt(index); - - if (_elementFactoryGetArgs == null) - { - // Create one. - _elementFactoryGetArgs = new ElementFactoryGetArgs(); - } - - var args = _elementFactoryGetArgs; - args.Data = data; - args.Parent = _owner; - args.Index= index; - - var element = itemTemplateFactory.GetElement(args); - - args.Data = null; - args.Parent = null; + var element = itemTemplateFactory.GetElement(_owner, data); var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) From acfa8329672d23db92b8e9f6e7afc54cdbc77ad9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 13:34:28 +0200 Subject: [PATCH 07/30] Try up code. Remove unused code and implement some focus stuff. --- .../Repeaters/FlowLayoutAlgorithm.cs | 8 -- .../Repeaters/ITrackerHandleManager.cs | 10 --- .../Repeaters/ItemsRepeater.cs | 13 --- .../Repeaters/NonVirtualizingLayout.cs | 21 +---- .../Repeaters/StackLayout.cs | 1 - .../Repeaters/UniformGridLayout.cs | 2 - .../Repeaters/UniqueIdElementPool.cs | 5 -- .../Repeaters/ViewManager.cs | 79 +++---------------- .../Repeaters/ViewportManager.cs | 49 ------------ .../Repeaters/VirtualizationInfo.cs | 8 -- 10 files changed, 14 insertions(+), 182 deletions(-) delete mode 100644 src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs index 6aaad0bdb5..f605bf72c8 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -407,7 +407,6 @@ namespace Avalonia.Controls.Repeaters _elementManager.ClearRealizedRange(); // FlowLayout requires that the anchor is the first element in the row. var internalAnchor = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(index, availableSize, context); - //MUX_ASSERT(internalAnchor.Index <= index); // No need to set the position of the anchor. // (0,0) is fine for now since the extent can @@ -505,7 +504,6 @@ namespace Avalonia.Controls.Repeaters int realizedElementCount = _elementManager.GetRealizedElementCount(); if (realizedElementCount > 0) { - //MUX_ASSERT(_firstRealizedDataIndexInsideRealizationWindow != -1 && _lastRealizedDataIndexInsideRealizationWindow != -1); int countInLine = 0; var previousElementBounds = _elementManager.GetLayoutBoundsForDataIndex(_firstRealizedDataIndexInsideRealizationWindow); var currentLineOffset = _orientation.MajorStart(previousElementBounds); @@ -662,12 +660,6 @@ namespace Avalonia.Controls.Repeaters { _context.LayoutOrigin = new Point(_lastExtent.X, _lastExtent.Y); } - else - { - // Should have 0 origin for non-virtualizing layout since we always start from - // the first item - //MUX_ASSERT(m_lastExtent.X == 0 && m_lastExtent.Y == 0); - } } public IControl GetElementIfRealized(int dataIndex) diff --git a/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs b/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs deleted file mode 100644 index 55ce6c28ca..0000000000 --- a/src/Avalonia.Controls/Repeaters/ITrackerHandleManager.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Controls.Repeaters -{ - internal interface ITrackerHandleManager - { - } -} diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs index 9d71685b11..b0e9842d57 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -243,19 +243,11 @@ namespace Avalonia.Controls.Repeaters else { var newBounds = element.Bounds; - - //if (virtInfo.ArrangeBounds != ItemsRepeater.InvalidRect && - // newBounds != virtInfo.ArrangeBounds) - //{ - // _animationManager.OnElementBoundsChanged(element, virtInfo.ArrangeBounds, newBounds); - //} - virtInfo.ArrangeBounds = newBounds; } } _viewportManager.OnOwnerArranged(); - //_animationManager.OnOwnerArranged(); return arrangeSize; } @@ -299,10 +291,6 @@ namespace Avalonia.Controls.Repeaters { OnLayoutChanged((Layout)args.OldValue, (Layout)args.NewValue); } - //else if (property == AnimatorProperty) - //{ - // OnAnimatorChanged((ElementAnimator)args.OldValue, (ElementAnimator)args.NewValue); - //} else if (property == HorizontalCacheLengthProperty) { _viewportManager.HorizontalCacheLength = (double)args.NewValue; @@ -592,7 +580,6 @@ namespace Avalonia.Controls.Repeaters try { - //_animationManager.OnItemsSourceChanged(sender, args); _viewManager.OnItemsSourceChanged(sender, args); if (Layout != null) diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs index c5ab27fabb..fb8d329d7f 100644 --- a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs @@ -4,26 +4,7 @@ using System.Text; namespace Avalonia.Controls.Repeaters { - public class NonVirtualizingLayout : Layout + public abstract class NonVirtualizingLayout : Layout { - public override Size Arrange(LayoutContext context, Size finalSize) - { - throw new NotImplementedException(); - } - - public override void InitializeForContext(LayoutContext context) - { - throw new NotImplementedException(); - } - - public override Size Measure(LayoutContext context, Size availableSize) - { - throw new NotImplementedException(); - } - - public override void UninitializeForContext(LayoutContext context) - { - throw new NotImplementedException(); - } } } diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index db1900d03b..66b0be5b92 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -53,7 +53,6 @@ namespace Avalonia.Controls.Repeaters { if (firstRealized != null) { - //MUX_ASSERT(lastRealized); _orientation.SetMajorStart( ref extent, _orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize); diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs index 4d062e23ab..624e477354 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs @@ -217,8 +217,6 @@ namespace Avalonia.Controls.Repeaters if (firstRealized != null) { - ////MUX_ASSERT(lastRealized); - _orientation.SetMajorStart( ref extent, _orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize); diff --git a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs index 4b3922b9c3..3ad883c04b 100644 --- a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs +++ b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs @@ -14,8 +14,6 @@ namespace Avalonia.Controls.Repeaters public void Add(IControl element) { - //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); - var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var key = virtInfo.UniqueId; @@ -29,8 +27,6 @@ namespace Avalonia.Controls.Repeaters public IControl Remove(int index) { - //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); - // Check if there is already a element in the mapping and if so, use it. string key = _owner.ItemsSourceView.KeyFromIndex(index); @@ -44,7 +40,6 @@ namespace Avalonia.Controls.Repeaters public void Clear() { - //MUX_ASSERT(_owner.ItemsSourceView.HasKeyIndexMapping); _elementMap.Clear(); } diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs index 5fccba5412..1167705b5b 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -108,7 +108,7 @@ namespace Avalonia.Controls.Repeaters _owner.ItemTemplateShim.RecycleElement(_owner, element); virtInfo.MoveOwnershipToElementFactory(); - //_phaser.StopPhasing(element, virtInfo); + if (_lastFocusedElement == element) { // Focused element is going away. Remove the tracked last focused element @@ -125,13 +125,9 @@ namespace Avalonia.Controls.Repeaters var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild); if (focusCandidate != null) { - //var focusState = _lastFocusedElement?.FocusState ?? FocusState.Programmatic; - - // If the last focused element has focus, use its focus state, if not use programmatic. - //focusState = focusState == FocusState.Unfocused ? FocusState.Programmatic : focusState; focusCandidate.Focus(); - _lastFocusedElement = focusedChild; + // Add pin to hold the focused element. UpdatePin(focusedChild, true /* addPin */); } @@ -186,12 +182,12 @@ namespace Avalonia.Controls.Repeaters focusCandidate = nextElement as IControl; if (focusCandidate != null) { - ////var firstFocus = FocusManager.FindFirstFocusableElement(nextElement); + var firstFocus = KeyboardNavigationHandler.GetNext(nextElement, NavigationDirection.First); - ////if (firstFocus != null) - ////{ - //// focusCandidate = firstFocus as IControl; - ////} + if (firstFocus != null) + { + focusCandidate = firstFocus as IControl; + } } } @@ -200,12 +196,12 @@ namespace Avalonia.Controls.Repeaters focusCandidate = previousElement as IControl; if (previousElement != null) { - ////var lastFocus = FocusManager.FindLastFocusableElement(previousElement); + var lastFocus = KeyboardNavigationHandler.GetNext(previousElement, NavigationDirection.Last); - ////if (lastFocus != null) - ////{ - //// focusCandidate = lastFocus as IControl; - ////} + if (lastFocus != null) + { + focusCandidate = lastFocus as IControl; + } } } @@ -233,8 +229,6 @@ namespace Avalonia.Controls.Repeaters var elementInfo = _pinnedPool[i]; var virtInfo = elementInfo.VirtualizationInfo; - //MUX_ASSERT(virtInfo.Owner() == ElementOwner.PinnedPool); - if (!virtInfo.IsPinned) { _pinnedPool.RemoveAt(i); @@ -471,16 +465,10 @@ namespace Avalonia.Controls.Repeaters IControl element = null; bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault; - //MUX_ASSERT(!cachedFirstLastIndicesInvalid || m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault); - bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout); if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange) { - // Both First and Last indices need to be valid or default. - //MUX_ASSERT((m_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout == LastRealizedElementIndexDefault) || - // (m_firstRealizedElementIndexHeldByLayout != FirstRealizedElementIndexDefault && m_lastRealizedElementIndexHeldByLayout != LastRealizedElementIndexDefault)); - foreach (var child in _owner.Children) { var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); @@ -572,27 +560,7 @@ namespace Avalonia.Controls.Repeaters } // Prepare the element - // If we are phasing, run phase 0 before setting DataContext. If phase 0 is not - // run before setting DataContext, when setting DataContext all the phases will be - // run in the OnDataContextChanged handler in code generated by the xaml compiler (code-gen). - var extension = false; ////CachedVisualTreeHelpers.GetDataTemplateComponent(element); - if (extension) - { - ////// Clear out old data. - ////extension.Recycle(); - ////int nextPhase = VirtualizationInfo.PhaseReachedEnd; - ////// Run Phase 0 - ////extension.ProcessBindings(data, index, 0 /* currentPhase */, nextPhase); - - ////// Setup phasing information, so that Phaser can pick up any pending phases left. - ////// Update phase on virtInfo. Set data and templateComponent only if x:Phase was used. - ////virtInfo.UpdatePhasingInfo(nextPhase, nextPhase > 0 ? data : null, nextPhase > 0 ? extension : null); - } - else - { - // Set data context only if no x:Bind was used. ie. No data template component on the root. - element.DataContext = data; - } + element.DataContext = data; virtInfo.MoveOwnershipToLayoutFromElementFactory( index, @@ -613,9 +581,7 @@ namespace Avalonia.Controls.Repeaters children.Add(element); } - ////repeater.AnimationManager.OnElementPrepared(element); repeater.OnElementPrepared(element, index); - ////_phaser.PhaseElement(element, virtInfo); // Update realized indices _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); @@ -635,25 +601,6 @@ namespace Avalonia.Controls.Repeaters return _isDataSourceStableResetPending; } - private bool ClearElementToAnimator(IControl element, VirtualizationInfo virtInfo) - { - return false; - ////bool cleared = _owner.AnimationManager.ClearElement(element); - ////if (cleared) - ////{ - //// int clearedIndex = virtInfo.Index; - //// virtInfo.MoveOwnershipToAnimator(); - //// 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); - //// } - ////} - ////return cleared; - } - private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange) { if (_isDataSourceStableResetPending) diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index cf69f9beb6..238870644f 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -62,7 +62,6 @@ namespace Avalonia.Controls.Repeaters { ValidateCacheLength(value); _maximumHorizontalCacheLength = value; - ResetCacheBuffer(); } } } @@ -76,7 +75,6 @@ namespace Avalonia.Controls.Repeaters { ValidateCacheLength(value); _maximumVerticalCacheLength = value; - ResetCacheBuffer(); } } } @@ -182,7 +180,6 @@ namespace Avalonia.Controls.Repeaters _expectedViewportShift = default; _pendingViewportShift = default; _unshiftableShift = default; - ResetCacheBuffer(); _effectiveViewportChangedRevoker?.Dispose(); @@ -250,10 +247,6 @@ namespace Avalonia.Controls.Repeaters _horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide); _verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide); - - // Since we grow the cache buffer at the end of the arrange pass, - // we need to register work even if we just reached cache potential. - RegisterCacheBuildWork(); } } } @@ -294,14 +287,6 @@ namespace Avalonia.Controls.Repeaters { if (!_managingViewportDisabled) { - // We do not animate bring-into-view operations where the anchor is disconnected because - // it doesn't look good (the blank space is obvious because the layout can't keep track - // of two realized ranges while the animation is going on). - if (_isAnchorOutsideRealizedRange) - { - ////args.AnimationDesired(false); - } - // 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 @@ -389,8 +374,6 @@ namespace Avalonia.Controls.Repeaters private void UpdateViewport(Rect viewport) { - //assert(!m_managingViewportDisabled); - var previousVisibleWindow = _visibleWindow; var currentVisibleWindow = viewport; if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && @@ -407,18 +390,6 @@ namespace Avalonia.Controls.Repeaters TryInvalidateMeasure(); } - private void ResetCacheBuffer() - { - _horizontalCacheBufferPerSide = 0.0; - _verticalCacheBufferPerSide = 0.0; - - if (!_managingViewportDisabled) - { - // We need to start building the realization buffer again. - RegisterCacheBuildWork(); - } - } - private static void ValidateCacheLength(double cacheLength) { if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength)) @@ -427,26 +398,6 @@ namespace Avalonia.Controls.Repeaters } } - private void RegisterCacheBuildWork() - { - ////assert(!m_managingViewportDisabled); - if (_owner.Layout != null && - _cacheBuildAction == null) - { - // We capture 'owner' (a strong refernce on ItemsRepeater) to make sure ItemsRepeater is still around - // when the async action completes. By protecting ItemsRepeater, we also ensure that this instance - // of ViewportManager (referenced by 'this' pointer) is valid because the lifetime of ItemsRepeater - // and ViewportManager is the same (see ItemsRepeater::m_viewportManager). - // We can't simply hold a strong reference on ViewportManager because it's not a COM object. - ////auto strongOwner = m_owner->get_strong(); - ////m_cacheBuildAction.set( - //// m_owner->Dispatcher().RunIdleAsync([this, strongOwner](const winrt::IdleDispatchedHandlerArgs&) - ////{ - //// OnCacheBuildActionCompleted(); - ////})); - } - } - private void TryInvalidateMeasure() { // Don't invalidate measure if we have an invalid window. diff --git a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs index 5c6554be81..dddf00efd0 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs @@ -37,7 +37,6 @@ namespace Avalonia.Controls.Repeaters public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId) { - //MUX_ASSERT(_owner == ElementOwner.ElementFactory); Owner = ElementOwner.Layout; Index = index; UniqueId = uniqueId; @@ -45,20 +44,16 @@ namespace Avalonia.Controls.Repeaters public void MoveOwnershipToLayoutFromUniqueIdResetPool() { - //MUX_ASSERT(_owner == ElementOwner.UniqueIdResetPool); Owner = ElementOwner.Layout; } public void MoveOwnershipToLayoutFromPinnedPool() { - //MUX_ASSERT(_owner == ElementOwner.PinnedPool); - //MUX_ASSERT(IsPinned()); Owner = ElementOwner.Layout; } public void MoveOwnershipToElementFactory() { - //MUX_ASSERT(_owner != ElementOwner.ElementFactory); Owner = ElementOwner.ElementFactory; _pinCounter = 0; Index = -1; @@ -68,7 +63,6 @@ namespace Avalonia.Controls.Repeaters public void MoveOwnershipToUniqueIdResetPoolFromLayout() { - //MUX_ASSERT(_owner == ElementOwner.Layout); Owner = ElementOwner.UniqueIdResetPool; // Keep the pinCounter the same. If the container survives the reset // it can go on being pinned as if nothing happened. @@ -79,7 +73,6 @@ namespace Avalonia.Controls.Repeaters // 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. - //MUX_ASSERT(_owner == ElementOwner.Layout || _owner == ElementOwner.UniqueIdResetPool); Owner = ElementOwner.Animator; Index = -1; _pinCounter = 0; @@ -87,7 +80,6 @@ namespace Avalonia.Controls.Repeaters public void MoveOwnershipToPinnedPool() { - //MUX_ASSERT(_owner == ElementOwner.Layout); Owner = ElementOwner.PinnedPool; } From e8a118e11c1fc6cfd031c547af4967199c1f8e76 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 13:58:04 +0200 Subject: [PATCH 08/30] Added headers. --- src/Avalonia.Controls/Repeaters/ElementManager.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs | 7 ++++++- .../Repeaters/IFlowLayoutAlgorithmDelegates.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ItemsRepeater.cs | 7 ++++++- .../Repeaters/ItemsRepeaterElementClearingEventArgs.cs | 7 ++++++- .../Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs | 7 ++++++- .../Repeaters/ItemsRepeaterElementPreparedEventArgs.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ItemsSourceView.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/Layout.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/LayoutContext.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs | 7 ++++++- .../Repeaters/OrientationBasedMeasures.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/RecyclePool.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/StackLayout.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/StackLayoutState.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/UniformGridLayout.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ViewManager.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/ViewportManager.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs | 7 ++++++- src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs | 7 ++++++- 24 files changed, 144 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/Repeaters/ElementManager.cs b/src/Avalonia.Controls/Repeaters/ElementManager.cs index 2ce0ad24fa..871e0527cd 100644 --- a/src/Avalonia.Controls/Repeaters/ElementManager.cs +++ b/src/Avalonia.Controls/Repeaters/ElementManager.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs index f605bf72c8..27eef3f35e 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs @@ -1,4 +1,9 @@ -using System; +// 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.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs index e27f00540c..04d715ea59 100644 --- a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs +++ b/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs index 300fd11d1c..86d452742c 100644 --- a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs @@ -1,4 +1,9 @@ -using System; +// 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.Controls.Templates; diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs index b0e9842d57..f5c832a9e8 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs index 20e62bc3b8..a6984a3e72 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs @@ -1,4 +1,9 @@ -using System; +// 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.Repeaters { diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs index c81c997bd4..a4457a2b06 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -1,4 +1,9 @@ -using System; +// 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.Repeaters { diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs index 3180058fae..12abfa155e 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs index 0d21af0975..f57db0ec07 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/Layout.cs b/src/Avalonia.Controls/Repeaters/Layout.cs index a03e881616..d5f5b215c1 100644 --- a/src/Avalonia.Controls/Repeaters/Layout.cs +++ b/src/Avalonia.Controls/Repeaters/Layout.cs @@ -1,4 +1,9 @@ -using System; +// 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.Repeaters { diff --git a/src/Avalonia.Controls/Repeaters/LayoutContext.cs b/src/Avalonia.Controls/Repeaters/LayoutContext.cs index ce462f6a16..75ee815fc0 100644 --- a/src/Avalonia.Controls/Repeaters/LayoutContext.cs +++ b/src/Avalonia.Controls/Repeaters/LayoutContext.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs index fb8d329d7f..95a66834a0 100644 --- a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs index 2f30f4c551..6f133e6286 100644 --- a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs +++ b/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs @@ -1,4 +1,9 @@ -namespace Avalonia.Controls.Repeaters +// 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.Repeaters { internal class OrientationBasedMeasures { diff --git a/src/Avalonia.Controls/Repeaters/RecyclePool.cs b/src/Avalonia.Controls/Repeaters/RecyclePool.cs index e602a6b674..a1fa05597b 100644 --- a/src/Avalonia.Controls/Repeaters/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeaters/RecyclePool.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs index 3f002c3121..c4fdc0021f 100644 --- a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index 66b0be5b92..2377061d60 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -1,4 +1,9 @@ -using System; +// 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.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs index cefaf38f3a..9ffc5be586 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs index 624e477354..633f1a968d 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs @@ -1,4 +1,9 @@ -using System; +// 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.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs index e62f64192d..0570c1dde2 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; diff --git a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs index 3ad883c04b..f360c4a9f9 100644 --- a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs +++ b/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs index 1167705b5b..3578ef0498 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -1,4 +1,9 @@ -using System; +// 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.Controls.Templates; diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index 238870644f..643ec4c87d 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -1,4 +1,9 @@ -using System; +// 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; diff --git a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs index dddf00efd0..0e33badef3 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs @@ -1,4 +1,9 @@ -using System; +// 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.Repeaters { diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs index fa31095f9e..02700f716c 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs @@ -1,4 +1,9 @@ -using System; +// 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.Text; From f944dde6c4cee21c7b82269bb32ac35cf70fdc3b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 13:41:58 +0200 Subject: [PATCH 09/30] Added IScrollAnchorProvider. Though nothing implements it yet. Uncomment/implement the parts of `ViewportManager` that required this interface. --- .../IScrollAnchorProvider.cs | 9 ++ .../Repeaters/ViewportManager.cs | 123 ++++++++++++++---- 2 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 src/Avalonia.Controls/IScrollAnchorProvider.cs 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/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index 643ec4c87d..7a34d7f12a 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -17,6 +17,8 @@ namespace Avalonia.Controls.Repeaters { 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; @@ -50,11 +52,45 @@ namespace Avalonia.Controls.Repeaters _owner = owner; } - // TODO: Implement - public IControl SuggestedAnchor => null; + 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; + } + } + } - // TODO: Implement - public bool HasScroller => false; + return suggestedAnchor; + } + } + + public bool HasScroller => _scroller != null; public IControl MadeAnchor => _makeAnchorElement; @@ -169,13 +205,10 @@ namespace Avalonia.Controls.Repeaters // 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. - ////if (_scroller != null) - ////{ - //// ((IControl)_scroller).InvalidateArrange(); - ////} + ((IControl)_scroller)?.InvalidateArrange(); } - public Point GetOrigin() => throw new NotImplementedException(); + public Point GetOrigin() => _layoutExtent.TopLeft; public void OnLayoutChanged(bool isVirtualizing) { @@ -190,18 +223,7 @@ namespace Avalonia.Controls.Repeaters if (!_managingViewportDisabled) { - // 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. - _effectiveViewportChangedRevoker = _owner.GetObservable(Visual.TransformedBoundsProperty) - .Merge(_owner.GetObservable(Visual.BoundsProperty).Select(_ => _owner.TransformedBounds)) - .Skip(1) - .Subscribe(OnEffectiveViewportChanged); + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); } } @@ -222,7 +244,7 @@ namespace Avalonia.Controls.Repeaters // 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(); + EnsureScroller(); } public void OnOwnerArranged() @@ -342,9 +364,10 @@ namespace Avalonia.Controls.Repeaters public void ResetScrollers() { - ////_scroller = null; - ////_effectiveViewportChangedRevoker.Dispose(); - ////m_ensuredScroller = false; + _scroller = null; + _effectiveViewportChangedRevoker.Dispose(); + _effectiveViewportChangedRevoker = null; + _ensuredScroller = false; } private void OnEffectiveViewportChanged(TransformedBounds? bounds) @@ -377,6 +400,40 @@ namespace Avalonia.Controls.Repeaters } } + 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; @@ -415,6 +472,22 @@ namespace Avalonia.Controls.Repeaters } } + 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) From 5d813c901190a678a5ef43698726b734f64f11a8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 14:37:53 +0200 Subject: [PATCH 10/30] Scaffold IScrollAnchorProvider on ScrollViewer. It doesn't do anything yet, but `ItemsPresenter` requires `ScrollViewer` to implement this interface. --- .../Repeaters/ViewportManager.cs | 2 +- src/Avalonia.Controls/ScrollViewer.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index 7a34d7f12a..c32ac3855f 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -365,7 +365,7 @@ namespace Avalonia.Controls.Repeaters public void ResetScrollers() { _scroller = null; - _effectiveViewportChangedRevoker.Dispose(); + _effectiveViewportChangedRevoker?.Dispose(); _effectiveViewportChangedRevoker = null; _ensuredScroller = false; } 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); From 3a2876f1ef17c943c5586fba3eb15ea788d42be5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 20:32:17 +0200 Subject: [PATCH 11/30] Added docs for ItemsRepeater-related classes. --- .../Repeaters/ItemTemplateWrapper.cs | 3 - .../Repeaters/ItemsRepeater.cs | 108 ++++++++++++++- .../ItemsRepeaterElementClearingEventArgs.cs | 8 ++ ...emsRepeaterElementIndexChangedEventArgs.cs | 12 ++ .../ItemsRepeaterElementPreparedEventArgs.cs | 13 +- .../Repeaters/ItemsSourceView.cs | 49 ++++++- src/Avalonia.Controls/Repeaters/Layout.cs | 79 ++++++++++- .../Repeaters/LayoutContext.cs | 14 +- .../Repeaters/NonVirtualizingLayout.cs | 8 +- .../Repeaters/StackLayout.cs | 27 +++- .../Repeaters/StackLayoutState.cs | 3 + .../Repeaters/UniformGridLayout.cs | 124 ++++++++++++++++++ .../Repeaters/UniformGridLayoutState.cs | 3 + .../Repeaters/VirtualizingLayout.cs | 68 +++++++++- 14 files changed, 494 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs index 86d452742c..00da39cd9d 100644 --- a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs @@ -3,9 +3,6 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; using Avalonia.Controls.Templates; namespace Avalonia.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs index f5c832a9e8..2f39d62e08 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -11,18 +11,42 @@ using Avalonia.Input; namespace Avalonia.Controls.Repeaters { + /// + /// 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"); @@ -40,6 +64,9 @@ namespace Avalonia.Controls.Repeaters private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + /// + /// Initializes a new instance of the class. + /// public ItemsRepeater() { _viewManager = new ViewManager(this); @@ -53,36 +80,61 @@ namespace Avalonia.Controls.Repeaters 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 Layout 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; } @@ -107,19 +159,69 @@ namespace Avalonia.Controls.Repeaters } } + /// + /// 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); - public void PinElement(IControl element) => _viewManager.UpdatePin(element, true); + internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true); - public void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); + internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); - public IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) { diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs index a6984a3e72..ff011469e7 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs @@ -7,10 +7,18 @@ using System; namespace Avalonia.Controls.Repeaters { + /// + /// 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/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs index a4457a2b06..c30ba38d23 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -7,6 +7,9 @@ using System; namespace Avalonia.Controls.Repeaters { + /// + /// Provides data for the event. + /// public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs { internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) @@ -16,10 +19,19 @@ namespace Avalonia.Controls.Repeaters 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) diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs index 12abfa155e..9c26c0d136 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs @@ -3,12 +3,11 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; - namespace Avalonia.Controls.Repeaters { + /// + /// Provides data for the event. + /// public class ItemsRepeaterElementPreparedEventArgs { internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) @@ -17,8 +16,14 @@ namespace Avalonia.Controls.Repeaters 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) diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs index f57db0ec07..1caffe881a 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs @@ -11,12 +11,26 @@ using System.Linq; namespace Avalonia.Controls.Repeaters { + /// + /// 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); @@ -35,6 +49,9 @@ namespace Avalonia.Controls.Repeaters ListenToCollectionChanges(); } + /// + /// Gets the number of items in the collection. + /// public int Count { get @@ -48,11 +65,20 @@ namespace Avalonia.Controls.Repeaters } } + /// + /// 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) @@ -61,13 +87,34 @@ namespace Avalonia.Controls.Repeaters } } + /// + /// 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(); diff --git a/src/Avalonia.Controls/Repeaters/Layout.cs b/src/Avalonia.Controls/Repeaters/Layout.cs index d5f5b215c1..bf7ce95a23 100644 --- a/src/Avalonia.Controls/Repeaters/Layout.cs +++ b/src/Avalonia.Controls/Repeaters/Layout.cs @@ -7,23 +7,100 @@ using System; namespace Avalonia.Controls.Repeaters { + /// + /// Represents the base class for an object that sizes and arranges child elements for a host. + /// public abstract class Layout : AvaloniaObject { - public string LayoutId { get; set; } + 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 IControl 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 IControl 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 IControl 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.Controls/Repeaters/LayoutContext.cs b/src/Avalonia.Controls/Repeaters/LayoutContext.cs index 75ee815fc0..1a4b26c547 100644 --- a/src/Avalonia.Controls/Repeaters/LayoutContext.cs +++ b/src/Avalonia.Controls/Repeaters/LayoutContext.cs @@ -3,16 +3,22 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; - namespace Avalonia.Controls.Repeaters { + /// + /// 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.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs index 95a66834a0..bac2e10f88 100644 --- a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs @@ -3,12 +3,12 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; - namespace Avalonia.Controls.Repeaters { + /// + /// Represents the base class for an object that sizes and arranges child elements for a host + /// and and does not support virtualization. + /// public abstract class NonVirtualizingLayout : Layout { } diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Controls/Repeaters/StackLayout.cs index 2377061d60..8b887b149e 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayout.cs @@ -8,34 +8,57 @@ using System.Collections.Specialized; namespace Avalonia.Controls.Repeaters { + /// + /// 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 = StackPanel.OrientationProperty.AddOwner(); + /// + /// Defines the property. + /// public static readonly StyledProperty SpacingProperty = StackPanel.SpacingProperty.AddOwner(); 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); } - public Rect GetExtent( + internal Rect GetExtent( Size availableSize, VirtualizingLayoutContext context, IControl firstRealized, @@ -73,7 +96,7 @@ namespace Avalonia.Controls.Repeaters return extent; } - public void OnElementMeasured( + internal void OnElementMeasured( IControl element, int index, Size availableSize, diff --git a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs index 9ffc5be586..130522c908 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs +++ b/src/Avalonia.Controls/Repeaters/StackLayoutState.cs @@ -9,6 +9,9 @@ using System.Linq; namespace Avalonia.Controls.Repeaters { + /// + /// Represents the state of a StackLayout. + /// public class StackLayoutState { private const int BufferSize = 100; diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs index 633f1a968d..4c2327b15b 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs @@ -8,43 +8,111 @@ using System.Collections.Specialized; namespace Avalonia.Controls.Repeaters { + /// + /// 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 = StackPanel.OrientationProperty.AddOwner(); @@ -56,6 +124,9 @@ namespace Avalonia.Controls.Repeaters private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsStretch _itemsStretch; + /// + /// Initializes a new instance of the class. + /// public UniformGridLayout() { LayoutId = "UniformGridLayout"; @@ -66,42 +137,95 @@ namespace Avalonia.Controls.Repeaters 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); diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs index 0570c1dde2..5095ff35a6 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs +++ b/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs @@ -10,6 +10,9 @@ using System.Text; namespace Avalonia.Controls.Repeaters { + /// + /// 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. diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs index 02700f716c..08c72b188e 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs +++ b/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs @@ -3,47 +3,109 @@ // // 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.Text; namespace Avalonia.Controls.Repeaters { + /// + /// Represents the base class for an object that sizes and arranges child elements for a host + /// and supports virtualization. + /// public abstract class VirtualizingLayout : Layout { + /// 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 IControl 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 IControl 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, From 2210b441a2fd63fafb00ad910839dcf2d8648fdc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 23:23:46 +0200 Subject: [PATCH 12/30] Moved attached layout code to Avalonia.Layout. Also involved moving `Orientation` enum. --- .../Pages/ItemsRepeaterPage.xaml.cs | 1 + .../ViewModels/MainWindowViewModel.cs | 1 + src/Avalonia.Controls.DataGrid/DataGrid.cs | 1 + src/Avalonia.Controls/ContextMenu.cs | 1 + src/Avalonia.Controls/GridSplitter.cs | 1 + src/Avalonia.Controls/Menu.cs | 1 + .../Presenters/ItemVirtualizer.cs | 1 + src/Avalonia.Controls/Primitives/ScrollBar.cs | 1 + src/Avalonia.Controls/Primitives/TabStrip.cs | 1 + src/Avalonia.Controls/Primitives/Track.cs | 1 + src/Avalonia.Controls/ProgressBar.cs | 7 +-- .../Repeaters/ItemsRepeater.cs | 23 +++++----- .../Repeaters/RepeaterLayoutContext.cs | 5 ++- .../Repeaters/ViewManager.cs | 1 + .../Repeaters/ViewportManager.cs | 2 +- src/Avalonia.Controls/Slider.cs | 1 + src/Avalonia.Controls/StackPanel.cs | 4 +- src/Avalonia.Controls/WrapPanel.cs | 1 + .../AttachedLayout.cs} | 12 ++--- .../ElementManager.cs | 45 +++++++++---------- .../FlowLayoutAlgorithm.cs | 14 +++--- .../IFlowLayoutAlgorithmDelegates.cs | 12 ++--- .../LayoutContext.cs | 2 +- .../NonVirtualizingLayout.cs | 4 +- .../Orientation.cs | 2 +- .../OrientationBasedMeasures.cs | 40 ++++++++++------- .../StackLayout.cs | 20 ++++----- .../StackLayoutState.cs | 2 +- .../UniformGridLayout.cs | 20 ++++----- .../UniformGridLayoutState.cs | 8 ++-- .../Utils/ListUtils.cs | 2 +- .../VirtualizingLayout.cs | 27 +++++++++-- .../VirtualizingLayoutContext.cs | 14 +++--- .../Data/DefaultValueConverterTests.cs | 1 + .../Primitives/RangeBaseTests.cs | 3 +- .../Primitives/TrackTests.cs | 1 + .../ScrollViewerTests.cs | 3 +- .../SliderTests.cs | 1 + .../WrapPanelTests.cs | 3 +- .../Converters/NullableConverterTests.cs | 1 + .../VisualExtensions_GetVisualsAt.cs | 1 + 41 files changed, 166 insertions(+), 126 deletions(-) rename src/{Avalonia.Controls/Repeaters/Layout.cs => Avalonia.Layout/AttachedLayout.cs} (94%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/ElementManager.cs (89%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/FlowLayoutAlgorithm.cs (99%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/IFlowLayoutAlgorithmDelegates.cs (89%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/LayoutContext.cs (95%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/NonVirtualizingLayout.cs (80%) rename src/{Avalonia.Controls => Avalonia.Layout}/Orientation.cs (94%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/OrientationBasedMeasures.cs (67%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/StackLayout.cs (95%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/StackLayoutState.cs (98%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/UniformGridLayout.cs (96%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/UniformGridLayoutState.cs (97%) rename src/{Avalonia.Controls => Avalonia.Layout}/Utils/ListUtils.cs (95%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/VirtualizingLayout.cs (80%) rename src/{Avalonia.Controls/Repeaters => Avalonia.Layout}/VirtualizingLayoutContext.cs (94%) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index dccb242a78..f32d01ca38 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -5,6 +5,7 @@ using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Repeaters; +using Avalonia.Layout; using Avalonia.Markup.Xaml; namespace ControlCatalog.Pages 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.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 92293a32d6..cf5fa512e8 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +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/Menu.cs b/src/Avalonia.Controls/Menu.cs index b0fb3f2b3b..e65e674eb0 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/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 0e15ae4d7b..346d4340fb 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/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs index 2f39d62e08..d6e03d804f 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs @@ -8,6 +8,7 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Repeaters { @@ -38,8 +39,8 @@ namespace Avalonia.Controls.Repeaters /// /// Defines the property. /// - public static readonly AvaloniaProperty LayoutProperty = - AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + public static readonly AvaloniaProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout), new StackLayout()); /// /// Defines the property. @@ -58,7 +59,6 @@ namespace Avalonia.Controls.Repeaters private IEnumerable _items; private VirtualizingLayoutContext _layoutContext; private NotifyCollectionChangedEventArgs _processingItemsSourceChange; - private Size _lastAvailableSize; private bool _isLayoutInProgress; private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; @@ -87,7 +87,7 @@ namespace Avalonia.Controls.Repeaters /// The layout used to size and position elements. The default is a StackLayout with /// vertical orientation. /// - public Layout Layout + public AttachedLayout Layout { get => GetValue(LayoutProperty); set => SetValue(LayoutProperty, value); @@ -300,7 +300,6 @@ namespace Avalonia.Controls.Repeaters } _viewportManager.SetLayoutExtent(extent); - _lastAvailableSize = availableSize; return desiredSize; } finally @@ -396,7 +395,7 @@ namespace Avalonia.Controls.Repeaters } else if (property == LayoutProperty) { - OnLayoutChanged((Layout)args.OldValue, (Layout)args.NewValue); + OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); } else if (property == HorizontalCacheLengthProperty) { @@ -479,7 +478,7 @@ namespace Avalonia.Controls.Repeaters throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); } - element = GetLayoutContext().GetOrCreateElementAt(index); + element = (IControl)GetLayoutContext().GetOrCreateElementAt(index); element.Measure(Size.Infinity); } @@ -566,7 +565,7 @@ namespace Avalonia.Controls.Repeaters if (Layout is VirtualizingLayout virtualLayout) { var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); - virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args); + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); } else if (Layout is NonVirtualizingLayout nonVirtualLayout) { @@ -605,14 +604,14 @@ namespace Avalonia.Controls.Repeaters try { - virtualLayout.OnItemsChangedCore(GetLayoutContext(), newValue, args); + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); } finally { _processingItemsSourceChange = null; } } - else if (Layout is NonVirtualizingLayout nonVirtualLayout) + else if (Layout is NonVirtualizingLayout) { // Walk through all the elements and make sure they are cleared for // non-virtualizing layouts. @@ -631,7 +630,7 @@ namespace Avalonia.Controls.Repeaters InvalidateMeasure(); } - private void OnLayoutChanged(Layout oldValue, Layout newValue) + private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue) { if (_isLayoutInProgress) { @@ -693,7 +692,7 @@ namespace Avalonia.Controls.Repeaters { if (Layout is VirtualizingLayout virtualLayout) { - virtualLayout.OnItemsChangedCore(GetLayoutContext(), sender, args); + virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args); } else { diff --git a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs index c4fdc0021f..acdecde9d0 100644 --- a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Text; +using Avalonia.Layout; namespace Avalonia.Controls.Repeaters { @@ -47,7 +48,7 @@ namespace Avalonia.Controls.Repeaters protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0; - protected override IControl GetOrCreateElementAtCore(int index, ElementRealizationOptions options) + protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) { return _owner.GetElementImpl( index, @@ -57,7 +58,7 @@ namespace Avalonia.Controls.Repeaters protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); - protected override void RecycleElementCore(IControl element) => _owner.ClearElementImpl(element); + protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); protected override Rect RealizationRectCore() => _owner.RealizationWindow; } diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeaters/ViewManager.cs index 3578ef0498..441946b323 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewManager.cs @@ -9,6 +9,7 @@ using System.Collections.Specialized; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls.Repeaters diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeaters/ViewportManager.cs index c32ac3855f..6696e645eb 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeaters/ViewportManager.cs @@ -234,7 +234,7 @@ namespace Avalonia.Controls.Repeaters ////element.CanBeScrollAnchor(true); } - public void OnElementCleared(IControl element) + public void OnElementCleared(ILayoutable element) { ////element.CanBeScrollAnchor(false); } 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 b391ff061d..d7d49c1870 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 4df1b39400..0dab3e326a 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Utilities; using static System.Math; diff --git a/src/Avalonia.Controls/Repeaters/Layout.cs b/src/Avalonia.Layout/AttachedLayout.cs similarity index 94% rename from src/Avalonia.Controls/Repeaters/Layout.cs rename to src/Avalonia.Layout/AttachedLayout.cs index bf7ce95a23..5622731a7c 100644 --- a/src/Avalonia.Controls/Repeaters/Layout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -5,12 +5,12 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the base class for an object that sizes and arranges child elements for a host. /// - public abstract class Layout : AvaloniaObject + public abstract class AttachedLayout : AvaloniaObject { internal string LayoutId { get; set; } @@ -26,7 +26,7 @@ namespace Avalonia.Controls.Repeaters /// /// Initializes any per-container state the layout requires when it is attached to an - /// container. + /// container. /// /// /// The context object that facilitates communication between the layout and its host @@ -49,7 +49,7 @@ namespace Avalonia.Controls.Repeaters public abstract void InitializeForContext(LayoutContext context); /// - /// Removes any state the layout previously stored on the IControl container. + /// Removes any state the layout previously stored on the ILayoutable container. /// /// /// The context object that facilitates communication between the layout and its host @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Repeaters /// 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 IControl children. + /// for each of the container’s ILayoutable children. /// /// /// The context object that facilitates communication between the layout and its host @@ -91,7 +91,7 @@ namespace Avalonia.Controls.Repeaters public abstract Size Arrange(LayoutContext context, Size finalSize); /// - /// Invalidates the measurement state (layout) for all IControl containers that reference + /// Invalidates the measurement state (layout) for all ILayoutable containers that reference /// this layout. /// protected void InvalidateMeasure() => MeasureInvalidated?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Controls/Repeaters/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs similarity index 89% rename from src/Avalonia.Controls/Repeaters/ElementManager.cs rename to src/Avalonia.Layout/ElementManager.cs index 871e0527cd..0a8816480f 100644 --- a/src/Avalonia.Controls/Repeaters/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -6,14 +6,13 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Text; -using Avalonia.Controls.Utils; +using Avalonia.Layout.Utils; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { internal class ElementManager { - private readonly List _realizedElements = new List(); + private readonly List _realizedElements = new List(); private readonly List _realizedElementLayoutBounds = new List(); private int _firstRealizedDataIndex; private VirtualizingLayoutContext _context; @@ -34,7 +33,7 @@ namespace Avalonia.Controls.Repeaters public void SetContext(VirtualizingLayoutContext virtualContext) => _context = virtualContext; - public void OnBeginMeasure(Orientation orientation) + public void OnBeginMeasure(ScrollOrientation orientation) { if (_context != null) { @@ -69,9 +68,9 @@ namespace Avalonia.Controls.Repeaters return IsVirtualizingContext ? _realizedElements.Count : _context.ItemCount; } - public IControl GetAt(int realizedIndex) + public ILayoutable GetAt(int realizedIndex) { - IControl element; + ILayoutable element; if (IsVirtualizingContext) { @@ -100,7 +99,7 @@ namespace Avalonia.Controls.Repeaters return element; } - public void Add(IControl element, int dataIndex) + public void Add(ILayoutable element, int dataIndex) { if (_realizedElements.Count == 0) { @@ -111,7 +110,7 @@ namespace Avalonia.Controls.Repeaters _realizedElementLayoutBounds.Add(default); } - public void Insert(int realizedIndex, int dataIndex, IControl element) + public void Insert(int realizedIndex, int dataIndex, ILayoutable element) { if (realizedIndex == 0) { @@ -208,7 +207,7 @@ namespace Avalonia.Controls.Repeaters public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount; - public IControl GetRealizedElement(int dataIndex) + public ILayoutable GetRealizedElement(int dataIndex) { return IsVirtualizingContext ? GetAt(GetRealizedRangeIndexFromDataIndex(dataIndex)) : @@ -236,7 +235,7 @@ namespace Avalonia.Controls.Repeaters } } - public bool IsWindowConnected(in Rect window, Orientation orientation, bool scrollOrientationSameAsFlow) + public bool IsWindowConnected(in Rect window, ScrollOrientation orientation, bool scrollOrientationSameAsFlow) { bool intersects = false; @@ -246,14 +245,14 @@ namespace Avalonia.Controls.Repeaters var lastElementBounds = GetLayoutBoundsForRealizedIndex(GetRealizedElementCount() - 1); var effectiveOrientation = scrollOrientationSameAsFlow ? - (orientation == Orientation.Vertical ? Orientation.Horizontal : Orientation.Vertical) : + (orientation == ScrollOrientation.Vertical ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical) : orientation; - var windowStart = effectiveOrientation == Orientation.Vertical ? window.Y : window.X; - var windowEnd = effectiveOrientation == Orientation.Vertical ? window.Y + window.Height : window.X + window.Width; - var firstElementStart = effectiveOrientation == Orientation.Vertical ? firstElementBounds.Y : firstElementBounds.X; - var lastElementEnd = effectiveOrientation == Orientation.Vertical ? lastElementBounds.Y + lastElementBounds.Height : lastElementBounds.X + lastElementBounds.Width; + 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 && @@ -298,7 +297,7 @@ namespace Avalonia.Controls.Repeaters } } - public int GetElementDataIndex(IControl suggestedAnchor) + public int GetElementDataIndex(ILayoutable suggestedAnchor) { var it = _realizedElements.IndexOf(suggestedAnchor); return it != -1 ? GetDataIndexFromRealizedRangeIndex(it) : -1; @@ -314,7 +313,7 @@ namespace Avalonia.Controls.Repeaters return IsVirtualizingContext ? dataIndex - _firstRealizedDataIndex : dataIndex; } - private void DiscardElementsOutsideWindow(in Rect window, Orientation orientation) + 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 @@ -369,12 +368,12 @@ namespace Avalonia.Controls.Repeaters } } - private static bool Intersects(in Rect lhs, in Rect rhs, Orientation orientation) + private static bool Intersects(in Rect lhs, in Rect rhs, ScrollOrientation orientation) { - var lhsStart = orientation == Orientation.Vertical ? lhs.Y : lhs.X; - var lhsEnd = orientation == Orientation.Vertical ? lhs.Y + lhs.Height : lhs.X + lhs.Width; - var rhsStart = orientation == Orientation.Vertical ? rhs.Y : rhs.X; - var rhsEnd = orientation == Orientation.Vertical ? rhs.Y + rhs.Height : rhs.X + rhs.Width; + 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; } diff --git a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs rename to src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 27eef3f35e..0617a96825 100644 --- a/src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Specialized; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { internal class FlowLayoutAlgorithm { @@ -72,7 +72,7 @@ namespace Avalonia.Controls.Repeaters bool isWrapping, double minItemSpacing, double lineSpacing, - Orientation orientation, + ScrollOrientation orientation, string layoutId) { _orientation.ScrollOrientation = orientation; @@ -135,7 +135,7 @@ namespace Avalonia.Controls.Repeaters } public Size MeasureElement( - IControl element, + ILayoutable element, int index, Size availableSize, VirtualizingLayoutContext context) @@ -469,9 +469,9 @@ namespace Avalonia.Controls.Repeaters private Rect EstimateExtent(Size availableSize, string layoutId) { - IControl firstRealizedElement = null; + ILayoutable firstRealizedElement = null; Rect firstBounds = new Rect(); - IControl lastRealizedElement = null; + ILayoutable lastRealizedElement = null; Rect lastBounds = new Rect(); int firstDataIndex = -1; int lastDataIndex = -1; @@ -667,7 +667,7 @@ namespace Avalonia.Controls.Repeaters } } - public IControl GetElementIfRealized(int dataIndex) + public ILayoutable GetElementIfRealized(int dataIndex) { if (_elementManager.IsDataIndexRealized(dataIndex)) { @@ -677,7 +677,7 @@ namespace Avalonia.Controls.Repeaters return null; } - public bool TryAddElement0(IControl element) + public bool TryAddElement0(ILayoutable element) { if (_elementManager.GetRealizedElementCount() == 0) { diff --git a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs similarity index 89% rename from src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs rename to src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs index 04d715ea59..907a3adf0f 100644 --- a/src/Avalonia.Controls/Repeaters/IFlowLayoutAlgorithmDelegates.cs +++ b/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs @@ -3,11 +3,7 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { internal struct FlowLayoutAnchorInfo { @@ -25,14 +21,14 @@ namespace Avalonia.Controls.Repeaters Rect Algorithm_GetExtent( Size availableSize, VirtualizingLayoutContext context, - IControl firstRealized, + ILayoutable firstRealized, int firstRealizedItemIndex, Rect firstRealizedLayoutBounds, - IControl lastRealized, + ILayoutable lastRealized, int lastRealizedItemIndex, Rect lastRealizedLayoutBounds); void Algorithm_OnElementMeasured( - IControl element, + ILayoutable element, int index, Size availableSize, Size measureSize, diff --git a/src/Avalonia.Controls/Repeaters/LayoutContext.cs b/src/Avalonia.Layout/LayoutContext.cs similarity index 95% rename from src/Avalonia.Controls/Repeaters/LayoutContext.cs rename to src/Avalonia.Layout/LayoutContext.cs index 1a4b26c547..45a8048ea2 100644 --- a/src/Avalonia.Controls/Repeaters/LayoutContext.cs +++ b/src/Avalonia.Layout/LayoutContext.cs @@ -3,7 +3,7 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the base class for an object that facilitates communication between an attached diff --git a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs similarity index 80% rename from src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs rename to src/Avalonia.Layout/NonVirtualizingLayout.cs index bac2e10f88..681096ab19 100644 --- a/src/Avalonia.Controls/Repeaters/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -3,13 +3,13 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -namespace Avalonia.Controls.Repeaters +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. /// - public abstract class NonVirtualizingLayout : Layout + public abstract class NonVirtualizingLayout : AttachedLayout { } } 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.Controls/Repeaters/OrientationBasedMeasures.cs b/src/Avalonia.Layout/OrientationBasedMeasures.cs similarity index 67% rename from src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs rename to src/Avalonia.Layout/OrientationBasedMeasures.cs index 6f133e6286..23a8b0e168 100644 --- a/src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs +++ b/src/Avalonia.Layout/OrientationBasedMeasures.cs @@ -3,24 +3,30 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { + internal enum ScrollOrientation + { + Vertical, + Horizontal, + } + internal class OrientationBasedMeasures { - public Orientation ScrollOrientation { get; set; } = Orientation.Vertical; + public ScrollOrientation ScrollOrientation { get; set; } = ScrollOrientation.Vertical; - public double Major(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Height : size.Width; - public double Minor(in Size size) => ScrollOrientation == Orientation.Vertical ? size.Width : size.Height; - public double MajorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Height : rect.Width; - public double MinorSize(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Width : rect.Height; - public double MajorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Y : rect.X; - public double MinorStart(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.X : rect.Y; - public double MajorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Bottom : rect.Right; - public double MinorEnd(in Rect rect) => ScrollOrientation == Orientation.Vertical ? rect.Right : rect.Bottom; + 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 == Orientation.Vertical) + if (ScrollOrientation == ScrollOrientation.Vertical) { rect = rect.WithHeight(value); } @@ -32,7 +38,7 @@ namespace Avalonia.Controls.Repeaters public void SetMinorSize(ref Rect rect, double value) { - if (ScrollOrientation == Orientation.Vertical) + if (ScrollOrientation == ScrollOrientation.Vertical) { rect = rect.WithWidth(value); } @@ -44,7 +50,7 @@ namespace Avalonia.Controls.Repeaters public void SetMajorStart(ref Rect rect, double value) { - if (ScrollOrientation == Orientation.Vertical) + if (ScrollOrientation == ScrollOrientation.Vertical) { rect = rect.WithY(value); } @@ -56,7 +62,7 @@ namespace Avalonia.Controls.Repeaters public void SetMinorStart(ref Rect rect, double value) { - if (ScrollOrientation == Orientation.Vertical) + if (ScrollOrientation == ScrollOrientation.Vertical) { rect = rect.WithX(value); } @@ -68,21 +74,21 @@ namespace Avalonia.Controls.Repeaters public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize) { - return ScrollOrientation == Orientation.Vertical ? + 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 == Orientation.Vertical ? + return ScrollOrientation == ScrollOrientation.Vertical ? new Point(minor, major) : new Point(major, minor); } public Size MinorMajorSize(double minor, double major) { - return ScrollOrientation == Orientation.Vertical ? + return ScrollOrientation == ScrollOrientation.Vertical ? new Size(minor, major) : new Size(major, minor); } diff --git a/src/Avalonia.Controls/Repeaters/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs similarity index 95% rename from src/Avalonia.Controls/Repeaters/StackLayout.cs rename to src/Avalonia.Layout/StackLayout.cs index 8b887b149e..3cc094f091 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Specialized; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically. @@ -17,13 +17,13 @@ namespace Avalonia.Controls.Repeaters /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - StackPanel.OrientationProperty.AddOwner(); + AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); /// /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - StackPanel.SpacingProperty.AddOwner(); + AvaloniaProperty.Register(nameof(Spacing)); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); @@ -61,10 +61,10 @@ namespace Avalonia.Controls.Repeaters internal Rect GetExtent( Size availableSize, VirtualizingLayoutContext context, - IControl firstRealized, + ILayoutable firstRealized, int firstRealizedItemIndex, Rect firstRealizedLayoutBounds, - IControl lastRealized, + ILayoutable lastRealized, int lastRealizedItemIndex, Rect lastRealizedLayoutBounds) { @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Repeaters } internal void OnElementMeasured( - IControl element, + ILayoutable element, int index, Size availableSize, Size measureSize, @@ -164,10 +164,10 @@ namespace Avalonia.Controls.Repeaters Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( Size availableSize, VirtualizingLayoutContext context, - IControl firstRealized, + ILayoutable firstRealized, int firstRealizedItemIndex, Rect firstRealizedLayoutBounds, - IControl lastRealized, + ILayoutable lastRealized, int lastRealizedItemIndex, Rect lastRealizedLayoutBounds) { @@ -182,7 +182,7 @@ namespace Avalonia.Controls.Repeaters lastRealizedLayoutBounds); } - void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) { OnElementMeasured( element, @@ -295,7 +295,7 @@ namespace Avalonia.Controls.Repeaters { //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation. - _orientation.ScrollOrientation = (Orientation)e.NewValue; + _orientation.ScrollOrientation = (ScrollOrientation)e.NewValue; } InvalidateLayout(); diff --git a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs b/src/Avalonia.Layout/StackLayoutState.cs similarity index 98% rename from src/Avalonia.Controls/Repeaters/StackLayoutState.cs rename to src/Avalonia.Layout/StackLayoutState.cs index 130522c908..05ad9bca8e 100644 --- a/src/Avalonia.Controls/Repeaters/StackLayoutState.cs +++ b/src/Avalonia.Layout/StackLayoutState.cs @@ -7,7 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the state of a StackLayout. diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs similarity index 96% rename from src/Avalonia.Controls/Repeaters/UniformGridLayout.cs rename to src/Avalonia.Layout/UniformGridLayout.cs index 4c2327b15b..6d157715a3 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Specialized; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis. @@ -114,7 +114,7 @@ namespace Avalonia.Controls.Repeaters /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - StackPanel.OrientationProperty.AddOwner(); + StackLayout.OrientationProperty.AddOwner(); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); private double _minItemWidth = double.NaN; @@ -316,10 +316,10 @@ namespace Avalonia.Controls.Repeaters Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( Size availableSize, VirtualizingLayoutContext context, - IControl firstRealized, + ILayoutable firstRealized, int firstRealizedItemIndex, Rect firstRealizedLayoutBounds, - IControl lastRealized, + ILayoutable lastRealized, int lastRealizedItemIndex, Rect lastRealizedLayoutBounds) { @@ -359,7 +359,7 @@ namespace Avalonia.Controls.Repeaters return extent; } - void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) { } @@ -440,7 +440,7 @@ namespace Avalonia.Controls.Repeaters //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) ? Orientation.Vertical : Orientation.Horizontal; + var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; _orientation.ScrollOrientation = scrollOrientation; } else if (args.Property == MinColumnSpacingProperty) @@ -475,7 +475,7 @@ namespace Avalonia.Controls.Repeaters { var minItemSpacing = MinItemSpacing; var gridState = (UniformGridLayoutState)context.LayoutState; - return _orientation.ScrollOrientation == Orientation.Vertical? + return _orientation.ScrollOrientation == ScrollOrientation.Vertical? gridState.EffectiveItemWidth + minItemSpacing : gridState.EffectiveItemHeight + minItemSpacing; } @@ -484,7 +484,7 @@ namespace Avalonia.Controls.Repeaters { var lineSpacing = LineSpacing; var gridState = (UniformGridLayoutState)context.LayoutState; - return _orientation.ScrollOrientation == Orientation.Vertical ? + return _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight + lineSpacing : gridState.EffectiveItemWidth + lineSpacing; } @@ -503,8 +503,8 @@ namespace Avalonia.Controls.Repeaters Rect bounds = _orientation.MinorMajorRect( indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent), rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent), - _orientation.ScrollOrientation == Orientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, - _orientation.ScrollOrientation == Orientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth); + _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, + _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth); return bounds; } diff --git a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs similarity index 97% rename from src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs rename to src/Avalonia.Layout/UniformGridLayoutState.cs index 5095ff35a6..37660d0354 100644 --- a/src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -4,11 +4,9 @@ // 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.Text; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the state of a . @@ -20,7 +18,7 @@ namespace Avalonia.Controls.Repeaters // 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 IControl _cachedFirstElement; + private ILayoutable _cachedFirstElement; internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm(); internal double EffectiveItemWidth { get; private set; } @@ -86,7 +84,7 @@ namespace Avalonia.Controls.Repeaters } private void SetSize( - IControl element, + ILayoutable element, double layoutItemWidth, double LayoutItemHeight, Size availableSize, diff --git a/src/Avalonia.Controls/Utils/ListUtils.cs b/src/Avalonia.Layout/Utils/ListUtils.cs similarity index 95% rename from src/Avalonia.Controls/Utils/ListUtils.cs rename to src/Avalonia.Layout/Utils/ListUtils.cs index 8599c53057..eb2480acd3 100644 --- a/src/Avalonia.Controls/Utils/ListUtils.cs +++ b/src/Avalonia.Layout/Utils/ListUtils.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Avalonia.Controls.Utils +namespace Avalonia.Layout.Utils { internal static class ListUtils { diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs similarity index 80% rename from src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs rename to src/Avalonia.Layout/VirtualizingLayout.cs index 08c72b188e..c25277b9ce 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizingLayout.cs +++ b/src/Avalonia.Layout/VirtualizingLayout.cs @@ -5,13 +5,13 @@ using System.Collections.Specialized; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Represents the base class for an object that sizes and arranges child elements for a host /// and supports virtualization. /// - public abstract class VirtualizingLayout : Layout + public abstract class VirtualizingLayout : AttachedLayout { /// public sealed override void InitializeForContext(LayoutContext context) @@ -37,9 +37,28 @@ namespace Avalonia.Controls.Repeaters 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 IControl container. + /// requires when it is attached to an ILayoutable container. /// /// /// The context object that facilitates communication between the layout and its host @@ -51,7 +70,7 @@ namespace Avalonia.Controls.Repeaters /// /// When overridden in a derived class, removes any state the layout previously stored on - /// the IControl container. + /// the ILayoutable container. /// /// /// The context object that facilitates communication between the layout and its host diff --git a/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs b/src/Avalonia.Layout/VirtualizingLayoutContext.cs similarity index 94% rename from src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs rename to src/Avalonia.Layout/VirtualizingLayoutContext.cs index da8d6f0f03..980daec2eb 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizingLayoutContext.cs +++ b/src/Avalonia.Layout/VirtualizingLayoutContext.cs @@ -5,7 +5,7 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Layout { /// /// Defines constants that specify whether to suppress automatic recycling of the retrieved @@ -114,7 +114,7 @@ namespace Avalonia.Controls.Repeaters /// This method calls /// with options set to None. GetElementAtCore must be implemented in a derived class. /// - public IControl GetOrCreateElementAt(int index) + public ILayoutable GetOrCreateElementAt(int index) => GetOrCreateElementAtCore(index, ElementRealizationOptions.None); /// @@ -138,7 +138,7 @@ namespace Avalonia.Controls.Repeaters /// advanced layouts that choose to explicitly manage the realization and recycling of /// elements as a performance optimization. /// - public IControl GetOrCreateElementAt(int index, ElementRealizationOptions options) + public ILayoutable GetOrCreateElementAt(int index, ElementRealizationOptions options) => GetOrCreateElementAtCore(index, options); /// @@ -146,10 +146,10 @@ namespace Avalonia.Controls.Repeaters /// /// The element to clear. /// - /// This method calls , which must be implemented + /// This method calls , which must be implemented /// in a derived class. /// - public void RecycleElement(IControl element) => RecycleElementCore(element); + public void RecycleElement(ILayoutable element) => RecycleElementCore(element); /// /// When implemented in a derived class, retrieves the number of items in the data. @@ -178,13 +178,13 @@ namespace Avalonia.Controls.Repeaters /// A value of that specifies whether to suppress /// automatic recycling of the retrieved element or force creation of a new element. /// - protected abstract IControl GetOrCreateElementAtCore(int index, ElementRealizationOptions options); + 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(IControl element); + protected abstract void RecycleElementCore(ILayoutable element); } } 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 42578c61ac..bbeba2fe2e 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 Xunit; @@ -199,4 +200,4 @@ namespace Avalonia.Controls.UnitTests.Primitives } } } -} \ No newline at end of file +} 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 d1385176c5..cd27436241 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 @@ -113,4 +114,4 @@ namespace Avalonia.Controls.UnitTests }; } } -} \ No newline at end of file +} 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; From f6cba5ab698f36ffd6e6c88044415f735c0fcecf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 23:45:39 +0200 Subject: [PATCH 13/30] Implemented NonVirtualizingLayout. --- src/Avalonia.Layout/NonVirtualizingLayout.cs | 82 ++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index 681096ab19..ad4dc302b0 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -11,5 +11,87 @@ namespace Avalonia.Layout /// 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; } } From 9297d46e3adadbaeb22ef436e0196851f1be9fa7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 00:15:08 +0200 Subject: [PATCH 14/30] More docs. --- src/Avalonia.Layout/NonVirtualizingLayout.cs | 6 ++++++ src/Avalonia.Layout/VirtualizingLayout.cs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index ad4dc302b0..fba91e66c7 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -9,6 +9,12 @@ 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 { /// diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs index c25277b9ce..4c601175f3 100644 --- a/src/Avalonia.Layout/VirtualizingLayout.cs +++ b/src/Avalonia.Layout/VirtualizingLayout.cs @@ -11,6 +11,12 @@ 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 { /// From 997fd1aa30ddf7bba3e6b94546c529e2dd554035 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 09:27:29 +0200 Subject: [PATCH 15/30] Moved ItemsRepeater to Avalonia.Controls. Keep it in a subdirectory though. --- samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs | 4 ---- src/Avalonia.Controls/Properties/AssemblyInfo.cs | 1 - .../{Repeaters => Repeater}/ItemTemplateWrapper.cs | 2 +- .../{Repeaters => Repeater}/ItemsRepeater.cs | 2 +- .../ItemsRepeaterElementClearingEventArgs.cs | 2 +- .../ItemsRepeaterElementIndexChangedEventArgs.cs | 2 +- .../ItemsRepeaterElementPreparedEventArgs.cs | 2 +- .../{Repeaters => Repeater}/ItemsSourceView.cs | 2 +- src/Avalonia.Controls/{Repeaters => Repeater}/RecyclePool.cs | 2 +- .../{Repeaters => Repeater}/RepeaterLayoutContext.cs | 2 +- .../{Repeaters => Repeater}/UniqueIdElementPool.cs | 2 +- src/Avalonia.Controls/{Repeaters => Repeater}/ViewManager.cs | 2 +- .../{Repeaters => Repeater}/ViewportManager.cs | 2 +- .../{Repeaters => Repeater}/VirtualizationInfo.cs | 2 +- 14 files changed, 12 insertions(+), 17 deletions(-) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemTemplateWrapper.cs (97%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsRepeater.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsRepeaterElementClearingEventArgs.cs (95%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsRepeaterElementIndexChangedEventArgs.cs (97%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsRepeaterElementPreparedEventArgs.cs (96%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ItemsSourceView.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/RecyclePool.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/RepeaterLayoutContext.cs (98%) rename src/Avalonia.Controls/{Repeaters => Repeater}/UniqueIdElementPool.cs (97%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ViewManager.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/ViewportManager.cs (99%) rename src/Avalonia.Controls/{Repeaters => Repeater}/VirtualizationInfo.cs (99%) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index f32d01ca38..214de89253 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections; -using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Repeaters; using Avalonia.Layout; using Avalonia.Markup.Xaml; diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 8fa80e55d2..c04c66a77f 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -13,7 +13,6 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Repeaters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")] diff --git a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs similarity index 97% rename from src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs rename to src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs index 00da39cd9d..04d859c742 100644 --- a/src/Avalonia.Controls/Repeaters/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs @@ -5,7 +5,7 @@ using Avalonia.Controls.Templates; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class ItemTemplateWrapper { diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/ItemsRepeater.cs rename to src/Avalonia.Controls/Repeater/ItemsRepeater.cs index d6e03d804f..44783e2c97 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -10,7 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Represents a data-driven collection control that incorporates a flexible layout system, diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs similarity index 95% rename from src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs rename to src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs index ff011469e7..75d50e52a6 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementClearingEventArgs.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs @@ -5,7 +5,7 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Provides data for the event. diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs similarity index 97% rename from src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs rename to src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs index c30ba38d23..7ca68140b2 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementIndexChangedEventArgs.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -5,7 +5,7 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Provides data for the event. diff --git a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs similarity index 96% rename from src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs rename to src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs index 9c26c0d136..5a30dbcf2a 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsRepeaterElementPreparedEventArgs.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs @@ -3,7 +3,7 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Provides data for the event. diff --git a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/ItemsSourceView.cs rename to src/Avalonia.Controls/Repeater/ItemsSourceView.cs index 1caffe881a..732ba8501c 100644 --- a/src/Avalonia.Controls/Repeaters/ItemsSourceView.cs +++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { /// /// Represents a standardized view of the supported interactions between a given ItemsSource diff --git a/src/Avalonia.Controls/Repeaters/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/RecyclePool.cs rename to src/Avalonia.Controls/Repeater/RecyclePool.cs index a1fa05597b..4e5950bdc5 100644 --- a/src/Avalonia.Controls/Repeaters/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls.Templates; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class RecyclePool { diff --git a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs similarity index 98% rename from src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs rename to src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs index acdecde9d0..977d9d794c 100644 --- a/src/Avalonia.Controls/Repeaters/RepeaterLayoutContext.cs +++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; using System.Text; using Avalonia.Layout; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class RepeaterLayoutContext : VirtualizingLayoutContext { diff --git a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs similarity index 97% rename from src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs rename to src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs index f360c4a9f9..775aa3f113 100644 --- a/src/Avalonia.Controls/Repeaters/UniqueIdElementPool.cs +++ b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs @@ -8,7 +8,7 @@ using System.Collections; using System.Collections.Generic; using System.Text; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class UniqueIdElementPool : IEnumerable> { diff --git a/src/Avalonia.Controls/Repeaters/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/ViewManager.cs rename to src/Avalonia.Controls/Repeater/ViewManager.cs index 441946b323..b90f8dacfe 100644 --- a/src/Avalonia.Controls/Repeaters/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -12,7 +12,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.VisualTree; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal sealed class ViewManager { diff --git a/src/Avalonia.Controls/Repeaters/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/ViewportManager.cs rename to src/Avalonia.Controls/Repeater/ViewportManager.cs index 6696e645eb..6f0e792aa5 100644 --- a/src/Avalonia.Controls/Repeaters/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -11,7 +11,7 @@ using Avalonia.Layout; using Avalonia.Threading; using Avalonia.VisualTree; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal class ViewportManager { diff --git a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs similarity index 99% rename from src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs rename to src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index 0e33badef3..eb30c1b7cf 100644 --- a/src/Avalonia.Controls/Repeaters/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -5,7 +5,7 @@ using System; -namespace Avalonia.Controls.Repeaters +namespace Avalonia.Controls { internal enum ElementOwner { From ac660567f737f990f00e0c757850f6f08e0839b0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 11:20:09 +0200 Subject: [PATCH 16/30] Fix horizontal StackLayout. --- src/Avalonia.Layout/StackLayout.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 3cc094f091..e89d178b0e 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -293,9 +293,11 @@ namespace Avalonia.Layout { 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 = (ScrollOrientation)e.NewValue; + _orientation.ScrollOrientation = orientation == Orientation.Horizontal ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical; } InvalidateLayout(); From 66e01468983b073d012d01f047abe4aae34823d5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 13:05:02 +0200 Subject: [PATCH 17/30] Set default cache size to 0. Scrolling on UWP happens asynchronous to the UI thread, so `ItemsRepeater` on that platform generate an additional buffer of prepared elements sufficient to fill an area larger than the viewport. We don't have async scroll rendering on Avalonia so make the default cache size 0. --- src/Avalonia.Controls/Repeater/ViewportManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index 6f0e792aa5..10c11889d0 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -35,8 +35,8 @@ namespace Avalonia.Controls // 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 = 2.0; - private double _maximumVerticalCacheLength = 2.0; + private double _maximumHorizontalCacheLength = 0.0; + private double _maximumVerticalCacheLength = 0.0; private double _horizontalCacheBufferPerSide; private double _verticalCacheBufferPerSide; private bool _isBringIntoViewInProgress; From cd04e768f0939b1f187ba650800788d592fbb85b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 13:45:21 +0200 Subject: [PATCH 18/30] Avoid reset of containers when anchor element is replaced in ItemsRepeater. * avoid reset of containers when anchor element is replaced * fix element 0 ownership issue Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/e45e83d010502911aa1c7e14ea5bf681ceffb048 --- src/Avalonia.Layout/ElementManager.cs | 34 +++++++++++++++++-- src/Avalonia.Layout/UniformGridLayout.cs | 4 +++ src/Avalonia.Layout/UniformGridLayoutState.cs | 7 ++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index 0a8816480f..1748a3be03 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -276,8 +276,38 @@ namespace Avalonia.Layout case NotifyCollectionChangedAction.Replace: { - OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count); - OnItemsAdded(args.NewStartingIndex, args.NewItems.Count); + 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; diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 6d157715a3..edc2042922 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -409,6 +409,10 @@ namespace Avalonia.Layout _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); } diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index 37660d0354..e30c1a3c4b 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -134,10 +134,13 @@ namespace Avalonia.Layout } } - internal void EnsureFirstElementOwnership() + internal void EnsureFirstElementOwnership(VirtualizingLayoutContext context) { - if (FlowAlgorithm.GetElementIfRealized(0) != null) + 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; } } From 6f0befabdbf76fd97d936494fdf8f6e50cfe947e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 3 Jul 2019 18:35:13 +0200 Subject: [PATCH 19/30] Prevent IndexOutOfRange exception. Prevent the exception described in https://github.com/AvaloniaUI/Avalonia/pull/2603#issuecomment-508011071. This makes our codebase diverge from WinUI's but Im unable to debug into the WinUI code consistently enough to work out why this problem doesn't show up there. --- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 25 +++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 0617a96825..615ce725bd 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -281,6 +281,7 @@ namespace Avalonia.Layout var lineOffset = _orientation.MajorStart(anchorBounds); var lineMajorSize = _orientation.MajorSize(anchorBounds); int countInLine = 1; + int count = 0; bool lineNeedsReposition = false; while (_elementManager.IsIndexValidInData(currentIndex) && @@ -290,6 +291,7 @@ namespace Avalonia.Layout _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); @@ -387,17 +389,20 @@ namespace Avalonia.Layout // If we did not reach the top or bottom of the extent, we realized one // extra item before we knew we were outside the realization window. Do not // account for that element in the indicies inside the realization window. - if (direction == GenerateDirection.Forward) + if (count > 0) { - 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); + 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); From b26ec443749c8186c2d1b3aeed3f760280f56dd7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 4 Jul 2019 12:04:11 +0200 Subject: [PATCH 20/30] Removed non-working code. And replace it with a TODO. --- src/Avalonia.Controls/Repeater/ViewManager.cs | 33 ++----------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index b90f8dacfe..833e708e9e 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; @@ -180,38 +181,10 @@ namespace Avalonia.Controls } } - // Find the next element if one exists, if not use the previous element. + // 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. - IControl focusCandidate = null; - if (nextElement != null) - { - focusCandidate = nextElement as IControl; - if (focusCandidate != null) - { - var firstFocus = KeyboardNavigationHandler.GetNext(nextElement, NavigationDirection.First); - - if (firstFocus != null) - { - focusCandidate = firstFocus as IControl; - } - } - } - - if (focusCandidate == null && previousElement != null) - { - focusCandidate = previousElement as IControl; - if (previousElement != null) - { - var lastFocus = KeyboardNavigationHandler.GetNext(previousElement, NavigationDirection.Last); - - if (lastFocus != null) - { - focusCandidate = lastFocus as IControl; - } - } - } - return focusCandidate; + return nextElement; } public int GetElementIndex(VirtualizationInfo virtInfo) From 689781f24e481083318c65bb3110ae465e093b14 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 20 Jul 2019 16:59:13 +0200 Subject: [PATCH 21/30] Make ViewModelBase and ViewLocator internal. --- samples/ControlCatalog/ViewModels/MainWindowViewModel.cs | 3 +-- src/Avalonia.Diagnostics/ViewLocator.cs | 2 +- src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs | 7 +++++-- .../Views/{TreePage.xaml.cs => TreePageView.xaml.cs} | 0 4 files changed, 7 insertions(+), 5 deletions(-) rename src/Avalonia.Diagnostics/Views/{TreePage.xaml.cs => TreePageView.xaml.cs} (100%) 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/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/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/Views/TreePage.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs similarity index 100% rename from src/Avalonia.Diagnostics/Views/TreePage.xaml.cs rename to src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs From 4f83c4e8c79bae43648d0508f6fd4ba924d28626 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 20 Jul 2019 17:45:32 +0200 Subject: [PATCH 22/30] Reformat Avalonia.Diagnostics. --- .editorconfig | 3 +- src/Avalonia.Diagnostics/DevTools.xaml | 36 +++---- src/Avalonia.Diagnostics/DevTools.xaml.cs | 33 +++--- .../Models/EventChainLink.cs | 7 +- .../ViewModels/ControlDetailsViewModel.cs | 12 +-- .../ViewModels/DevToolsViewModel.cs | 1 - .../ViewModels/EventOwnerTreeNode.cs | 15 ++- .../ViewModels/EventTreeNode.cs | 9 +- .../ViewModels/EventTreeNodeBase.cs | 17 +-- .../ViewModels/FiredEvent.cs | 12 +-- .../ViewModels/LogicalTreeNode.cs | 3 +- .../ViewModels/PropertyDetails.cs | 16 +-- .../ViewModels/TreeNode.cs | 15 ++- .../ViewModels/TreePageViewModel.cs | 22 ++-- .../ViewModels/VisualTreeNode.cs | 5 +- .../Views/ControlDetailsView.cs | 17 ++- .../Views/EventsView.xaml | 100 +++++++++--------- .../Views/EventsView.xaml.cs | 8 +- .../Views/GridRepeater.cs | 3 +- .../Views/PropertyChangedExtensions.cs | 10 +- src/Avalonia.Diagnostics/Views/SimpleGrid.cs | 8 +- .../Views/TreePageView.xaml | 12 +-- .../Views/TreePageView.xaml.cs | 4 +- 23 files changed, 173 insertions(+), 195 deletions(-) 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/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/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/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"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -