diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
index 1dc21a42558..cff34175f46 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
+++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj
@@ -115,7 +115,7 @@
6.1.0-build.6
- 2.2.190917002
+ 2.3.200213001
0.7.0-alpha
@@ -273,6 +273,7 @@
+
@@ -367,6 +368,7 @@
+
@@ -531,10 +533,16 @@
ImageCropperPage.xaml
+
+ StaggeredLayoutPage.xaml
+
TokenizingTextBoxPage.xaml
+
+ WrapLayoutPage.xaml
+
@@ -545,6 +553,8 @@
+
+
@@ -959,6 +969,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
MSBuild:Compile
Designer
@@ -1335,6 +1349,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
MSBuild:Compile
Designer
@@ -1408,6 +1426,10 @@
{daeb9cec-c817-33b2-74b2-bc379380db72}
Microsoft.Toolkit.Uwp.UI.Controls.DataGrid
+
+ {cb444381-18ba-4a51-bb32-3a498bcc1e99}
+ Microsoft.Toolkit.Uwp.UI.Controls.Layout
+
{e9faabfb-d726-42c1-83c1-cb46a29fea81}
Microsoft.Toolkit.Uwp.UI.Controls
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayout.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayout.bind
new file mode 100644
index 00000000000..089b5918bb1
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayout.bind
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayout.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayout.png
new file mode 100644
index 00000000000..08ab2b77ce7
Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayout.png differ
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayoutPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayoutPage.xaml
new file mode 100644
index 00000000000..2ab42395d48
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayoutPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayoutPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayoutPage.xaml.cs
new file mode 100644
index 00000000000..7b62a3b8e2a
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/StaggeredLayout/StaggeredLayoutPage.xaml.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.ObjectModel;
+using Microsoft.Toolkit.Uwp.UI.Extensions;
+using Microsoft.UI.Xaml.Controls;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class StaggeredLayoutPage : Page, IXamlRenderListener
+ {
+ private ObservableCollection- _items = new ObservableCollection
- ();
+ private Random _random;
+
+ public StaggeredLayoutPage()
+ {
+ this.InitializeComponent();
+
+ _random = new Random(DateTime.Now.Millisecond);
+ for (int i = 0; i < _random.Next(1000, 5000); i++)
+ {
+ var item = new Item { Index = i, Width = _random.Next(50, 250), Height = _random.Next(50, 250), Color = Color.FromArgb(255, (byte)_random.Next(0, 255), (byte)_random.Next(0, 255), (byte)_random.Next(0, 255)) };
+ _items.Add(item);
+ }
+ }
+
+ public void OnXamlRendered(FrameworkElement control)
+ {
+ var repeater = control.FindChildByName("StaggeredRepeater") as ItemsRepeater;
+
+ if (repeater != null)
+ {
+ repeater.ItemsSource = _items;
+ }
+ }
+
+ private class Item
+ {
+ public int Index { get; internal set; }
+
+ public int Width { get; internal set; }
+
+ public int Height { get; internal set; }
+
+ public Color Color { get; internal set; }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayout.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayout.bind
new file mode 100644
index 00000000000..18549c16f82
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayout.bind
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayout.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayout.png
new file mode 100644
index 00000000000..5e7ae5ab2d1
Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayout.png differ
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayoutPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayoutPage.xaml
new file mode 100644
index 00000000000..fcfebb4223b
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayoutPage.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayoutPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayoutPage.xaml.cs
new file mode 100644
index 00000000000..97b27f8d55b
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WrapLayout/WrapLayoutPage.xaml.cs
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.ObjectModel;
+using Microsoft.Toolkit.Uwp.UI.Extensions;
+using Microsoft.UI.Xaml.Controls;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages
+{
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class WrapLayoutPage : Page, IXamlRenderListener
+ {
+ private ObservableCollection
- _items = new ObservableCollection
- ();
+ private Random _random;
+
+ public WrapLayoutPage()
+ {
+ this.InitializeComponent();
+
+ _random = new Random(DateTime.Now.Millisecond);
+ for (int i = 0; i < _random.Next(1000, 5000); i++)
+ {
+ var item = new Item { Index = i, Width = _random.Next(50, 250), Height = _random.Next(50, 250), Color = Color.FromArgb(255, (byte)_random.Next(0, 255), (byte)_random.Next(0, 255), (byte)_random.Next(0, 255)) };
+ _items.Add(item);
+ }
+ }
+
+ public void OnXamlRendered(FrameworkElement control)
+ {
+ var repeater = control.FindChildByName("WrapRepeater") as ItemsRepeater;
+
+ if (repeater != null)
+ {
+ repeater.ItemsSource = _items;
+ }
+ }
+
+ private class Item
+ {
+ public int Index { get; internal set; }
+
+ public int Width { get; internal set; }
+
+ public int Height { get; internal set; }
+
+ public Color Color { get; internal set; }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
index e40eb165412..d440632a6d2 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json
@@ -229,6 +229,16 @@
"Icon": "/SamplePages/WrapPanel/WrapPanel.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/WrapPanel.md"
},
+ {
+ "Name": "WrapLayout",
+ "Type": "WrapLayoutPage",
+ "Subcategory": "Layout - ItemsRepeater",
+ "About": "The WrapLayout virtualizes child elements in sequential position from left to right, breaking content to the next line at the edge of the containing box.",
+ "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout",
+ "XamlCodeFile": "WrapLayout.bind",
+ "Icon": "/SamplePages/WrapLayout/WrapLayout.png",
+ "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/layout/WrapLayout.md"
+ },
{
"Name": "OrbitView",
"Type": "OrbitViewPage",
@@ -302,6 +312,16 @@
"Icon": "/SamplePages/StaggeredPanel/StaggeredPanel.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/StaggeredPanel.md"
},
+ {
+ "Name": "StaggeredLayout",
+ "Type": "StaggeredLayoutPage",
+ "Subcategory": "Layout - ItemsRepeater",
+ "About": "The StaggeredLayout virtualizes items in a column approach where an item will be added to whichever column has used the least amount of space.",
+ "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout",
+ "XamlCodeFile": "StaggeredLayout.bind",
+ "Icon": "/SamplePages/StaggeredLayout/StaggeredLayout.png",
+ "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/layout/StaggeredLayout.md"
+ },
{
"Name": "LayoutTransformControl",
"Type": "LayoutTransformControlPage",
@@ -424,7 +444,7 @@
"XamlCodeFile": "FocusTrackerXaml.bind",
"Icon": "/SamplePages/FocusTracker/FocusTracker.png",
"DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/developer-tools/FocusTracker.md"
- }//,
+ } //,
//{
// "Name": "TokenizingTextBox",
// "Type": "TokenizingTextBoxPage",
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Microsoft.Toolkit.Uwp.UI.Controls.Layout.csproj b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Microsoft.Toolkit.Uwp.UI.Controls.Layout.csproj
new file mode 100644
index 00000000000..b1f67ff8750
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Microsoft.Toolkit.Uwp.UI.Controls.Layout.csproj
@@ -0,0 +1,31 @@
+
+
+
+ uap10.0.17134
+ 10.0.18362.0
+ Windows Community Toolkit Layout
+
+ This library provides XAML layout controls. It is part of the Windows Community Toolkit.
+
+ Microsoft.Toolkit.Uwp.UI.Controls
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Microsoft.Toolkit.Uwp.UI.Controls.Layout.csproj.DotSettings b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Microsoft.Toolkit.Uwp.UI.Controls.Layout.csproj.DotSettings
new file mode 100644
index 00000000000..65848b5a192
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Microsoft.Toolkit.Uwp.UI.Controls.Layout.csproj.DotSettings
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Properties/AssemblyInfo.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000000..64a4e43ebc0
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Properties/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Resources;
+using System.Runtime.CompilerServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: InternalsVisibleTo("UnitTests")]
+[assembly: NeutralResourcesLanguage("en-US")]
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Properties/Microsoft.Windows.Toolkit.UI.Controls.Layout.rd.xml b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Properties/Microsoft.Windows.Toolkit.UI.Controls.Layout.rd.xml
new file mode 100644
index 00000000000..342912dbe1c
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/Properties/Microsoft.Windows.Toolkit.UI.Controls.Layout.rd.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredColumnLayout.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredColumnLayout.cs
new file mode 100644
index 00000000000..5d3ce1f9b58
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredColumnLayout.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, Height = {Height}")]
+ internal class StaggeredColumnLayout : List
+ {
+ public double Height { get; private set; }
+
+ public new void Add(StaggeredItem item)
+ {
+ Height = item.Top + item.Height;
+ base.Add(item);
+ }
+
+ public new void Clear()
+ {
+ Height = 0;
+ base.Clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredItem.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredItem.cs
new file mode 100644
index 00000000000..c1bcac34815
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredItem.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Foundation;
+using Windows.UI.Xaml;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ internal class StaggeredItem
+ {
+ public StaggeredItem(int index)
+ {
+ this.Index = index;
+ }
+
+ public double Top { get; internal set; }
+
+ public double Height { get; internal set; }
+
+ public int Index { get; }
+
+ public UIElement Element { get; internal set; }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredLayout.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredLayout.cs
new file mode 100644
index 00000000000..350c14be778
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredLayout.cs
@@ -0,0 +1,328 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Foundation;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// Arranges child elements into a staggered grid pattern where items are added to the column that has used least amount of space.
+ ///
+ public class StaggeredLayout : VirtualizingLayout
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StaggeredLayout()
+ {
+ }
+
+ ///
+ /// Gets or sets the desired width for each column.
+ ///
+ ///
+ /// The width of columns can exceed the DesiredColumnWidth if the HorizontalAlignment is set to Stretch.
+ ///
+ public double DesiredColumnWidth
+ {
+ get { return (double)GetValue(DesiredColumnWidthProperty); }
+ set { SetValue(DesiredColumnWidthProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ /// The identifier for the dependency property.
+ public static readonly DependencyProperty DesiredColumnWidthProperty = DependencyProperty.Register(
+ nameof(DesiredColumnWidth),
+ typeof(double),
+ typeof(StaggeredLayout),
+ new PropertyMetadata(250d, OnDesiredColumnWidthChanged));
+
+ ///
+ /// Gets or sets the spacing between columns of items.
+ ///
+ public double ColumnSpacing
+ {
+ get { return (double)GetValue(ColumnSpacingProperty); }
+ set { SetValue(ColumnSpacingProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ColumnSpacingProperty = DependencyProperty.Register(
+ nameof(ColumnSpacing),
+ typeof(double),
+ typeof(StaggeredLayout),
+ new PropertyMetadata(0d, OnSpacingChanged));
+
+ ///
+ /// Gets or sets the spacing between rows of items.
+ ///
+ public double RowSpacing
+ {
+ get { return (double)GetValue(RowSpacingProperty); }
+ set { SetValue(RowSpacingProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty RowSpacingProperty = DependencyProperty.Register(
+ nameof(RowSpacing),
+ typeof(double),
+ typeof(StaggeredLayout),
+ new PropertyMetadata(0d, OnSpacingChanged));
+
+ ///
+ protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ context.LayoutState = new StaggeredLayoutState(context);
+ base.InitializeForContextCore(context);
+ }
+
+ ///
+ protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ context.LayoutState = null;
+ base.UninitializeForContextCore(context);
+ }
+
+ ///
+ protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
+ {
+ var state = (StaggeredLayoutState)context.LayoutState;
+
+ switch (args.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ state.RemoveFromIndex(args.NewStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Replace:
+ state.RemoveFromIndex(args.NewStartingIndex);
+
+ // We must recycle the element to ensure that it gets the correct context
+ state.RecycleElementAt(args.NewStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Move:
+ int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
+ int maxIndex = Math.Max(args.NewStartingIndex, args.OldStartingIndex);
+ state.RemoveRange(minIndex, maxIndex);
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ state.RemoveFromIndex(args.OldStartingIndex);
+ break;
+ case NotifyCollectionChangedAction.Reset:
+ state.Clear();
+ break;
+ }
+
+ base.OnItemsChangedCore(context, source, args);
+ }
+
+ ///
+ protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
+ {
+ if (context.ItemCount == 0)
+ {
+ return new Size(availableSize.Width, 0);
+ }
+
+ if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
+ {
+ return new Size(availableSize.Width, 0.0);
+ }
+
+ var state = (StaggeredLayoutState)context.LayoutState;
+
+ double availableWidth = availableSize.Width;
+ double availableHeight = availableSize.Height;
+
+ double columnWidth = Math.Min(DesiredColumnWidth, availableWidth);
+ if (columnWidth != state.ColumnWidth)
+ {
+ // The items will need to be remeasured
+ state.Clear();
+ }
+
+ state.ColumnWidth = Math.Min(DesiredColumnWidth, availableWidth);
+ int numColumns = Math.Max(1, (int)Math.Floor(availableWidth / state.ColumnWidth));
+
+ // adjust for column spacing on all columns expect the first
+ double totalWidth = state.ColumnWidth + ((numColumns - 1) * (state.ColumnWidth + ColumnSpacing));
+ if (totalWidth > availableWidth)
+ {
+ numColumns--;
+ }
+ else if (double.IsInfinity(availableWidth))
+ {
+ availableWidth = totalWidth;
+ }
+
+ if (numColumns != state.NumberOfColumns)
+ {
+ // The items will not need to be remeasured, but they will need to go into new columns
+ state.ClearColumns();
+ }
+
+ if (RowSpacing != state.RowSpacing)
+ {
+ // If the RowSpacing changes the height of the rows will be different.
+ // The columns stores the height so we'll want to clear them out to
+ // get the proper height
+ state.ClearColumns();
+ state.RowSpacing = RowSpacing;
+ }
+
+ var columnHeights = new double[numColumns];
+ var itemsPerColumn = new int[numColumns];
+ var deadColumns = new HashSet();
+
+ for (int i = 0; i < context.ItemCount; i++)
+ {
+ var columnIndex = GetColumnIndex(columnHeights);
+
+ bool measured = false;
+ StaggeredItem item = state.GetItemAt(i);
+ if (item.Height == 0)
+ {
+ // Item has not been measured yet. Get the element and store the values
+ item.Element = context.GetOrCreateElementAt(i);
+ item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
+ item.Height = item.Element.DesiredSize.Height;
+ measured = true;
+ }
+
+ double spacing = itemsPerColumn[columnIndex] > 0 ? RowSpacing : 0;
+ item.Top = columnHeights[columnIndex] + spacing;
+ double bottom = item.Top + item.Height;
+ columnHeights[columnIndex] = bottom;
+ itemsPerColumn[columnIndex]++;
+ state.AddItemToColumn(item, columnIndex);
+
+ if (bottom < context.RealizationRect.Top)
+ {
+ // The bottom of the element is above the realization area
+ if (item.Element != null)
+ {
+ context.RecycleElement(item.Element);
+ item.Element = null;
+ }
+ }
+ else if (item.Top > context.RealizationRect.Bottom)
+ {
+ // The top of the element is below the realization area
+ if (item.Element != null)
+ {
+ context.RecycleElement(item.Element);
+ item.Element = null;
+ }
+
+ deadColumns.Add(columnIndex);
+ }
+ else if (measured == false)
+ {
+ // We ALWAYS want to measure an item that will be in the bounds
+ item.Element = context.GetOrCreateElementAt(i);
+ item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
+ if (item.Height != item.Element.DesiredSize.Height)
+ {
+ // this item changed size; we need to recalculate layout for everything after this
+ state.RemoveFromIndex(i + 1);
+ item.Height = item.Element.DesiredSize.Height;
+ columnHeights[columnIndex] = item.Top + item.Height;
+ }
+ }
+
+ if (deadColumns.Count == numColumns)
+ {
+ break;
+ }
+ }
+
+ double desiredHeight = state.GetHeight();
+
+ return new Size(availableWidth, desiredHeight);
+ }
+
+ ///
+ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ {
+ if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
+ {
+ return finalSize;
+ }
+
+ var state = (StaggeredLayoutState)context.LayoutState;
+
+ // Cycle through each column and arrange the items that are within the realization bounds
+ for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
+ {
+ StaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
+ for (int i = 0; i < layout.Count; i++)
+ {
+ StaggeredItem item = layout[i];
+
+ double bottom = item.Top + item.Height;
+ if (bottom < context.RealizationRect.Top)
+ {
+ // element is above the realization bounds
+ continue;
+ }
+
+ if (item.Top <= context.RealizationRect.Bottom)
+ {
+ double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (ColumnSpacing * columnIndex);
+
+ Rect bounds = new Rect(itemHorizontalOffset, item.Top, state.ColumnWidth, item.Height);
+ UIElement element = context.GetOrCreateElementAt(item.Index);
+ element.Arrange(bounds);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ return finalSize;
+ }
+
+ private static void OnDesiredColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var panel = (StaggeredLayout)d;
+ panel.InvalidateMeasure();
+ }
+
+ private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var panel = (StaggeredLayout)d;
+ panel.InvalidateMeasure();
+ }
+
+ private static int GetColumnIndex(double[] columnHeights)
+ {
+ int columnIndex = 0;
+ double height = columnHeights[0];
+ for (int j = 1; j < columnHeights.Length; j++)
+ {
+ if (columnHeights[j] < height)
+ {
+ columnIndex = j;
+ height = columnHeights[j];
+ }
+ }
+
+ return columnIndex;
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredLayoutState.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredLayoutState.cs
new file mode 100644
index 00000000000..15ce0c0d0ce
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/StaggeredLayout/StaggeredLayoutState.cs
@@ -0,0 +1,197 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.UI.Xaml.Controls;
+using Windows.UI.Xaml;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ internal class StaggeredLayoutState
+ {
+ private List _items = new List();
+ private VirtualizingLayoutContext _context;
+ private Dictionary _columnLayout = new Dictionary();
+ private double _lastAverageHeight;
+
+ public StaggeredLayoutState(VirtualizingLayoutContext context)
+ {
+ _context = context;
+ }
+
+ public double ColumnWidth { get; internal set; }
+
+ public int NumberOfColumns
+ {
+ get
+ {
+ return _columnLayout.Count;
+ }
+ }
+
+ public double RowSpacing { get; internal set; }
+
+ internal void AddItemToColumn(StaggeredItem item, int columnIndex)
+ {
+ if (_columnLayout.TryGetValue(columnIndex, out StaggeredColumnLayout columnLayout) == false)
+ {
+ columnLayout = new StaggeredColumnLayout();
+ _columnLayout[columnIndex] = columnLayout;
+ }
+
+ if (columnLayout.Contains(item) == false)
+ {
+ columnLayout.Add(item);
+ }
+ }
+
+ internal StaggeredItem GetItemAt(int index)
+ {
+ if (index < 0)
+ {
+ throw new IndexOutOfRangeException();
+ }
+
+ if (index <= (_items.Count - 1))
+ {
+ return _items[index];
+ }
+ else
+ {
+ StaggeredItem item = new StaggeredItem(index);
+ _items.Add(item);
+ return item;
+ }
+ }
+
+ internal StaggeredColumnLayout GetColumnLayout(int columnIndex)
+ {
+ _columnLayout.TryGetValue(columnIndex, out StaggeredColumnLayout columnLayout);
+ return columnLayout;
+ }
+
+ ///
+ /// Clear everything that has been calculated.
+ ///
+ internal void Clear()
+ {
+ _columnLayout.Clear();
+ _items.Clear();
+ }
+
+ ///
+ /// Clear the layout columns so they will be recalculated.
+ ///
+ internal void ClearColumns()
+ {
+ _columnLayout.Clear();
+ }
+
+ ///
+ /// Gets the estimated height of the layout.
+ ///
+ /// The estimated height of the layout.
+ ///
+ /// If all of the items have been calculated then the actual height will be returned.
+ /// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
+ ///
+ internal double GetHeight()
+ {
+ double desiredHeight = Enumerable.Max(_columnLayout.Values, c => c.Height);
+
+ var itemCount = Enumerable.Sum(_columnLayout.Values, c => c.Count);
+ if (itemCount == _context.ItemCount)
+ {
+ return desiredHeight;
+ }
+
+ double averageHeight = 0;
+ foreach (var kvp in _columnLayout)
+ {
+ averageHeight += kvp.Value.Height / kvp.Value.Count;
+ }
+
+ averageHeight /= _columnLayout.Count;
+ double estimatedHeight = (averageHeight * _context.ItemCount) / _columnLayout.Count;
+ if (estimatedHeight > desiredHeight)
+ {
+ desiredHeight = estimatedHeight;
+ }
+
+ if (Math.Abs(desiredHeight - _lastAverageHeight) < 5)
+ {
+ return _lastAverageHeight;
+ }
+
+ _lastAverageHeight = desiredHeight;
+ return desiredHeight;
+ }
+
+ internal void RecycleElementAt(int index)
+ {
+ UIElement element = _context.GetOrCreateElementAt(index);
+ _context.RecycleElement(element);
+ }
+
+ internal 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);
+
+ foreach (var kvp in _columnLayout)
+ {
+ StaggeredColumnLayout layout = kvp.Value;
+ for (int i = 0; i < layout.Count; i++)
+ {
+ if (layout[i].Index >= index)
+ {
+ numToRemove = layout.Count - i;
+ layout.RemoveRange(i, numToRemove);
+ break;
+ }
+ }
+ }
+ }
+
+ internal void RemoveRange(int startIndex, int endIndex)
+ {
+ for (int i = startIndex; i <= endIndex; i++)
+ {
+ if (i > _items.Count)
+ {
+ break;
+ }
+
+ StaggeredItem item = _items[i];
+ item.Height = 0;
+ item.Top = 0;
+
+ // We must recycle all elements to ensure that it gets the correct context
+ RecycleElementAt(i);
+ }
+
+ foreach (var kvp in _columnLayout)
+ {
+ StaggeredColumnLayout layout = kvp.Value;
+ for (int i = 0; i < layout.Count; i++)
+ {
+ if ((startIndex <= layout[i].Index) && (layout[i].Index <= endIndex))
+ {
+ int numToRemove = layout.Count - i;
+ layout.RemoveRange(i, numToRemove);
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/VisualStudioToolsManifest.xml b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/VisualStudioToolsManifest.xml
new file mode 100644
index 00000000000..8d2ad7fe8bf
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/VisualStudioToolsManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/UvBounds.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/UvBounds.cs
new file mode 100644
index 00000000000..616275c4d99
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/UvBounds.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.Foundation;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ internal struct UvBounds
+ {
+ public UvBounds(Orientation orientation, Rect rect)
+ {
+ if (orientation == Orientation.Horizontal)
+ {
+ UMin = rect.Left;
+ UMax = rect.Right;
+ VMin = rect.Top;
+ VMax = rect.Bottom;
+ }
+ else
+ {
+ UMin = rect.Top;
+ UMax = rect.Bottom;
+ VMin = rect.Left;
+ VMax = rect.Right;
+ }
+ }
+
+ public double UMin { get; }
+
+ public double UMax { get; }
+
+ public double VMin { get; }
+
+ public double VMax { get; }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/UvMeasure.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/UvMeasure.cs
new file mode 100644
index 00000000000..9aca6edf8c5
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/UvMeasure.cs
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ [System.Diagnostics.DebuggerDisplay("U = {U} V = {V}")]
+ internal struct UvMeasure
+ {
+ internal static readonly UvMeasure Zero = default(UvMeasure);
+
+ internal double U { get; set; }
+
+ internal double V { get; set; }
+
+ public UvMeasure(Orientation orientation, double width, double height)
+ {
+ if (orientation == Orientation.Horizontal)
+ {
+ U = width;
+ V = height;
+ }
+ else
+ {
+ U = height;
+ V = width;
+ }
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is UvMeasure measure)
+ {
+ return (measure.U == U) && (measure.V == V);
+ }
+
+ return false;
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapItem.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapItem.cs
new file mode 100644
index 00000000000..ea5ac2f90f3
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapItem.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.UI.Xaml;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ internal class WrapItem
+ {
+ public WrapItem(int index)
+ {
+ this.Index = index;
+ }
+
+ public int Index { get; }
+
+ public UvMeasure? Measure { get; internal set; }
+
+ public UvMeasure? Position { get; internal set; }
+
+ public UIElement Element { get; internal set; }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapLayout.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapLayout.cs
new file mode 100644
index 00000000000..7895892fe32
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapLayout.cs
@@ -0,0 +1,334 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Specialized;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Foundation;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// Arranges elements by wrapping them to fit the available space.
+ /// When is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
+ /// When is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
+ ///
+ public class WrapLayout : VirtualizingLayout
+ {
+ ///
+ /// Gets or sets a uniform Horizontal distance (in pixels) between items when is set to Horizontal,
+ /// or between columns of items when is set to Vertical.
+ ///
+ public double HorizontalSpacing
+ {
+ get { return (double)GetValue(HorizontalSpacingProperty); }
+ set { SetValue(HorizontalSpacingProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty HorizontalSpacingProperty =
+ DependencyProperty.Register(
+ nameof(HorizontalSpacing),
+ typeof(double),
+ typeof(WrapLayout),
+ new PropertyMetadata(0d, LayoutPropertyChanged));
+
+ ///
+ /// Gets or sets a uniform Vertical distance (in pixels) between items when is set to Vertical,
+ /// or between rows of items when is set to Horizontal.
+ ///
+ public double VerticalSpacing
+ {
+ get { return (double)GetValue(VerticalSpacingProperty); }
+ set { SetValue(VerticalSpacingProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty VerticalSpacingProperty =
+ DependencyProperty.Register(
+ nameof(VerticalSpacing),
+ typeof(double),
+ typeof(WrapLayout),
+ new PropertyMetadata(0d, LayoutPropertyChanged));
+
+ ///
+ /// Gets or sets the orientation of the WrapLayout.
+ /// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
+ /// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
+ ///
+ public Orientation Orientation
+ {
+ get { return (Orientation)GetValue(OrientationProperty); }
+ set { SetValue(OrientationProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty OrientationProperty =
+ DependencyProperty.Register(
+ nameof(Orientation),
+ typeof(Orientation),
+ typeof(WrapLayout),
+ new PropertyMetadata(Orientation.Horizontal, LayoutPropertyChanged));
+
+ private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is WrapLayout wp)
+ {
+ wp.InvalidateMeasure();
+ wp.InvalidateArrange();
+ }
+ }
+
+ ///
+ protected override void InitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ var state = new WrapLayoutState(context);
+ context.LayoutState = state;
+ base.InitializeForContextCore(context);
+ }
+
+ ///
+ protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
+ {
+ context.LayoutState = null;
+ base.UninitializeForContextCore(context);
+ }
+
+ ///
+ protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
+ {
+ var 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)
+ {
+ var totalMeasure = UvMeasure.Zero;
+ var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height);
+ var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
+ var realizationBounds = new UvBounds(Orientation, context.RealizationRect);
+ var position = UvMeasure.Zero;
+
+ var state = (WrapLayoutState)context.LayoutState;
+ if (state.Orientation != Orientation)
+ {
+ state.SetOrientation(Orientation);
+ }
+
+ if (spacingMeasure.Equals(state.Spacing) == false)
+ {
+ state.ClearPositions();
+ state.Spacing = spacingMeasure;
+ }
+
+ if (state.AvailableU != parentMeasure.U)
+ {
+ state.ClearPositions();
+ state.AvailableU = parentMeasure.U;
+ }
+
+ double currentV = 0;
+ for (int i = 0; i < context.ItemCount; i++)
+ {
+ bool measured = false;
+ WrapItem item = state.GetItemAt(i);
+ if (item.Measure == null)
+ {
+ item.Element = context.GetOrCreateElementAt(i);
+ item.Element.Measure(availableSize);
+ item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height);
+ measured = true;
+ }
+
+ UvMeasure currentMeasure = item.Measure.Value;
+ if (currentMeasure.U == 0)
+ {
+ continue; // ignore collapsed items
+ }
+
+ if (item.Position == null)
+ {
+ if (parentMeasure.U < position.U + currentMeasure.U)
+ {
+ // New Row
+ position.U = 0;
+ position.V += currentV + spacingMeasure.V;
+ currentV = 0;
+ }
+
+ item.Position = position;
+ }
+
+ position = item.Position.Value;
+
+ double vEnd = position.V + currentMeasure.V;
+ if (vEnd < realizationBounds.VMin)
+ {
+ // Item is "above" the bounds
+ if (item.Element != null)
+ {
+ context.RecycleElement(item.Element);
+ item.Element = null;
+ }
+ }
+ else if (position.V > realizationBounds.VMax)
+ {
+ // Item is "below" the bounds.
+ if (item.Element != null)
+ {
+ context.RecycleElement(item.Element);
+ item.Element = null;
+ }
+
+ // We don't need to measure anything below the bounds
+ break;
+ }
+ else if (measured == false)
+ {
+ // Always measure elements that are within the bounds
+ item.Element = context.GetOrCreateElementAt(i);
+ item.Element.Measure(availableSize);
+
+ currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height);
+ if (currentMeasure.Equals(item.Measure) == false)
+ {
+ // this item changed size; we need to recalculate layout for everything after this
+ state.RemoveFromIndex(i + 1);
+ item.Measure = currentMeasure;
+
+ // did the change make it go into the new row?
+ if (parentMeasure.U < position.U + currentMeasure.U)
+ {
+ // New Row
+ position.U = 0;
+ position.V += currentV + spacingMeasure.V;
+ currentV = 0;
+ }
+
+ item.Position = position;
+ }
+ }
+
+ position.U += currentMeasure.U + spacingMeasure.U;
+ currentV = Math.Max(currentMeasure.V, currentV);
+ }
+
+ // update value with the last line
+ // if the the last loop is(parentMeasure.U > currentMeasure.U + lineMeasure.U) the total isn't calculated then calculate it
+ // if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here
+ // for the last condition it is zeros so adding it will make no difference
+ // this way is faster than an if condition in every loop for checking the last item
+ totalMeasure.U = parentMeasure.U;
+ totalMeasure.V = state.GetHeight();
+
+ totalMeasure.U = Math.Ceiling(totalMeasure.U);
+
+ return Orientation == Orientation.Horizontal ? new Size(totalMeasure.U, totalMeasure.V) : new Size(totalMeasure.V, totalMeasure.U);
+ }
+
+ ///
+ protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
+ {
+ if (context.ItemCount > 0)
+ {
+ var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height);
+ var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing);
+ var realizationBounds = new UvBounds(Orientation, context.RealizationRect);
+
+ var state = (WrapLayoutState)context.LayoutState;
+ bool Arrange(WrapItem item, bool isLast = false)
+ {
+ if (item.Measure.HasValue == false)
+ {
+ return false;
+ }
+
+ if (item.Position == null)
+ {
+ return false;
+ }
+
+ var desiredMeasure = item.Measure.Value;
+ if (desiredMeasure.U == 0)
+ {
+ return true; // if an item is collapsed, avoid adding the spacing
+ }
+
+ UvMeasure position = item.Position.Value;
+
+ // Stretch the last item to fill the available space
+ if (isLast)
+ {
+ desiredMeasure.U = parentMeasure.U - position.U;
+ }
+
+ if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax))
+ {
+ // place the item
+ UIElement child = context.GetOrCreateElementAt(item.Index);
+ if (Orientation == Orientation.Horizontal)
+ {
+ child.Arrange(new Rect(position.U, position.V, desiredMeasure.U, desiredMeasure.V));
+ }
+ else
+ {
+ child.Arrange(new Rect(position.V, position.U, desiredMeasure.V, desiredMeasure.U));
+ }
+ }
+ else if (position.V > realizationBounds.VMax)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ for (var i = 0; i < context.ItemCount; i++)
+ {
+ bool continueArranging = Arrange(state.GetItemAt(i));
+ if (continueArranging == false)
+ {
+ break;
+ }
+ }
+ }
+
+ return finalSize;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapLayoutState.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapLayoutState.cs
new file mode 100644
index 00000000000..e9a55cfd40f
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls.Layout/WrapLayout/WrapLayoutState.cs
@@ -0,0 +1,146 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.UI.Xaml.Controls;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ internal class WrapLayoutState
+ {
+ private List _items = new List();
+ private VirtualizingLayoutContext _context;
+
+ public WrapLayoutState(VirtualizingLayoutContext context)
+ {
+ this._context = context;
+ }
+
+ public Orientation Orientation { get; private set; }
+
+ public UvMeasure Spacing { get; internal set; }
+
+ public double AvailableU { get; internal set; }
+
+ internal WrapItem GetItemAt(int index)
+ {
+ if (index < 0)
+ {
+ throw new IndexOutOfRangeException();
+ }
+
+ if (index <= (_items.Count - 1))
+ {
+ return _items[index];
+ }
+ else
+ {
+ WrapItem item = new WrapItem(index);
+ _items.Add(item);
+ return item;
+ }
+ }
+
+ internal void Clear()
+ {
+ _items.Clear();
+ }
+
+ internal 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);
+ }
+
+ internal void SetOrientation(Orientation orientation)
+ {
+ foreach (var item in _items.Where(i => i.Measure.HasValue))
+ {
+ UvMeasure measure = item.Measure.Value;
+ double v = measure.V;
+ measure.V = measure.U;
+ measure.U = v;
+ item.Measure = measure;
+ item.Position = null;
+ }
+
+ Orientation = orientation;
+ AvailableU = 0;
+ }
+
+ internal void ClearPositions()
+ {
+ foreach (var item in _items)
+ {
+ item.Position = null;
+ }
+ }
+
+ internal double GetHeight()
+ {
+ if (_items.Count == 0)
+ {
+ return 0;
+ }
+
+ bool calculateAverage = true;
+ if ((_items.Count == _context.ItemCount) && _items[_items.Count - 1].Position.HasValue)
+ {
+ calculateAverage = false;
+ }
+
+ UvMeasure? lastPosition = null;
+ double maxV = 0;
+
+ int itemCount = _items.Count;
+ for (int i = _items.Count - 1; i >= 0; i--)
+ {
+ var item = _items[i];
+ if (item.Position == null)
+ {
+ itemCount--;
+ continue;
+ }
+
+ if (lastPosition != null)
+ {
+ if (lastPosition.Value.V > item.Position.Value.V)
+ {
+ // This is a row above the last item. Exit and calculate the average
+ break;
+ }
+ }
+
+ lastPosition = item.Position;
+ maxV = Math.Max(maxV, item.Measure.Value.V);
+ }
+
+ double totalHeight = lastPosition.Value.V + maxV;
+ if (calculateAverage)
+ {
+ return (totalHeight / itemCount) * _context.ItemCount;
+ }
+ else
+ {
+ return totalHeight;
+ }
+ }
+
+ internal void RecycleElementAt(int index)
+ {
+ UIElement element = _context.GetOrCreateElementAt(index);
+ _context.RecycleElement(element);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Windows Community Toolkit.sln b/Windows Community Toolkit.sln
index a7a3f016cb5..b963eda466f 100644
--- a/Windows Community Toolkit.sln
+++ b/Windows Community Toolkit.sln
@@ -90,6 +90,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GazeInputTest", "GazeInputT
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Toolkit.Uwp.UI.Media", "Microsoft.Toolkit.Uwp.UI.Media\Microsoft.Toolkit.Uwp.UI.Media.csproj", "{75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Toolkit.Uwp.UI.Controls.Layout", "Microsoft.Toolkit.Uwp.UI.Controls.Layout\Microsoft.Toolkit.Uwp.UI.Controls.Layout.csproj", "{CB444381-18BA-4A51-BB32-3A498BCC1E99}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Toolkit.HighPerformance", "Microsoft.Toolkit.HighPerformance\Microsoft.Toolkit.HighPerformance.csproj", "{7E30D48C-4CD8-47BE-B557-10A20391DCC4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests.HighPerformance.NetCore", "UnitTests\UnitTests.HighPerformance.NetCore\UnitTests.HighPerformance.NetCore.csproj", "{D9BDBC68-3D0A-47FC-9C88-0BF769101644}"
@@ -813,6 +814,36 @@ Global
{75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Release|x64.Build.0 = Release|Any CPU
{75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Release|x86.ActiveCfg = Release|Any CPU
{75F9EE44-3EFA-47BC-AEDD-351B9834A0AF}.Release|x86.Build.0 = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|ARM.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|x64.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Debug|x86.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|Any CPU.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|Any CPU.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|ARM.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|ARM.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|ARM64.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|ARM64.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|x64.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|x64.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|x86.ActiveCfg = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Native|x86.Build.0 = Debug|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|ARM.ActiveCfg = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|ARM.Build.0 = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|ARM64.Build.0 = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|x64.ActiveCfg = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|x64.Build.0 = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|x86.ActiveCfg = Release|Any CPU
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99}.Release|x86.Build.0 = Release|Any CPU
{7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E30D48C-4CD8-47BE-B557-10A20391DCC4}.Debug|ARM.ActiveCfg = Debug|Any CPU
@@ -919,6 +950,7 @@ Global
{262BB7CE-EF42-4BF7-B90C-107E6CBB57FF} = {096ECFD7-7035-4487-9C87-81DCE9389620}
{A122EA02-4DE7-413D-BFBF-AF7DFC668DD6} = {B30036C4-D514-4E5B-A323-587A061772CE}
{75F9EE44-3EFA-47BC-AEDD-351B9834A0AF} = {F1AFFFA7-28FE-4770-BA48-10D76F3E59BC}
+ {CB444381-18BA-4A51-BB32-3A498BCC1E99} = {F1AFFFA7-28FE-4770-BA48-10D76F3E59BC}
{D9BDBC68-3D0A-47FC-9C88-0BF769101644} = {262CDB74-CF21-47AC-8DD9-CBC33C73B7CF}
{9B3A94A6-0D29-4523-880B-6938E2EFEEF7} = {262CDB74-CF21-47AC-8DD9-CBC33C73B7CF}
{262CDB74-CF21-47AC-8DD9-CBC33C73B7CF} = {B30036C4-D514-4E5B-A323-587A061772CE}