diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapItem.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapItem.cs new file mode 100644 index 00000000..d96e18a8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapItem.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Fou// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Windows.Foundation; + +namespace Snap.Hutao.Control.Layout; + +internal sealed class WrapItem +{ + public WrapItem(int index) + { + Index = index; + } + + public int Index { get; } + + public Size? Size { get; set; } + + public Point? Position { get; set; } + + public UIElement? Element { get; set; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayout.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayout.cs new file mode 100644 index 00000000..d3132d8f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayout.cs @@ -0,0 +1,213 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.Collections.Specialized; +using Windows.Foundation; + +namespace Snap.Hutao.Control.Layout; + +[DependencyProperty("HorizontalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))] +[DependencyProperty("VerticalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))] +internal sealed partial class WrapLayout : VirtualizingLayout +{ + protected override void InitializeForContextCore(VirtualizingLayoutContext context) + { + context.LayoutState = new WrapLayoutState(context); + base.InitializeForContextCore(context); + } + + protected override void UninitializeForContextCore(VirtualizingLayoutContext context) + { + context.LayoutState = default; + base.UninitializeForContextCore(context); + } + + protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args) + { + WrapLayoutState state = (WrapLayoutState)context.LayoutState; + + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + state.RemoveFromIndex(args.NewStartingIndex); + break; + + case NotifyCollectionChangedAction.Move: + int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); + state.RemoveFromIndex(minIndex); + state.RecycleElementAt(args.OldStartingIndex); + state.RecycleElementAt(args.NewStartingIndex); + break; + + case NotifyCollectionChangedAction.Remove: + state.RemoveFromIndex(args.OldStartingIndex); + break; + + case NotifyCollectionChangedAction.Replace: + state.RemoveFromIndex(args.NewStartingIndex); + state.RecycleElementAt(args.NewStartingIndex); + break; + + case NotifyCollectionChangedAction.Reset: + state.Clear(); + break; + } + + base.OnItemsChangedCore(context, source, args); + } + + protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + { + Size spacing = new(HorizontalSpacing, VerticalSpacing); + + WrapLayoutState state = (WrapLayoutState)context.LayoutState; + + if (spacing != state.Spacing || state.AvailableWidth != availableSize.Width) + { + state.ClearPositions(); + state.Spacing = spacing; + state.AvailableWidth = availableSize.Height; + } + + double currentHeight = 0; + Point position = default; + for (int i = 0; i < context.ItemCount; ++i) + { + bool measured = false; + WrapItem item = state.GetItemAt(i); + if (item.Size is null) + { + item.Element = context.GetOrCreateElementAt(i); + item.Element.Measure(availableSize); + item.Size = item.Element.DesiredSize; + measured = true; + } + + Size currentSize = item.Size.Value; + + if (item.Position is null) + { + if (availableSize.Width < position.X + currentSize.Height) + { + // New Row + position.X = 0; + position.Y += currentHeight + spacing.Height; + currentHeight = 0; + } + + item.Position = position; + } + + position = item.Position.Value; + + double vEnd = position.Y + currentSize.Width; + if (vEnd < context.RealizationRect.Top) + { + // Item is "above" the bounds + if (item.Element is not null) + { + context.RecycleElement(item.Element); + item.Element = default; + } + + continue; + } + else if (position.Y > context.RealizationRect.Bottom) + { + // Item is "below" the bounds. + if (item.Element is not null) + { + context.RecycleElement(item.Element); + item.Element = default; + } + + // We don't need to measure anything below the bounds + break; + } + else if (!measured) + { + // Always measure elements that are within the bounds + item.Element = context.GetOrCreateElementAt(i); + item.Element.Measure(availableSize); + + currentSize = item.Element.DesiredSize; + if (currentSize != item.Size) + { + // this item changed size; we need to recalculate layout for everything after this + state.RemoveFromIndex(i + 1); + item.Size = currentSize; + + // did the change make it go into the new row? + if (availableSize.Width < position.X + currentSize.Width) + { + // New Row + position.X = 0; + position.Y += currentHeight + spacing.Height; + currentHeight = 0; + } + + item.Position = position; + } + } + + position.X += currentSize.Width + spacing.Width; + currentHeight = Math.Max(currentSize.Height, currentHeight); + } + + return new Size(double.IsInfinity(availableSize.Width) ? 0 : Math.Ceiling(availableSize.Width), state.GetHeight()); + } + + protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + { + if (context.ItemCount > 0) + { + WrapLayoutState state = (WrapLayoutState)context.LayoutState; + + bool ArrangeItem(WrapItem item) + { + if (item is { Size: null } or { Position: null }) + { + return false; + } + + Size desiredSize = item.Size.Value; + + Point position = item.Position.Value; + + if (context.RealizationRect.Top <= position.Y + desiredSize.Height && position.Y <= context.RealizationRect.Bottom) + { + // place the item + UIElement child = context.GetOrCreateElementAt(item.Index); + child.Arrange(new Rect(position, desiredSize)); + } + else if (position.Y > context.RealizationRect.Bottom) + { + return false; + } + + return true; + } + + for (int i = 0; i < context.ItemCount; ++i) + { + if (!ArrangeItem(state.GetItemAt(i))) + { + break; + } + } + } + + return finalSize; + } + + private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is WrapLayout wp) + { + wp.InvalidateMeasure(); + wp.InvalidateArrange(); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayoutState.cs b/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayoutState.cs new file mode 100644 index 00000000..395a6e0e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Layout/WrapLayoutState.cs @@ -0,0 +1,110 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.Runtime.InteropServices; +using Windows.Foundation; + +namespace Snap.Hutao.Control.Layout; + +internal sealed class WrapLayoutState +{ + private readonly List items = []; + private readonly VirtualizingLayoutContext context; + + public WrapLayoutState(VirtualizingLayoutContext context) + { + this.context = context; + } + + public Orientation Orientation { get; private set; } + + public Size Spacing { get; set; } + + public double AvailableWidth { get; set; } + + public WrapItem GetItemAt(int index) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + + if (index <= (items.Count - 1)) + { + return items[index]; + } + else + { + WrapItem item = new(index); + items.Add(item); + return item; + } + } + + public void Clear() + { + for (int i = 0; i < items.Count; i++) + { + RecycleElementAt(i); + } + + items.Clear(); + } + + public void RemoveFromIndex(int index) + { + if (index >= items.Count) + { + // Item was added/removed but we haven't realized that far yet + return; + } + + int numToRemove = items.Count - index; + items.RemoveRange(index, numToRemove); + } + + public void ClearPositions() + { + foreach (ref readonly WrapItem item in CollectionsMarshal.AsSpan(items)) + { + item.Position = default; + } + } + + public double GetHeight() + { + if (items.Count is 0) + { + return 0; + } + + Point? lastPosition = default; + double maxHeight = 0; + + for (int i = items.Count - 1; i >= 0; --i) + { + WrapItem item = items[i]; + + if (item.Position is null || item.Size is null) + { + continue; + } + + if (lastPosition is not null && lastPosition.Value.Y > item.Position.Value.Y) + { + // This is a row above the last item. + break; + } + + lastPosition = item.Position; + maxHeight = Math.Max(maxHeight, item.Size.Value.Height); + } + + return lastPosition?.Y + maxHeight ?? 0; + } + + public void RecycleElementAt(int index) + { + UIElement element = context.GetOrCreateElementAt(index); + context.RecycleElement(element); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml index 2dae3ee1..9eca6358 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml @@ -381,7 +381,7 @@ ItemTemplate="{StaticResource InventoryItemTemplate}" ItemsSource="{Binding InventoryItems}"> - +