/* Based on VirtualizingWrapPanel created by S. Bäumlisberger licensed under MIT license. https://github.com/sbaeumlisberger/VirtualizingWrapPanel Copyright (C) S. Bäumlisberger All Rights Reserved. */ using System.Windows.Controls; using System.Windows.Controls.Primitives; using Point = System.Windows.Point; using Size = System.Windows.Size; // ReSharper disable once CheckNamespace namespace WPFluent.Controls; /// /// Extended base class for . Based on . /// public class VirtualizingWrapPanel : VirtualizingPanelBase { /// /// Identifies the dependency property. /// public static readonly DependencyProperty ItemSizeProperty = DependencyProperty.Register( nameof(ItemSize), typeof(Size), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(Size.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( nameof(Orientation), typeof(Orientation), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata( Orientation.Vertical, FrameworkPropertyMetadataOptions.AffectsMeasure, OnOrientationChanged)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty SpacingModeProperty = DependencyProperty.Register( nameof(SpacingMode), typeof(SpacingMode), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(SpacingMode.Uniform, FrameworkPropertyMetadataOptions.AffectsMeasure)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty StretchItemsProperty = DependencyProperty.Register( nameof(StretchItems), typeof(bool), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsArrange)); /// /// Calculates child size. /// private Size CalculateChildSize(Size _) { // REVIEW: this method comes with side effects. code smell if(Items.Count == 0) { return new Size(0, 0); } var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(0); using var at = ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true); var child = (UIElement)ItemContainerGenerator.GenerateNext(); AddInternalChild(child); ItemContainerGenerator.PrepareItemContainer(child); child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); return child.DesiredSize; } /// /// Gets item row index. /// private int GetRowIndex(double location) { var calculatedRowIndex = (int)Math.Floor(location / GetHeight(ChildSize)); var maxRowIndex = (int)Math.Ceiling(Items.Count / (double)ItemsPerRowCount); return Math.Max(Math.Min(calculatedRowIndex, maxRowIndex), 0); } /// /// Private callback for . /// private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if(d is not VirtualizingWrapPanel panel) { return; } panel.OnOrientationChanged(); } /// /// Gets the style property value for item containers within the . /// /// The expected type of the property value. /// The to retrieve the value for. /// The value to return if the property is not set. /// The value of the specified property if found; otherwise, the . private T ReadItemContainerStyle(DependencyProperty property, T fallbackValue) where T : notnull { var value = ItemsControl .ItemContainerStyle?.Setters.OfType().FirstOrDefault(setter => setter.Property == property) ?.Value; return (T)(value ?? fallbackValue); } /// /// Updates child size of . /// private void UpdateChildSize(Size availableSize) { if(ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem && GetIsVirtualizingWhenGrouping(ItemsControl)) { if(Orientation == Orientation.Vertical) { availableSize.Width = groupItem.Constraints.Viewport.Size.Width; availableSize.Width = Math.Max(availableSize.Width - (Margin.Left + Margin.Right), 0); } else { availableSize.Height = groupItem.Constraints.Viewport.Size.Height; availableSize.Height = Math.Max(availableSize.Height - (Margin.Top + Margin.Bottom), 0); } } if(ItemSize != Size.Empty) { ChildSize = ItemSize; } else if(InternalChildren.Count != 0) { ChildSize = InternalChildren[0].DesiredSize; } else { ChildSize = CalculateChildSize(availableSize); } ItemsPerRowCount = double.IsInfinity(GetWidth(availableSize)) ? Items.Count : Math.Max(1, (int)Math.Floor(GetWidth(availableSize) / GetWidth(ChildSize))); RowCount = (int)Math.Ceiling((double)Items.Count / ItemsPerRowCount); } /// protected override Size ArrangeOverride(Size finalSize) { var offsetX = GetX(Offset); var offsetY = GetY(Offset); /* When the items owner is a group item offset is handled by the parent panel. */ if(ItemsOwner is IHierarchicalVirtualizationAndScrollInfo) { offsetY = 0; } var childSize = CalculateChildArrangeSize(finalSize); CalculateSpacing(finalSize, out var innerSpacing, out var outerSpacing); for(var childIndex = 0; childIndex < InternalChildren.Count; childIndex++) { var child = InternalChildren[childIndex]; var itemIndex = GetItemIndexFromChildIndex(childIndex); var columnIndex = itemIndex % ItemsPerRowCount; var rowIndex = itemIndex / ItemsPerRowCount; var x = outerSpacing + columnIndex * (GetWidth(childSize) + innerSpacing); var y = rowIndex * GetHeight(childSize); if(GetHeight(finalSize) == 0.0) { /* When the parent panel is grouping and a cached group item is not * in the viewport it has no valid arrangement. That means that the * height/width is 0. Therefore the items should not be visible so * that they are not falsely displayed. */ child.Arrange(new Rect(0, 0, 0, 0)); } else { child.Arrange(CreateRect(x - offsetX, y - offsetY, childSize.Width, childSize.Height)); } } return finalSize; } /// protected override void BringIndexIntoView(int index) { if(index < 0 || index >= Items.Count) { throw new ArgumentOutOfRangeException( nameof(index), $"The argument {nameof(index)} must be >= 0 and < the number of items."); } if(ItemsPerRowCount == 0) { throw new InvalidOperationException(); } var offset = index / ItemsPerRowCount * GetHeight(ChildSize); if(Orientation == Orientation.Horizontal) { SetHorizontalOffset(offset); } else { SetVerticalOffset(offset); } } /// /// Calculates desired child arrange size. /// protected Size CalculateChildArrangeSize(Size finalSize) { if(!StretchItems) { return ChildSize; } if(Orientation == Orientation.Vertical) { var childMaxWidth = ReadItemContainerStyle(MaxWidthProperty, double.PositiveInfinity); var maxPossibleChildWith = finalSize.Width / ItemsPerRowCount; var childWidth = Math.Min(maxPossibleChildWith, childMaxWidth); return new Size(childWidth, ChildSize.Height); } var childMaxHeight = ReadItemContainerStyle(MaxHeightProperty, double.PositiveInfinity); var maxPossibleChildHeight = finalSize.Height / ItemsPerRowCount; var childHeight = Math.Min(maxPossibleChildHeight, childMaxHeight); return new Size(ChildSize.Width, childHeight); } /// protected override Size CalculateExtent(Size availableSize) { var extentWidth = SpacingMode != SpacingMode.None && !double.IsInfinity(GetWidth(availableSize)) ? GetWidth(availableSize) : GetWidth(ChildSize) * ItemsPerRowCount; if(ItemsOwner is IHierarchicalVirtualizationAndScrollInfo) { extentWidth = Orientation == Orientation.Vertical ? Math.Max(extentWidth - (Margin.Left + Margin.Right), 0) : Math.Max(extentWidth - (Margin.Top + Margin.Bottom), 0); } var extentHeight = GetHeight(ChildSize) * RowCount; return CreateSize(extentWidth, extentHeight); } /// /// Calculates desired spacing between items. /// protected void CalculateSpacing(Size finalSize, out double innerSpacing, out double outerSpacing) { var childSize = CalculateChildArrangeSize(finalSize); var finalWidth = GetWidth(finalSize); var totalItemsWidth = Math.Min(GetWidth(childSize) * ItemsPerRowCount, finalWidth); var unusedWidth = finalWidth - totalItemsWidth; var spacingMode = SpacingMode; switch(spacingMode) { case SpacingMode.Uniform: innerSpacing = outerSpacing = unusedWidth / (ItemsPerRowCount + 1); break; case SpacingMode.BetweenItemsOnly: innerSpacing = unusedWidth / Math.Max(ItemsPerRowCount - 1, 1); outerSpacing = 0; break; case SpacingMode.StartAndEndOnly: innerSpacing = 0; outerSpacing = unusedWidth / 2; break; case SpacingMode.None: default: innerSpacing = 0; outerSpacing = 0; break; } } /// /// Defines panel coordinates and size. /// protected Rect CreateRect(double x, double y, double width, double height) => Orientation == Orientation.Vertical ? new Rect(x, y, width, height) : new Rect(y, x, width, height); /// /// Defines panel size. /// protected Size CreateSize(double width, double height) => Orientation == Orientation.Vertical ? new Size(width, height) : new Size(height, width); /// /// Gets panel height. /// protected double GetHeight(Size size) => Orientation == Orientation.Vertical ? size.Height : size.Width; /// protected override double GetLineDownScrollAmount() => Math.Min( ChildSize.Height * ScrollLineDeltaItem, Viewport.Height); /// protected override double GetLineLeftScrollAmount() => -Math.Min( ChildSize.Width * ScrollLineDeltaItem, Viewport.Width); /// protected override double GetLineRightScrollAmount() => Math.Min( ChildSize.Width * ScrollLineDeltaItem, Viewport.Width); /// protected override double GetLineUpScrollAmount() => -Math.Min( ChildSize.Height * ScrollLineDeltaItem, Viewport.Height); /// protected override double GetMouseWheelDownScrollAmount() => Math.Min( ChildSize.Height * MouseWheelDeltaItem, Viewport.Height); /// protected override double GetMouseWheelLeftScrollAmount() => -Math.Min( ChildSize.Width * MouseWheelDeltaItem, Viewport.Width); /// protected override double GetMouseWheelRightScrollAmount() => Math.Min( ChildSize.Width * MouseWheelDeltaItem, Viewport.Width); /// protected override double GetMouseWheelUpScrollAmount() => -Math.Min( ChildSize.Height * MouseWheelDeltaItem, Viewport.Height); /// protected override double GetPageDownScrollAmount() => Viewport.Height; /// protected override double GetPageLeftScrollAmount() => -Viewport.Width; /// protected override double GetPageRightScrollAmount() => Viewport.Width; /// protected override double GetPageUpScrollAmount() => -Viewport.Height; /// /// Gets panel width. /// protected double GetWidth(Size size) => Orientation == Orientation.Vertical ? size.Width : size.Height; /* orientation aware helper methods */ /// /// Gets X panel orientation. /// protected double GetX(Point point) => Orientation == Orientation.Vertical ? point.X : point.Y; /// /// Gets Y panel orientation. /// protected double GetY(Point point) => Orientation == Orientation.Vertical ? point.Y : point.X; /// protected override Size MeasureOverride(Size availableSize) { UpdateChildSize(availableSize); return base.MeasureOverride(availableSize); } /// /// This virtual method is called when is changed. /// protected virtual void OnOrientationChanged() { MouseWheelScrollDirection = Orientation == Orientation.Vertical ? ScrollDirection.Vertical : ScrollDirection.Horizontal; } /// protected override ItemRange UpdateItemRange() { if(!IsVirtualizing) { return new ItemRange(0, Items.Count - 1); } int startIndex; int endIndex; if(ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem) { if(!GetIsVirtualizingWhenGrouping(ItemsControl)) { return new ItemRange(0, Items.Count - 1); } var offset = new Point(Offset.X, groupItem.Constraints.Viewport.Location.Y); int offsetRowIndex; double offsetInPixel; int rowCountInViewport; if(ScrollUnit == ScrollUnit.Item) { offsetRowIndex = GetY(offset) >= 1 ? (int)GetY(offset) - 1 : 0; // ignore header offsetInPixel = offsetRowIndex * GetHeight(ChildSize); } else { offsetInPixel = Math.Min( Math.Max(GetY(offset) - GetHeight(groupItem.HeaderDesiredSizes.PixelSize), 0), GetHeight(Extent)); offsetRowIndex = GetRowIndex(offsetInPixel); } var viewportHeight = Math.Min(GetHeight(Viewport), Math.Max(GetHeight(Extent) - offsetInPixel, 0)); rowCountInViewport = (int)Math.Ceiling((offsetInPixel + viewportHeight) / GetHeight(ChildSize)) - (int)Math.Floor(offsetInPixel / GetHeight(ChildSize)); startIndex = offsetRowIndex * ItemsPerRowCount; endIndex = Math.Min((offsetRowIndex + rowCountInViewport) * ItemsPerRowCount - 1, Items.Count - 1); if(CacheLengthUnit == VirtualizationCacheLengthUnit.Pixel) { var cacheBeforeInPixel = Math.Min(CacheLength.CacheBeforeViewport, offsetInPixel); var cacheAfterInPixel = Math.Min( CacheLength.CacheAfterViewport, GetHeight(Extent) - viewportHeight - offsetInPixel); var rowCountInCacheBefore = (int)(cacheBeforeInPixel / GetHeight(ChildSize)); var rowCountInCacheAfter = (int) Math.Ceiling((offsetInPixel + viewportHeight + cacheAfterInPixel) / GetHeight(ChildSize)) - (int)Math.Ceiling((offsetInPixel + viewportHeight) / GetHeight(ChildSize)); startIndex = Math.Max(startIndex - rowCountInCacheBefore * ItemsPerRowCount, 0); endIndex = Math.Min(endIndex + rowCountInCacheAfter * ItemsPerRowCount, Items.Count - 1); } else if(CacheLengthUnit == VirtualizationCacheLengthUnit.Item) { startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport, 0); endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport, Items.Count - 1); } } else { var viewportSartPos = GetY(Offset); var viewportEndPos = GetY(Offset) + GetHeight(Viewport); if(CacheLengthUnit == VirtualizationCacheLengthUnit.Pixel) { viewportSartPos = Math.Max(viewportSartPos - CacheLength.CacheBeforeViewport, 0); viewportEndPos = Math.Min(viewportEndPos + CacheLength.CacheAfterViewport, GetHeight(Extent)); } var startRowIndex = GetRowIndex(viewportSartPos); startIndex = startRowIndex * ItemsPerRowCount; var endRowIndex = GetRowIndex(viewportEndPos); endIndex = Math.Min(endRowIndex * ItemsPerRowCount + (ItemsPerRowCount - 1), Items.Count - 1); if(CacheLengthUnit == VirtualizationCacheLengthUnit.Page) { var itemsPerPage = endIndex - startIndex + 1; startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport * itemsPerPage, 0); endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport * itemsPerPage, Items.Count - 1); } else if(CacheLengthUnit == VirtualizationCacheLengthUnit.Item) { startIndex = Math.Max(startIndex - (int)CacheLength.CacheBeforeViewport, 0); endIndex = Math.Min(endIndex + (int)CacheLength.CacheAfterViewport, Items.Count - 1); } } return new ItemRange(startIndex, endIndex); } /// /// Gets or sets the size of the single child element. /// protected Size ChildSize { get; set; } /// /// Gets or sets the amount of displayed items per row. /// protected int ItemsPerRowCount { get; set; } /// /// Gets or sets the amount of the displayed rows. /// protected int RowCount { get; set; } /// /// Gets or sets a value that specifies the size of the items. The default value is . If the /// value is the size of the items gots measured by the first realized item. /// public Size ItemSize { get => (Size)GetValue(ItemSizeProperty); set => SetValue(ItemSizeProperty, value); } /// /// Gets or sets a value that specifies the orientation in which items are arranged. The default value is . /// public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } /// /// Gets or sets the spacing mode used when arranging the items. The default value is . /// public SpacingMode SpacingMode { get => (SpacingMode)GetValue(SpacingModeProperty); set => SetValue(SpacingModeProperty, value); } /// /// Gets or sets a value indicating whether the items get stretched to fill up remaining space. The default value is /// false. /// /// /// The MaxWidth and MaxHeight properties of the ItemContainerStyle can be used to limit the stretching. In this /// case the use of the remaining space will be determined by the SpacingMode property. /// public bool StretchItems { get => (bool)GetValue(StretchItemsProperty); set => SetValue(StretchItemsProperty, value); } }