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