diff --git a/components/WrapPanel2/OpenSolution.bat b/components/WrapPanel2/OpenSolution.bat
new file mode 100644
index 000000000..814a56d4b
--- /dev/null
+++ b/components/WrapPanel2/OpenSolution.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
\ No newline at end of file
diff --git a/components/WrapPanel2/samples/Assets/icon.png b/components/WrapPanel2/samples/Assets/icon.png
new file mode 100644
index 000000000..8435bcaa9
Binary files /dev/null and b/components/WrapPanel2/samples/Assets/icon.png differ
diff --git a/components/WrapPanel2/samples/Dependencies.props b/components/WrapPanel2/samples/Dependencies.props
new file mode 100644
index 000000000..e622e1df4
--- /dev/null
+++ b/components/WrapPanel2/samples/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/WrapPanel2/samples/WrapPanel2.Samples.csproj b/components/WrapPanel2/samples/WrapPanel2.Samples.csproj
new file mode 100644
index 000000000..a8cc6056f
--- /dev/null
+++ b/components/WrapPanel2/samples/WrapPanel2.Samples.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+ WrapPanel2
+
+
+
+
+
+
+ WrapPanel2BasicSample.xaml
+
+
+
diff --git a/components/WrapPanel2/samples/WrapPanel2.md b/components/WrapPanel2/samples/WrapPanel2.md
new file mode 100644
index 000000000..a80b15b8c
--- /dev/null
+++ b/components/WrapPanel2/samples/WrapPanel2.md
@@ -0,0 +1,54 @@
+---
+title: WrapPannel2
+author: Avid29
+description: A labs-component candidate for a new WrapPanel implementation.
+keywords: WrapPanel, Control, Layout
+dev_langs:
+ - csharp
+category: Layouts
+subcategory: Panel
+discussion-id: 762
+issue-id: 763
+icon: assets/icon.png
+---
+
+# WrapPanel2
+
+The WrapPanel2 is an experiment for a new WrapPanel API using GridLength definitions to define the item's desired sizings.
+
+When stretched along the main axis, the child elements with star-sized GridLength values will proportionally occupy the available space.
+
+When not stretched along the main axis, star-sized child elements will be the smallest size possible while maintaining proportional sizing relative to each other and ensuring that all child elements are fully visible.
+
+
+> [!Sample WrapPanel2BasicSample]
+
+## Properties
+
+### Fixed Row Length
+
+When `FixedRowLengths` is enabled, all rows/columns will to stretch to the size of the largest row/column in the panel. When this is not enabled, rows/columns will size to their content individually.
+
+### Forced Stretch Method
+
+The `ForcedStretchMethod` property allows you to specify how the panel should handle stretching in rows without star-sized definitions.
+
+#### None
+
+When set to `None`, this panel will not stretch rows/columns that do not have star-sized definitions. When the alignment is set to stretch, and even when fixed row lengths is enabled, the rows/columns without star-sized definitions will size to their content.
+
+#### First
+
+When set the `First`, this panel will stretch the first item in the row/column to occupy the remaining space when needed to comply with stretch alignment.
+
+#### Last
+
+When set to `Last`, this panel will stretch the last item in the row/column to occupy the remaining space when needed to comply with stretch alignment.
+
+#### Equal
+
+When set to `Equal`, this panel will stretch all items in the row/column to occupy the equal space throughout the row when needed to comply with stretch alignment.
+
+#### Proportional
+
+When set to `Proportional`, this panel will stretch all items in the row/column proportionally to their defined size to occupy the remaining space when needed to comply with stretch alignment.
diff --git a/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml
new file mode 100644
index 000000000..2718d035d
--- /dev/null
+++ b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs
new file mode 100644
index 000000000..e1196aee5
--- /dev/null
+++ b/components/WrapPanel2/samples/WrapPanel2BasicSample.xaml.cs
@@ -0,0 +1,75 @@
+// 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 CommunityToolkit.WinUI.Controls;
+
+namespace WrapPanel2Experiment.Samples;
+
+///
+/// An example sample page of a custom control inheriting from Panel.
+///
+[ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")]
+[ToolkitSampleMultiChoiceOption("LayoutHorizontalAlignment", "Left", "Center", "Right", "Stretch", Title = "Horizontal Alignment")]
+[ToolkitSampleMultiChoiceOption("LayoutVerticalAlignment", "Top", "Center", "Bottom", "Stretch", Title = "Vertical Alignment")]
+[ToolkitSampleNumericOption("ItemSpacing", 8, 0, 16, Title = "Item Spacing")]
+[ToolkitSampleNumericOption("LineSpacing", 2, 0, 16, Title = "Line Spacing")]
+[ToolkitSampleBoolOption("FixedRowLengths", false, Title = "Fixed Row Lengths")]
+[ToolkitSampleMultiChoiceOption("LayoutForcedStretchMethod", "None", "First", "Last", "Equal", "Proportional", Title = "Forced Stretch Method")]
+[ToolkitSampleMultiChoiceOption("LayoutOverflowBehavior", "Wrap", "Drop", Title = "Overflow Behavior")]
+
+[ToolkitSample(id: nameof(WrapPanel2BasicSample), "WrapPanel2 Basic Sample", description: $"A sample for showing how to use a {nameof(WrapPanel2)} panel.")]
+public sealed partial class WrapPanel2BasicSample : Page
+{
+ public WrapPanel2BasicSample()
+ {
+ this.InitializeComponent();
+ }
+
+ // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149
+ public static Orientation ConvertStringToOrientation(string orientation) => orientation switch
+ {
+ "Vertical" => Orientation.Vertical,
+ "Horizontal" => Orientation.Horizontal,
+ _ => throw new System.NotImplementedException(),
+ };
+
+ // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149
+ public static HorizontalAlignment ConvertStringToHorizontalAlignment(string alignment) => alignment switch
+ {
+ "Left" => HorizontalAlignment.Left,
+ "Center" => HorizontalAlignment.Center,
+ "Right" => HorizontalAlignment.Right,
+ "Stretch" => HorizontalAlignment.Stretch,
+ _ => throw new System.NotImplementedException(),
+ };
+
+ // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149
+ public static VerticalAlignment ConvertStringToVerticalAlignment(string alignment) => alignment switch
+ {
+ "Top" => VerticalAlignment.Top,
+ "Center" => VerticalAlignment.Center,
+ "Bottom" => VerticalAlignment.Bottom,
+ "Stretch" => VerticalAlignment.Stretch,
+ _ => throw new System.NotImplementedException(),
+ };
+
+ // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149
+ public static ForcedStretchMethod ConvertStringToForcedStretchMethod(string stretchMethod) => stretchMethod switch
+ {
+ "None" => ForcedStretchMethod.None,
+ "First" => ForcedStretchMethod.First,
+ "Last" => ForcedStretchMethod.Last,
+ "Equal" => ForcedStretchMethod.Equal,
+ "Proportional" => ForcedStretchMethod.Proportional,
+ _ => throw new System.NotImplementedException(),
+ };
+
+ // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149
+ public static OverflowBehavior ConvertStringToOverflowBehavior(string overflowBehavior) => overflowBehavior switch
+ {
+ "Wrap" => OverflowBehavior.Wrap,
+ "Drop" => OverflowBehavior.Drop,
+ _ => throw new System.NotImplementedException(),
+ };
+}
diff --git a/components/WrapPanel2/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj b/components/WrapPanel2/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj
new file mode 100644
index 000000000..fef145482
--- /dev/null
+++ b/components/WrapPanel2/src/CommunityToolkit.WinUI.Controls.StretchPanel.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+ WrapPanel2
+ This package contains WrapPanel2.
+
+
+ CommunityToolkit.WinUI.Controls.WrapPanel2Rns
+
+
+
+
+
diff --git a/components/WrapPanel2/src/Dependencies.props b/components/WrapPanel2/src/Dependencies.props
new file mode 100644
index 000000000..e622e1df4
--- /dev/null
+++ b/components/WrapPanel2/src/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/WrapPanel2/src/ForcedStretchMethod.cs b/components/WrapPanel2/src/ForcedStretchMethod.cs
new file mode 100644
index 000000000..ed8483840
--- /dev/null
+++ b/components/WrapPanel2/src/ForcedStretchMethod.cs
@@ -0,0 +1,36 @@
+// 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.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Describes the behavior of items in rows without a star-sized item.
+///
+public enum ForcedStretchMethod
+{
+ ///
+ /// Items will never be streched beyond their desired size.
+ ///
+ None,
+
+ ///
+ /// The first item in the row will be stretched to fill the row.
+ ///
+ First,
+
+ ///
+ /// The last item in the row will be stretched to fill the row.
+ ///
+ Last,
+
+ ///
+ /// Each item will be stretched to an equal size to fill the row.
+ ///
+ Equal,
+
+ ///
+ /// Each item will be stretched proportional to their desired size to fill the row.
+ ///
+ Proportional,
+}
diff --git a/components/WrapPanel2/src/MultiTarget.props b/components/WrapPanel2/src/MultiTarget.props
new file mode 100644
index 000000000..b11c19426
--- /dev/null
+++ b/components/WrapPanel2/src/MultiTarget.props
@@ -0,0 +1,9 @@
+
+
+
+ uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android;
+
+
\ No newline at end of file
diff --git a/components/WrapPanel2/src/OverflowBehavior.cs b/components/WrapPanel2/src/OverflowBehavior.cs
new file mode 100644
index 000000000..ebd22c0ca
--- /dev/null
+++ b/components/WrapPanel2/src/OverflowBehavior.cs
@@ -0,0 +1,21 @@
+// 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.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// Describes the behavior of items that exceed the available space in the panel.
+///
+public enum OverflowBehavior
+{
+ ///
+ /// When an item exceeds the available space, it will be moved to a new row or column.
+ ///
+ Wrap,
+
+ ///
+ /// Items which do not fit within the available space will be removed from the layout.
+ ///
+ Drop,
+}
diff --git a/components/WrapPanel2/src/WrapPanel2.Properties.cs b/components/WrapPanel2/src/WrapPanel2.Properties.cs
new file mode 100644
index 000000000..ed28b429f
--- /dev/null
+++ b/components/WrapPanel2/src/WrapPanel2.Properties.cs
@@ -0,0 +1,142 @@
+// 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.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+public partial class WrapPanel2
+{
+ ///
+ /// An attached property for identifying the requested layout of a child within the panel.
+ ///
+ public static readonly DependencyProperty LayoutLengthProperty =
+ DependencyProperty.Register(
+ "LayoutLength",
+ typeof(GridLength),
+ typeof(WrapPanel2),
+ new PropertyMetadata(GridLength.Auto));
+
+ ///
+ /// Backing for the property.
+ ///
+ public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
+ nameof(Orientation),
+ typeof(Orientation),
+ typeof(WrapPanel2),
+ new PropertyMetadata(default(Orientation), OnPropertyChanged));
+
+ ///
+ /// Backing for the property.
+ ///
+ public static readonly DependencyProperty ItemSpacingProperty = DependencyProperty.Register(
+ nameof(ItemSpacing),
+ typeof(double),
+ typeof(WrapPanel2),
+ new PropertyMetadata(default(double), OnPropertyChanged));
+
+ ///
+ /// Backing for the property.
+ ///
+ public static readonly DependencyProperty lineSpacingProperty = DependencyProperty.Register(
+ nameof(LineSpacing),
+ typeof(double),
+ typeof(WrapPanel2),
+ new PropertyMetadata(default(double), OnPropertyChanged));
+
+ ///
+ /// Backing for the property.
+ ///
+ public static readonly DependencyProperty FixedRowLengthsProperty = DependencyProperty.Register(
+ nameof(FixedRowLengths),
+ typeof(bool),
+ typeof(WrapPanel2),
+ new PropertyMetadata(default(bool), OnPropertyChanged));
+
+ ///
+ /// Backing for the property.
+ ///
+ public static readonly DependencyProperty ForcedStretchMethodProperty = DependencyProperty.Register(
+ nameof(ForcedStretchMethod),
+ typeof(ForcedStretchMethod),
+ typeof(WrapPanel2),
+ new PropertyMetadata(default(ForcedStretchMethod), OnPropertyChanged));
+
+ ///
+ /// Backing for the property.
+ ///
+ public static readonly DependencyProperty OverflowBehaviorProperty = DependencyProperty.Register(
+ nameof(OverflowBehavior),
+ typeof(OverflowBehavior),
+ typeof(WrapPanel2),
+ new PropertyMetadata(default(OverflowBehavior), OnPropertyChanged));
+
+ ///
+ /// Gets or sets the panel orientation.
+ ///
+ public Orientation Orientation
+ {
+ get => (Orientation)GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ ///
+ /// Gets or sets the spacing between items.
+ ///
+ public double ItemSpacing
+ {
+ get => (double)GetValue(ItemSpacingProperty);
+ set => SetValue(ItemSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the vertical spacing between items.
+ ///
+ public double LineSpacing
+ {
+ get => (double)GetValue(lineSpacingProperty);
+ set => SetValue(lineSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets whether or not all rows/columns should stretch to match the length of the longest.
+ ///
+ public bool FixedRowLengths
+ {
+ get => (bool)GetValue(FixedRowLengthsProperty);
+ set => SetValue(FixedRowLengthsProperty, value);
+ }
+
+ ///
+ /// Gets or sets the method used to fill rows without a star-sized item.
+ ///
+ public ForcedStretchMethod ForcedStretchMethod
+ {
+ get => (ForcedStretchMethod)GetValue(ForcedStretchMethodProperty);
+ set => SetValue(ForcedStretchMethodProperty, value);
+ }
+
+ ///
+ /// Gets or sets how the panel handles content overflowing the available space.
+ ///
+ public OverflowBehavior OverflowBehavior
+ {
+ get => (OverflowBehavior)GetValue(OverflowBehaviorProperty);
+ set => SetValue(OverflowBehaviorProperty, value);
+ }
+
+ ///
+ /// Gets the of an item in the .
+ ///
+ public static GridLength GetLayoutLength(DependencyObject obj) => (GridLength)obj.GetValue(LayoutLengthProperty);
+
+ ///
+ /// Sets the of an item in the .
+ ///
+ public static void SetLayoutLength(DependencyObject obj, GridLength value) => obj.SetValue(LayoutLengthProperty, value);
+
+ private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var panel = (WrapPanel2)d;
+ panel.InvalidateMeasure();
+ }
+}
diff --git a/components/WrapPanel2/src/WrapPanel2.Structs.cs b/components/WrapPanel2/src/WrapPanel2.Structs.cs
new file mode 100644
index 000000000..2c423730c
--- /dev/null
+++ b/components/WrapPanel2/src/WrapPanel2.Structs.cs
@@ -0,0 +1,155 @@
+// 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.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+public partial class WrapPanel2
+{
+ ///
+ /// A struct representing the specifications of a row or column in the panel.
+ ///
+ private struct RowSpec
+ {
+ public RowSpec(GridLength layout, UVCoord desiredSize)
+ {
+ switch (layout.GridUnitType)
+ {
+ case GridUnitType.Auto:
+ ReservedSpace = desiredSize.U;
+ break;
+ case GridUnitType.Pixel:
+ ReservedSpace = layout.Value;
+ break;
+ case GridUnitType.Star:
+ PortionsSum = layout.Value;
+ MinPortionSize = desiredSize.U / layout.Value;
+ break;
+ }
+
+ MaxOffAxisSize = desiredSize.V;
+ ItemsCount = 1;
+ }
+
+ ///
+ /// Gets the total reserved space for spacing in the row/column.
+ ///
+ ///
+ /// Items with a fixed size or auto size contribute to this value.
+ ///
+ public double ReservedSpace { get; private set; }
+
+ ///
+ /// Gets the sum of portions in the row/column.
+ ///
+ ///
+ /// Items with a star-sized length contribute to this value.
+ ///
+ public double PortionsSum { get; private set; }
+
+ ///
+ /// Gets the maximum width/height of items in the row/column.
+ ///
+ ///
+ /// Width in vertical orientation, height in horizontal orientation.
+ ///
+ public double MaxOffAxisSize { get; private set; }
+
+ ///
+ /// Gets the minimum size of a portion in the row/column.
+ ///
+ public double MinPortionSize { get; private set; }
+
+ ///
+ /// Gets the number of items in the row/column.
+ ///
+ public int ItemsCount { get; private set; }
+
+ public bool TryAdd(RowSpec addend, double spacing, double maxSize)
+ {
+ // Check if adding the new spec would exceed the maximum size
+ var sum = this + addend;
+ if (sum.Measure(spacing) > maxSize)
+ return false;
+
+ // Update the current spec to include the new spec
+ this = sum;
+ return true;
+ }
+
+ public readonly double Measure(double spacing)
+ {
+ var totalSpacing = (ItemsCount - 1) * spacing;
+ var totalSize = ReservedSpace + totalSpacing;
+
+ // Add star-sized items if applicable
+ if (!double.IsNaN(MinPortionSize) && !double.IsInfinity(MinPortionSize))
+ totalSize += MinPortionSize * PortionsSum;
+
+ return totalSize;
+ }
+
+ public static RowSpec operator +(RowSpec a, RowSpec b)
+ {
+ var combined = new RowSpec
+ {
+ ReservedSpace = a.ReservedSpace + b.ReservedSpace,
+ PortionsSum = a.PortionsSum + b.PortionsSum,
+ MinPortionSize = Math.Max(a.MinPortionSize, b.MinPortionSize),
+ MaxOffAxisSize = Math.Max(a.MaxOffAxisSize, b.MaxOffAxisSize),
+ ItemsCount = a.ItemsCount + b.ItemsCount
+ };
+ return combined;
+ }
+ }
+
+ ///
+ /// A struct for mapping X/Y coordinates to an orientation adjusted U/V coordinate system.
+ ///
+ private struct UVCoord(double x, double y, Orientation orientation)
+ {
+ private readonly bool _horizontal = orientation is Orientation.Horizontal;
+
+ public UVCoord(Size size, Orientation orientation) : this(size.Width, size.Height, orientation)
+ {
+ }
+
+ public double X { get; set; } = x;
+
+ public double Y { get; set; } = y;
+
+ public double U
+ {
+ readonly get => _horizontal ? X : Y;
+ set
+ {
+ if (_horizontal)
+ {
+ X = value;
+ }
+ else
+ {
+ Y = value;
+ }
+ }
+ }
+
+ public double V
+ {
+ readonly get => _horizontal ? Y : X;
+ set
+ {
+ if (_horizontal)
+ {
+ Y = value;
+ }
+ else
+ {
+ X = value;
+ }
+ }
+ }
+
+ public readonly Size Size => new(X, Y);
+ }
+}
diff --git a/components/WrapPanel2/src/WrapPanel2.cs b/components/WrapPanel2/src/WrapPanel2.cs
new file mode 100644
index 000000000..ba2bf1a50
--- /dev/null
+++ b/components/WrapPanel2/src/WrapPanel2.cs
@@ -0,0 +1,326 @@
+// 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.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// A panel that arranges its children in a grid-like fashion, stretching them to fill available space.
+///
+public partial class WrapPanel2 : Panel
+{
+ private List? _rowSpecs;
+ private double _longestRowSize = 0;
+
+ ///
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ _rowSpecs = [];
+ _longestRowSize = 0;
+
+ // Define XY/UV coordinate variables
+ var uvAvailableSize = new UVCoord(availableSize.Width, availableSize.Height, Orientation);
+
+ RowSpec currentRowSpec = default;
+
+ var elements = Children.Where(static e => e.Visibility is Visibility.Visible);
+
+ // Do nothing if the panel is empty
+ if (!elements.Any())
+ {
+ return new Size(0, 0);
+ }
+
+ foreach (var child in elements)
+ {
+ // Measure the child's desired size and get layout
+ child.Measure(availableSize);
+ var uvDesiredSize = new UVCoord(child.DesiredSize, Orientation);
+ var layoutLength = GetLayoutLength(child);
+
+ // Attempt to add the child to the current row/column
+ var spec = new RowSpec(layoutLength, uvDesiredSize);
+ if (!currentRowSpec.TryAdd(spec, ItemSpacing, uvAvailableSize.U))
+ {
+ // If the overflow behavior is drop, just end the row here.
+ if (OverflowBehavior is OverflowBehavior.Drop)
+ break;
+
+ // Could not add to current row/column
+ // Start a new row/column
+ _rowSpecs.Add(currentRowSpec);
+ _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(ItemSpacing));
+ currentRowSpec = spec;
+ }
+ }
+
+ // Add the final row/column
+ _rowSpecs.Add(currentRowSpec);
+ _longestRowSize = Math.Max(_longestRowSize, currentRowSpec.Measure(ItemSpacing));
+
+ // Calculate final desired size
+ var uvSize = new UVCoord(0, 0, Orientation)
+ {
+ U = IsMainAxisStretch(uvAvailableSize.U) ? uvAvailableSize.U : _longestRowSize,
+ V = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (LineSpacing * (_rowSpecs.Count - 1))
+ };
+
+ // Clamp to available size and return
+ uvSize.U = Math.Min(uvSize.U, uvAvailableSize.U);
+ uvSize.V = Math.Min(uvSize.V, uvAvailableSize.V);
+ return uvSize.Size;
+ }
+
+ ///
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ // Do nothing if there are no rows/columns
+ if (_rowSpecs is null || _rowSpecs.Count is 0)
+ return new Size(0, 0);
+
+ // Create XY/UV coordinate variables
+ var pos = new UVCoord(0, 0, Orientation);
+ var uvFinalSize = new UVCoord(finalSize, Orientation);
+
+ // Adjust the starting position based on off-axis alignment
+ var contentHeight = _rowSpecs.Sum(static rs => rs.MaxOffAxisSize) + (LineSpacing * (_rowSpecs.Count - 1));
+ pos.V = GetStartByAlignment(GetOffAlignment(), contentHeight, uvFinalSize.V);
+
+ var childQueue = new Queue(Children.Where(static e => e.Visibility is Visibility.Visible));
+
+ foreach (var row in _rowSpecs)
+ {
+ // Arrange the row/column
+ ArrangeRow(ref pos, row, uvFinalSize, childQueue);
+ }
+
+ // "Arrange" remaning children by rendering them with zero size
+ while (childQueue.TryDequeue(out var child))
+ {
+ // Arrange with zero size
+ child.Arrange(new Rect(0, 0, 0, 0));
+ }
+
+ return finalSize;
+ }
+
+ private void ArrangeRow(ref UVCoord pos, RowSpec row, UVCoord uvFinalSize, Queue childQueue)
+ {
+ var spacingTotalSize = ItemSpacing * (row.ItemsCount - 1);
+ var remainingSpace = uvFinalSize.U - row.ReservedSpace - spacingTotalSize;
+ var portionSize = row.MinPortionSize;
+
+ // Determine if the desired alignment is stretched.
+ // Or if fixed row lengths are in use.
+ bool stretch = IsMainAxisStretch(uvFinalSize.U) || FixedRowLengths;
+
+ // Calculate portion size if stretching
+ // Same logic applies for matching row lengths, since the size was determined during measure
+ if (stretch)
+ {
+ portionSize = remainingSpace / row.PortionsSum;
+ }
+
+ // Reset the starting U position
+ pos.U = 0;
+
+ // Adjust the starting position if not stretching
+ // Also do this if there are no star-sized items in the row/column and no forced streching is in use.
+ if (!stretch || (row.PortionsSum is 0 && ForcedStretchMethod is ForcedStretchMethod.None))
+ {
+ var rowSize = row.Measure(ItemSpacing);
+ pos.U = GetStartByAlignment(GetAlignment(), rowSize, uvFinalSize.U);
+ }
+
+ // Set a flag for if the row is being forced to stretch
+ bool forceStretch = row.PortionsSum is 0 && ForcedStretchMethod is not ForcedStretchMethod.None;
+
+ // Setup portionSize for forced stretching
+ if (forceStretch)
+ {
+ portionSize = ForcedStretchMethod switch
+ {
+ // The first child's size will be overridden to 1*
+ // Change portion size to fill remaining space plus its original size
+ ForcedStretchMethod.First =>
+ remainingSpace + GetChildSize(childQueue.Peek()),
+
+ // The last child's size will be overridden to 1*
+ // Change portion size to fill remaining space plus its original size
+ ForcedStretchMethod.Last =>
+ remainingSpace + GetChildSize(childQueue.ElementAt(row.ItemsCount - 1)),
+
+ // All children's sizes will be overridden to 1*
+ // Change portion size to evenly distribute remaining space
+ ForcedStretchMethod.Equal =>
+ (uvFinalSize.U - spacingTotalSize) / row.ItemsCount,
+
+ // All children's sizes will be overridden to star sizes proportional to their original size
+ // Change portion size to distribute remaining space proportionally
+ ForcedStretchMethod.Proportional =>
+ (uvFinalSize.U - spacingTotalSize) / row.ReservedSpace,
+
+ // Default case (should not be hit)
+ _ => row.MinPortionSize,
+ };
+ }
+
+ // Arrange each child in the row/column
+ for (int i = 0; i < row.ItemsCount; i++)
+ {
+ // Get the next child
+ var child = childQueue.Dequeue();
+
+ // Sanity check
+ if (child is null)
+ return;
+
+ // Determine the child's size
+ var size = GetChildSize(child, i, row, portionSize, forceStretch);
+
+ // NOTE: The arrange method is still in X/Y coordinate system
+ child.Arrange(new Rect(pos.X, pos.Y, size.X, size.Y));
+
+ // Advance the position
+ pos.U += size.U + ItemSpacing;
+ }
+
+ // Advance to the next row/column
+ pos.V += row.MaxOffAxisSize + LineSpacing;
+ }
+
+ private UVCoord GetChildSize(UIElement child, int indexInRow, RowSpec row, double portionSize, bool forceStretch)
+ {
+ // Get layout and desired size
+ var layoutLength = GetLayoutLength(child);
+ var uvDesiredSize = new UVCoord(child.DesiredSize, Orientation);
+
+ // Override the layout based on the forced stretch method if necessary
+ if (forceStretch)
+ {
+ var oneStar = new GridLength(1, GridUnitType.Star);
+ layoutLength = ForcedStretchMethod switch
+ {
+ // Override the first item's layout to 1*
+ ForcedStretchMethod.First when indexInRow is 0 => oneStar,
+
+ // Override the last item's layout to 1*
+ ForcedStretchMethod.Last when indexInRow == (row.ItemsCount - 1) => oneStar,
+
+ // Override all item's layouts to 1*
+ ForcedStretchMethod.Equal => oneStar,
+
+ // Override all item's layouts to star sizes proportional to their original size
+ ForcedStretchMethod.Proportional => layoutLength.GridUnitType switch
+ {
+ GridUnitType.Auto => new GridLength(uvDesiredSize.U, GridUnitType.Star),
+ GridUnitType.Pixel or _ => new GridLength(layoutLength.Value, GridUnitType.Star),
+ },
+
+ // If the above conditions aren't met, do nothing
+ _ => layoutLength,
+ };
+ }
+
+ // Determine the child's U size
+ double uSize = layoutLength.GridUnitType switch
+ {
+ GridUnitType.Auto => uvDesiredSize.U,
+ GridUnitType.Pixel => layoutLength.Value,
+ GridUnitType.Star => layoutLength.Value * portionSize,
+ _ => uvDesiredSize.U,
+ };
+
+ // Return the final size
+ return new UVCoord(0, 0, Orientation)
+ {
+ U = uSize,
+ V = row.MaxOffAxisSize
+ };
+ }
+
+ private static double GetStartByAlignment(Alignment alignment, double size, double availableSize)
+ {
+ return alignment switch
+ {
+ Alignment.Start => 0,
+ Alignment.Center => (availableSize / 2) - (size / 2),
+ Alignment.End => availableSize - size,
+ _ => 0,
+ };
+ }
+
+ private Alignment GetAlignment()
+ {
+ return Orientation switch
+ {
+ Orientation.Horizontal => HorizontalAlignment switch
+ {
+ HorizontalAlignment.Left => Alignment.Start,
+ HorizontalAlignment.Center => Alignment.Center,
+ HorizontalAlignment.Right => Alignment.End,
+ HorizontalAlignment.Stretch => Alignment.Stretch,
+ _ => Alignment.Start,
+ },
+ Orientation.Vertical => VerticalAlignment switch
+ {
+ VerticalAlignment.Top => Alignment.Start,
+ VerticalAlignment.Center => Alignment.Center,
+ VerticalAlignment.Bottom => Alignment.End,
+ VerticalAlignment.Stretch => Alignment.Stretch,
+ _ => Alignment.Start,
+ },
+ _ => Alignment.Start,
+ };
+ }
+
+ private Alignment GetOffAlignment()
+ {
+ return Orientation switch
+ {
+ Orientation.Horizontal => VerticalAlignment switch
+ {
+ VerticalAlignment.Top => Alignment.Start,
+ VerticalAlignment.Center => Alignment.Center,
+ VerticalAlignment.Bottom => Alignment.End,
+ VerticalAlignment.Stretch => Alignment.Stretch,
+ _ => Alignment.Start,
+ },
+ Orientation.Vertical => HorizontalAlignment switch
+ {
+ HorizontalAlignment.Left => Alignment.Start,
+ HorizontalAlignment.Center => Alignment.Center,
+ HorizontalAlignment.Right => Alignment.End,
+ HorizontalAlignment.Stretch => Alignment.Stretch,
+ _ => Alignment.Start,
+ },
+ _ => Alignment.Start,
+ };
+ }
+
+ ///
+ /// Determine if the desired alignment is stretched.
+ /// Don't stretch if infinite space is available though. Attempting to divide infinite space will result in a crash.
+ ///
+ private bool IsMainAxisStretch(double availableSize) => GetAlignment() is Alignment.Stretch && !double.IsInfinity(availableSize);
+
+ private double GetChildSize(UIElement child)
+ {
+ var childLayout = GetLayoutLength(child);
+
+ return childLayout.GridUnitType switch
+ {
+ GridUnitType.Auto => new UVCoord(child.DesiredSize, Orientation).U,
+ GridUnitType.Pixel => childLayout.Value,
+ _ => 0,
+ };
+ }
+
+ private enum Alignment
+ {
+ Start,
+ Center,
+ End,
+ Stretch
+ }
+}
diff --git a/components/WrapPanel2/tests/ExampleWrapPanel2TestClass.cs b/components/WrapPanel2/tests/ExampleWrapPanel2TestClass.cs
new file mode 100644
index 000000000..5a41388c8
--- /dev/null
+++ b/components/WrapPanel2/tests/ExampleWrapPanel2TestClass.cs
@@ -0,0 +1,134 @@
+// 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 CommunityToolkit.Tooling.TestGen;
+using CommunityToolkit.Tests;
+using CommunityToolkit.WinUI.Controls;
+
+namespace WrapPanel2Tests;
+
+[TestClass]
+public partial class ExampleWrapPanel2TestClass : VisualUITestBase
+{
+ // If you don't need access to UI objects directly or async code, use this pattern.
+ [TestMethod]
+ public void SimpleSynchronousExampleTest()
+ {
+ var assembly = typeof(WrapPanel2).Assembly;
+ var type = assembly.GetType(typeof(WrapPanel2).FullName ?? string.Empty);
+
+ Assert.IsNotNull(type, "Could not find WrapPanel2 type.");
+ Assert.AreEqual(typeof(WrapPanel2), type, "Type of WrapPanel2 does not match expected type.");
+ }
+
+ // If you don't need access to UI objects directly, use this pattern.
+ [TestMethod]
+ public async Task SimpleAsyncExampleTest()
+ {
+ await Task.Delay(250);
+
+ Assert.IsTrue(true);
+ }
+
+ // Example that shows how to check for exception throwing.
+ [TestMethod]
+ public void SimpleExceptionCheckTest()
+ {
+ // If you need to check exceptions occur for invalid inputs, etc...
+ // Use Assert.ThrowsException to limit the scope to where you expect the error to occur.
+ // Otherwise, using the ExpectedException attribute could swallow or
+ // catch other issues in setup code.
+ Assert.ThrowsException(() => throw new NotImplementedException());
+ }
+
+ // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects.
+ [UIThreadTestMethod]
+ public void SimpleUIAttributeExampleTest()
+ {
+ var component = new WrapPanel2();
+ Assert.IsNotNull(component);
+ }
+
+ // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter.
+ // This lets us actually test a control as it would behave within an actual application.
+ // The page will already be loaded by the time your test is called.
+ [UIThreadTestMethod]
+ public void SimpleUIExamplePageTest(ExampleWrapPanel2TestPage page)
+ {
+ // You can use the Toolkit Visual Tree helpers here to find the component by type or name:
+ var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+
+ var componentByName = page.FindDescendant("WrapPanel2Control");
+
+ Assert.IsNotNull(componentByName);
+ }
+
+ // You can still do async work with a UIThreadTestMethod as well.
+ [UIThreadTestMethod]
+ public async Task SimpleAsyncUIExamplePageTest(ExampleWrapPanel2TestPage page)
+ {
+ // This helper can be used to wait for a rendering pass to complete.
+ // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper.
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+ }
+
+ //// ----------------------------- ADVANCED TEST SCENARIOS -----------------------------
+
+ // If you need to use DataRow, you can use this pattern with the UI dispatch still.
+ // Otherwise, checkout the UIThreadTestMethod attribute above.
+ // See https://github.com/CommunityToolkit/Labs-Windows/issues/186
+ [TestMethod]
+ public async Task ComplexAsyncUIExampleTest()
+ {
+ await EnqueueAsync(() =>
+ {
+ var component = new WrapPanel2();
+ Assert.IsNotNull(component);
+ });
+ }
+
+ // If you want to load other content not within a XAML page using the UIThreadTestMethod above.
+ // Then you can do that using the Load/UnloadTestContentAsync methods.
+ [TestMethod]
+ public async Task ComplexAsyncLoadUIExampleTest()
+ {
+ await EnqueueAsync(async () =>
+ {
+ var component = new WrapPanel2();
+ Assert.IsNotNull(component);
+ Assert.IsFalse(component.IsLoaded);
+
+ await LoadTestContentAsync(component);
+
+ Assert.IsTrue(component.IsLoaded);
+
+ await UnloadTestContentAsync(component);
+
+ Assert.IsFalse(component.IsLoaded);
+ });
+ }
+
+ // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well:
+ [UIThreadTestMethod]
+ public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest()
+ {
+ var component = new WrapPanel2();
+ Assert.IsNotNull(component);
+ Assert.IsFalse(component.IsLoaded);
+
+ await LoadTestContentAsync(component);
+
+ Assert.IsTrue(component.IsLoaded);
+
+ await UnloadTestContentAsync(component);
+
+ Assert.IsFalse(component.IsLoaded);
+ }
+}
diff --git a/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml b/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml
new file mode 100644
index 000000000..5e5b69b41
--- /dev/null
+++ b/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml.cs b/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml.cs
new file mode 100644
index 000000000..0272a9498
--- /dev/null
+++ b/components/WrapPanel2/tests/ExampleWrapPanel2TestPage.xaml.cs
@@ -0,0 +1,16 @@
+// 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.
+
+namespace WrapPanel2Tests;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class ExampleWrapPanel2TestPage : Page
+{
+ public ExampleWrapPanel2TestPage()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/WrapPanel2/tests/WrapPanel2.Tests.projitems b/components/WrapPanel2/tests/WrapPanel2.Tests.projitems
new file mode 100644
index 000000000..5dd673b98
--- /dev/null
+++ b/components/WrapPanel2/tests/WrapPanel2.Tests.projitems
@@ -0,0 +1,23 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ 1EFF9838-CA24-43C3-AA4F-0B321F74861B
+
+
+ WrapPanel2Tests
+
+
+
+
+ ExampleWrapPanel2TestPage.xaml
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+
\ No newline at end of file
diff --git a/components/WrapPanel2/tests/WrapPanel2.Tests.shproj b/components/WrapPanel2/tests/WrapPanel2.Tests.shproj
new file mode 100644
index 000000000..8db9f9ff5
--- /dev/null
+++ b/components/WrapPanel2/tests/WrapPanel2.Tests.shproj
@@ -0,0 +1,13 @@
+
+
+
+ 1EFF9838-CA24-43C3-AA4F-0B321F74861B
+ 14.0
+
+
+
+
+
+
+
+
diff --git a/tooling b/tooling
index e7eb23621..fa4dd480f 160000
--- a/tooling
+++ b/tooling
@@ -1 +1 @@
-Subproject commit e7eb23621735ea8d4a3191ac399848acc918f1ec
+Subproject commit fa4dd480fda756e4d503bea17629eda6c89afae0