diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/AutoLayoutPage.xaml b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/AutoLayoutPage.xaml index 9c7495b3c..a0e9b5855 100644 --- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/AutoLayoutPage.xaml +++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/AutoLayoutPage.xaml @@ -27,7 +27,7 @@ - + diff --git a/src/Uno.Toolkit.RuntimeTests/Tests/AutoLayoutTest.cs b/src/Uno.Toolkit.RuntimeTests/Tests/AutoLayoutTest.cs index 0545727c7..55a347d42 100644 --- a/src/Uno.Toolkit.RuntimeTests/Tests/AutoLayoutTest.cs +++ b/src/Uno.Toolkit.RuntimeTests/Tests/AutoLayoutTest.cs @@ -10,6 +10,8 @@ using Uno.Toolkit.UI; using Uno.UI.RuntimeTests; using Windows.UI; +using FluentAssertions; +using FluentAssertions.Execution; #if IS_WINUI using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -64,34 +66,43 @@ public async Task When_SpaceBetween_with_spacing(Orientation orientation, double var layoutRect1Actual = LayoutInformation.GetLayoutSlot(SUT.Children[1] as FrameworkElement); var layoutRect2Actual = LayoutInformation.GetLayoutSlot(SUT.Children[2] as FrameworkElement); + using var _ = new AssertionScope(); + if (orientation is Orientation.Vertical) { - Assert.AreEqual(expectedResult1, layoutRect0Actual.Y); - Assert.AreEqual(expectedResult2, layoutRect1Actual.Y); - Assert.AreEqual(expectedResult3, layoutRect2Actual.Y); + layoutRect0Actual.Y.Should().Be(expectedResult1); + layoutRect1Actual.Y.Should().Be(expectedResult2); + layoutRect2Actual.Y.Should().Be(expectedResult3); } else { - Assert.AreEqual(expectedResult1, layoutRect0Actual.X); - Assert.AreEqual(expectedResult2, layoutRect1Actual.X); - Assert.AreEqual(expectedResult3, layoutRect2Actual.X); + layoutRect0Actual.X.Should().Be(expectedResult1); + layoutRect1Actual.X.Should().Be(expectedResult2); + layoutRect2Actual.X.Should().Be(expectedResult3); } } + [TestMethod] [RequiresFullWindow] - [DataRow(true, Orientation.Vertical, VerticalAlignment.Bottom, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 108, 0, 0, 10 }, 10, 298, 110, 12, 185)] - [DataRow(true, Orientation.Vertical, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 108, 10, 0, 0 }, 10, 12, 110, 12, 185)] - [DataRow(true, Orientation.Vertical, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 108, 10, 0, 0 }, -30, 12, 110, 12, 165)] - [DataRow(true, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 10, 10, 0, 0 }, 10, 12, 12, 12, 105)] - [DataRow(true, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 10, 10, 0, 0 }, 10, 12, 12, 12, 105)] - [DataRow(true, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 10, 10, 0, 0 }, -30, 12, 12, 12, 85)] - [DataRow(false, Orientation.Vertical, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 108, 10, 0, 0 }, 10, 12, 110, 138, 248)] - [DataRow(false, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 108, 10, 0, 0 }, 10, 12, 110, 78, 138)] - [DataRow(false, Orientation.Vertical, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 108, 10, 0, 0 }, -20, 12, 110, 168, 248)] - [DataRow(false, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 108, 10, 0, 0 }, -20, 12, 110, 108, 138)] + [DataRow(true, Orientation.Vertical, VerticalAlignment.Bottom, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 110, 0, 0, 10 }, 10, 300, 110, 12, 185)] + [DataRow(true, Orientation.Vertical, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 110, 10, 0, 0 }, 10, 10, 110, 12, 185)] + [DataRow(true, Orientation.Vertical, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 110, 10, 0, 0 }, -30, 10, 110, 12, 165)] + [DataRow(true, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 10, 10, 0, 0 }, 10, 10, 10, 12, 105)] + [DataRow(true, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 10, 10, 0, 0 }, 10, 10, 10, 12, 105)] + [DataRow(true, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 10, 10, 0, 0 }, -30, 10, 10, 12, 85)] + [DataRow(false, Orientation.Vertical, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 110, 10, 0, 0 }, 10, 10, 110, 138, 248)] + [DataRow(false, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 110, 10, 0, 0 }, 10, 10, 110, 78, 138)] + [DataRow(false, Orientation.Vertical, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 110, 10, 0, 0 }, -20, 10, 110, 168, 248)] + [DataRow(false, Orientation.Horizontal, VerticalAlignment.Top, HorizontalAlignment.Left, new[] { 10, 10, 10, 10 }, new[] { 110, 10, 0, 0 }, -20, 10, 110, 108, 138)] public async Task When_AbsolutePosition_WithPadding(bool isStretch, Orientation orientation, VerticalAlignment vAlign, HorizontalAlignment hAlign, int[] padding, int[] margin, int spacing, double expectedY, double expectedX, double rec1expected, double rec2expected) { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))) + { + Assert.Inconclusive("This test is not valid on Wasm"); + return; + } + var SUT = new AutoLayout() { Orientation = orientation, @@ -229,8 +240,8 @@ public async Task When_Padding(bool isStretch, Orientation orientation, AutoLayo var border1 = new Border() { Background = new SolidColorBrush(Color.FromArgb(255, 0, 0, 255)), - Width = 350, - Height = 350, + Width = orientation is Orientation.Horizontal && isStretch ? double.NaN : 350, + Height = orientation is Orientation.Vertical && isStretch ? double.NaN : 350, }; if (isStretch) diff --git a/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Layouting.Arrange.cs b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Layouting.Arrange.cs new file mode 100644 index 000000000..34b95fa67 --- /dev/null +++ b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Layouting.Arrange.cs @@ -0,0 +1,344 @@ +using System; +using System.Runtime.CompilerServices; +using Windows.Foundation; + +#if IS_WINUI +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#else +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#endif + +namespace Uno.Toolkit.UI; + +partial class AutoLayout +{ + protected override Size ArrangeOverride(Size finalSize) + { + // Local cache of DependencyProperties + var children = Children; + var orientation = Orientation; + var justify = Justify; + var primaryAxisAlignment = PrimaryAxisAlignment; + var padding = Padding; + var spacing = Spacing.FiniteOrDefault(0d); + var isHorizontal = orientation == Orientation.Horizontal; + var borderThickness = BorderBrush is null ? default : BorderThickness; + + var borderThicknessLength = borderThickness.GetLength(orientation); + + var totalNonFilledStackedSize = 0d; + var numberOfFilledChildren = 0; + var numberOfStackedChildren = 0; + var haveStartPadding = false; + var haveEndPadding = false; + + if (_calculatedChildren is null || children.Count != _calculatedChildren.Length) + { + // Children list changed, invalidate measure and wait for next pass + InvalidateMeasure(); + return finalSize; + } + + // 1. Calculate the total size of non-filled and filled children + for (var i = 0; i < children.Count; i++) + { + var child = children[i]; + var calculatedChild = _calculatedChildren[i]; + + if (!ReferenceEquals(child, calculatedChild.Element)) + { + // Invalid calculated child, invalidate measure and wait for next pass to fix this. + InvalidateMeasure(); + return finalSize; + } + + switch (calculatedChild.Role) + { + case AutoLayoutRole.Independent: + continue; // Independent children are not stacked, skip it from the calculation + case AutoLayoutRole.Filled: + numberOfFilledChildren++; + break; + case AutoLayoutRole.Hug: + case AutoLayoutRole.Fixed: + default: + totalNonFilledStackedSize += calculatedChild.MeasuredLength; + break; + } + + numberOfStackedChildren++; + } + + var atLeastOneFilledChild = numberOfFilledChildren > 0; + + switch (primaryAxisAlignment) + { + case AutoLayoutAlignment.Start: + haveStartPadding = true; + haveEndPadding = atLeastOneFilledChild || justify == AutoLayoutJustify.SpaceBetween; + break; + case AutoLayoutAlignment.End: + haveStartPadding = atLeastOneFilledChild || justify == AutoLayoutJustify.SpaceBetween; + haveEndPadding = true; + break; + case AutoLayoutAlignment.Center: + var havePadding = atLeastOneFilledChild || justify == AutoLayoutJustify.SpaceBetween; + haveEndPadding = havePadding; + haveStartPadding = havePadding; + break; + } + + var startPadding = haveStartPadding + ? isHorizontal ? padding.Left : padding.Top + : 0d; + var endPadding = haveEndPadding + ? isHorizontal ? padding.Right : padding.Bottom + : 0d; + + var totalPaddingSize = startPadding + endPadding; + + // Available Size is the final size minus the border thickness and the padding + var availableSizeForStackedChildren = finalSize.GetLength(orientation) - (borderThicknessLength + totalPaddingSize); + EnsureZeroFloor(ref availableSizeForStackedChildren); + + // Start the offset at the border + start padding + var currentOffset = borderThickness.GetStartLength(orientation) + startPadding; + + // Calculate the defined inter-element spacing size (if any, not taking care of SpaceBetween yet) + var totalSpacingSize = spacing * (numberOfStackedChildren - 1); + + // Calculate the remaining size for each filled children (if any) + var filledChildrenSize = atLeastOneFilledChild + ? (availableSizeForStackedChildren - (totalNonFilledStackedSize + totalSpacingSize)) / numberOfFilledChildren + : 0; + + EnsureZeroFloor(ref filledChildrenSize); + + // Establish actual inter-element spacing. + var spacingOffset = justify == AutoLayoutJustify.SpaceBetween && !atLeastOneFilledChild + // When SpaceBetween is defined and there's no filled children + ? ComputeSpaceBetween(availableSizeForStackedChildren, totalNonFilledStackedSize, numberOfStackedChildren) + // Fallback to the Spacing property + : spacing; + + var primaryAxisAlignmentOffset = PrimaryAxisAlignmentOffsetSize( + primaryAxisAlignment, + availableSizeForStackedChildren, + totalNonFilledStackedSize, + atLeastOneFilledChild, + spacing, + numberOfStackedChildren); + + currentOffset += primaryAxisAlignmentOffset; + + // 3. Arrange children, one by one (in reverse order if IsReverseZIndex is true) + var isReverse = IsReverseZIndex; + var start = isReverse ? children.Count - 1 : 0; + var stop = isReverse ? -1 : children.Count; + var increment = isReverse ? -1 : 1; + for (var i = start; i != stop; i += increment) + { + var child = _calculatedChildren[i]; + + if (child.Role == AutoLayoutRole.Independent) + { + // Independent is given all the available space, + // because it's not stacking with others. + // No padding is applied to it either. + child.Element.Arrange(new Rect(default, finalSize)); + + continue; // next child, current offset remains unchanged + } + + // Calculate the length of the child (size in the panel orientation) + + // Length depends on the role of the child + var childLength = child.Role == AutoLayoutRole.Filled + ? filledChildrenSize + : child.MeasuredLength; + + var offsetRelativeToPadding = currentOffset - (startPadding + borderThicknessLength); + + if (childLength > availableSizeForStackedChildren - offsetRelativeToPadding) + { + // Child is too big, truncate it to the remaining space + childLength = availableSizeForStackedChildren - offsetRelativeToPadding; + } + + EnsureZeroFloor(ref childLength); + + // Calculate the counter length of the child (size in the other dimension than + var childCounterLength = GetCounterLength(child.Element); + + var childFinalCounterLength = isHorizontal + ? double.IsNaN(childCounterLength) ? child.Element.DesiredSize.Height : childCounterLength + : double.IsNaN(childCounterLength) ? child.Element.DesiredSize.Width : childCounterLength; + + EnsureZeroFloor(ref childFinalCounterLength); + + // Calculate the position of the child by applying the alignment instructions + var counterAlignment = GetCounterAlignment(child.Element); + + var isPrimaryAlignmentStretch = GetPrimaryAlignment(child.Element) is AutoLayoutPrimaryAlignment.Stretch; + var isCounterAlignmentStretch = counterAlignment is AutoLayoutAlignment.Stretch; + + if (child.Element is FrameworkElement frameworkElement) + { + UpdateCounterAlignmentToStretch(ref frameworkElement, isHorizontal, isPrimaryAlignmentStretch, isCounterAlignmentStretch); + } + + var haveCounterStartPadding = counterAlignment is AutoLayoutAlignment.Stretch or AutoLayoutAlignment.Start; + var counterStartPadding = haveCounterStartPadding ? (isHorizontal ? padding.Top : padding.Left) : 0; + + var haveCounterEndPadding = counterAlignment is AutoLayoutAlignment.Stretch or AutoLayoutAlignment.End; + var counterEndPadding = haveCounterEndPadding ? (isHorizontal ? padding.Bottom : padding.Right) : 0; + + var test = borderThickness.GetCounterLength(orientation); + var availableCounterLength = finalSize.GetCounterLength(orientation) - (counterStartPadding + counterEndPadding + test); + + EnsureZeroFloor(ref availableCounterLength); + + var childSize = isHorizontal + ? new Size(childLength, counterAlignment is AutoLayoutAlignment.Stretch ? availableCounterLength : childFinalCounterLength) + : new Size(counterAlignment is AutoLayoutAlignment.Stretch ? availableCounterLength : childFinalCounterLength, childLength); + + ApplyMinMaxValues(child.Element, orientation, ref childSize); + + var counterAlignmentOffset = + ComputeCounterAlignmentOffset(counterAlignment, childFinalCounterLength, availableCounterLength, counterStartPadding, borderThickness.GetCounterStartLength(orientation)); + + var childOffsetPosition = new Point( + isHorizontal ? currentOffset : counterAlignmentOffset, + isHorizontal ? counterAlignmentOffset : currentOffset); + + // Arrange the child to its final position, determined by the calculated offset and size + child.Element.Arrange(new Rect(childOffsetPosition, childSize)); + + // Increment the offset for the next child + currentOffset += (childLength + spacingOffset); + } + + return finalSize; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UpdateCounterAlignmentToStretch(ref FrameworkElement frameworkElement, bool isHorizontal, bool isPrimaryAlignmentStretch, bool isCounterAlignmentStretch) + { + if (isHorizontal) + { + frameworkElement.HorizontalAlignment = isPrimaryAlignmentStretch ? HorizontalAlignment.Stretch : frameworkElement.HorizontalAlignment; + frameworkElement.VerticalAlignment = isCounterAlignmentStretch ? VerticalAlignment.Stretch : frameworkElement.VerticalAlignment; + } + else + { + frameworkElement.VerticalAlignment = isPrimaryAlignmentStretch ? VerticalAlignment.Stretch : frameworkElement.VerticalAlignment; + frameworkElement.HorizontalAlignment = isCounterAlignmentStretch ? HorizontalAlignment.Stretch : frameworkElement.HorizontalAlignment; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ApplyMinMaxValues(UIElement element, Orientation orientation, ref Size desiredSize) + { + if (element is not FrameworkElement frameworkElement) + { + return; + } + + // Cache local values to avoid multiple calls to the DP system + var width = frameworkElement.Width; + var height = frameworkElement.Height; + var maxWidth = frameworkElement.MaxWidth; + var maxHeight = frameworkElement.MaxHeight; + var minWidth = frameworkElement.MinWidth; + var minHeight = frameworkElement.MinHeight; + + var primaryLength = GetPrimaryLength(element); + var counterLength = GetCounterLength(element); + + // Apply primaryLength and counterLength constraints, if defined + if (orientation is Orientation.Horizontal) + { + if (!double.IsNaN(primaryLength)) desiredSize.Width = primaryLength; + if (!double.IsNaN(counterLength)) desiredSize.Height = counterLength; + } + else + { + if (!double.IsNaN(primaryLength)) desiredSize.Height = primaryLength; + if (!double.IsNaN(counterLength)) desiredSize.Width = counterLength; + } + + // Apply Width and Height constraints, if defined + if (!double.IsNaN(width)) desiredSize.Width = width; + if (!double.IsNaN(height)) desiredSize.Height = height; + + // Apply MaxWidth and MaxHeight constraints, if defined + if (!double.IsNaN(maxWidth) && desiredSize.Width > maxWidth) desiredSize.Width = maxWidth; + if (!double.IsNaN(maxHeight) && desiredSize.Height > maxHeight) desiredSize.Height = maxHeight; + + // Apply MinWidth and MinHeight constraints + if (!double.IsNaN(minWidth) && desiredSize.Width < minWidth) desiredSize.Width = minWidth; + if (!double.IsNaN(minHeight) && desiredSize.Height < minHeight) desiredSize.Height = minHeight; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double ComputeCounterAlignmentOffset( + AutoLayoutAlignment autoLayoutAlignment, + double childCounterLength, + double availableCounterLength, + double counterStartPadding, + double counterBorderThickness) + { + var alignmentOffsetSize = availableCounterLength - childCounterLength; + + var calculatedOffset = autoLayoutAlignment switch + { + AutoLayoutAlignment.End => alignmentOffsetSize, + AutoLayoutAlignment.Center when alignmentOffsetSize > 0 => alignmentOffsetSize / 2, + _ => 0 + }; + + return calculatedOffset + counterStartPadding + counterBorderThickness; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double ComputeSpaceBetween( + double availableSizeForStackedChildren, + double totalNonFilledStackedSize, + int numberOfStackedChildren) + { + var availableSizeForStackedSpaceBetween = availableSizeForStackedChildren - totalNonFilledStackedSize; + + EnsureZeroFloor(ref availableSizeForStackedSpaceBetween); + + return availableSizeForStackedSpaceBetween / (numberOfStackedChildren - 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double PrimaryAxisAlignmentOffsetSize( + AutoLayoutAlignment autoLayoutAlignment, + double availableSizeForStackedChildren, + double totalNonFilledStackedSize, + bool atLeastOneFilledChild, + double spacing, + int numberOfStackedChildren) + { + if (atLeastOneFilledChild || autoLayoutAlignment is AutoLayoutAlignment.Start) + { + return 0; + } + + var totalSpacingSize = spacing * (numberOfStackedChildren - 1); + + var alignmentOffsetSize = availableSizeForStackedChildren - + (totalNonFilledStackedSize + totalSpacingSize); + + return autoLayoutAlignment switch + { + AutoLayoutAlignment.End => alignmentOffsetSize, + AutoLayoutAlignment.Center when alignmentOffsetSize > 0 => alignmentOffsetSize / 2, + _ => 0 + }; + } +} diff --git a/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Layouting.Measure.cs b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Layouting.Measure.cs new file mode 100644 index 000000000..408110b41 --- /dev/null +++ b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Layouting.Measure.cs @@ -0,0 +1,452 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Windows.Foundation; +#if IS_WINUI +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#else +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#endif + +namespace Uno.Toolkit.UI; + +partial class AutoLayout +{ + protected override Size MeasureOverride(Size availableSize) + { + // - Each children can be one of the following role + // 1. Hug: The child is measured according to its own constraints, using its desired size. + // 2. Fixed: The child is measured according to its own constraints, using its fixed size. + // 3. Filled: The child will be given the remaining space, evenly distributed through all + // other "filled" children. + // 4. Independent: The child will follow the z-axis of its position in the children list, + // but won't be affected by the other children. + + // -1. Local cache of DependencyProperties + var orientation = Orientation; + var children = Children; + var justify = Justify; + var spacing = Spacing.FiniteOrDefault(0d); + var borderThickness = BorderBrush is null ? default : BorderThickness; + + // 0. Establish start remaining sizes to dispatch, according to the orientation + var remainingSize = availableSize.GetLength(orientation) - borderThickness.GetLength(orientation); + var availableCounterSize = availableSize.GetCounterLength(orientation) - borderThickness.GetCounterLength(orientation); + var desiredCounterSize = 0d; + + // 1. Establish the list of children roles + var filledAsHug = double.IsPositiveInfinity(remainingSize); + var (numberOfStackedChildren, atLeastOneFilledChild) = EstablishChildrenRoles(children, filledAsHug, orientation); + + // 2. Remove applicable padding and spacing from the remaining size + var paddingSize = Padding.GetLength(orientation); + Decrement(ref remainingSize, paddingSize); + + // 3. Measure fixed children + MeasureFixedChildren(orientation, availableCounterSize, ref remainingSize, ref desiredCounterSize); + + // 4. Establish the total spacing size, if applicable + var totalSpacingSize = 0d; + if (numberOfStackedChildren > 0 + && justify != AutoLayoutJustify.SpaceBetween + && spacing != 0 + && spacing < double.PositiveInfinity) + { + totalSpacingSize = (numberOfStackedChildren - 1) * spacing; + Decrement(ref remainingSize, totalSpacingSize); + } + + // 5. Calculate the size of Hug children + MeasureHugChildren(orientation, availableCounterSize, ref remainingSize, ref desiredCounterSize); + + // 6. Calculate the size of Filled children + MeasureFilledChildren(orientation, availableCounterSize, ref remainingSize, ref desiredCounterSize); + + // 7. Measure independent children, independently of the remaining size + var independentDesiredSize = MeasureIndependentChildren(availableSize, borderThickness, orientation, ref desiredCounterSize); + + EnsureZeroFloor(ref desiredCounterSize); + + // 8. Calculate the desired size of the panel + Size desiredSize; + + if ((atLeastOneFilledChild + || justify == AutoLayoutJustify.SpaceBetween) + && remainingSize is > 0 and < double.PositiveInfinity) + { + // 8a. Calculated the spacing size, when justify is SpaceBetween or there's at least one filled child + + // We don't need to calculate the spacing since it's the remaining size + // and the final spacing will be calculated in the arrange pass. + desiredSize = orientation switch + { + Orientation.Horizontal => new Size(availableSize.Width, desiredCounterSize), + Orientation.Vertical => new Size(desiredCounterSize, availableSize.Height), + _ => throw new ArgumentOutOfRangeException(), + }; + } + else + { + // 8b. Calculate the desired size of the panel, when there's at least one filled child + var stackedChildrenDesiredSize = + _calculatedChildren! + .Select(c => c.MeasuredLength) + .Sum() + + totalSpacingSize; + + var desiredSizeInPrimaryOrientation = Math.Max(independentDesiredSize, stackedChildrenDesiredSize); + + EnsureZeroFloor(ref desiredSizeInPrimaryOrientation); + + desiredSize = orientation switch + { + Orientation.Horizontal => new Size( + width: desiredSizeInPrimaryOrientation + paddingSize, + height: desiredCounterSize + Padding.GetCounterLength(orientation)), + Orientation.Vertical => new Size( + width: desiredCounterSize + Padding.GetCounterLength(orientation), + height: desiredSizeInPrimaryOrientation + paddingSize), + _ => throw new ArgumentOutOfRangeException(), + }; + } + + // 9. Apply Width and Height constraints, if any + ApplyMinMaxValues(ref desiredSize); + + return desiredSize; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ApplyMinMaxValues(ref Size desiredSize) + { + // Cache local values to avoid multiple calls to the DP system + var width = Width; + var height = Height; + var maxWidth = MaxWidth; + var maxHeight = MaxHeight; + var minWidth = MinWidth; + var minHeight = MinHeight; + + // Apply Width and Height constraints, if defined + if (!double.IsNaN(width)) desiredSize.Width = width; + if (!double.IsNaN(height)) desiredSize.Height = height; + + // Apply MaxWidth and MaxHeight constraints, if defined + if (!double.IsNaN(maxWidth) && desiredSize.Width > maxWidth) desiredSize.Width = maxWidth; + if (!double.IsNaN(maxHeight) && desiredSize.Height > maxHeight) desiredSize.Height = maxHeight; + + // Apply MinWidth and MinHeight constraints + if (!double.IsNaN(minWidth) && desiredSize.Width < minWidth) desiredSize.Width = minWidth; + if (!double.IsNaN(minHeight) && desiredSize.Height < minHeight) desiredSize.Height = minHeight; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private (int, bool) EstablishChildrenRoles(IList children, bool filledAsHug, Orientation orientation) + { + if(_calculatedChildren == null || _calculatedChildren.Length != children.Count) + { + _calculatedChildren = new CalculatedChildren[children.Count]; + } + + var numberOfStackedChildren = children.Count; + var atLeastOneFilledChild = false; + + for (var i = 0; i < children.Count; i++) + { + var child = children[i]; + + AutoLayoutRole role; + var length = 0d; + + if (GetIsIndependentLayout(child)) + { + role = AutoLayoutRole.Independent; + numberOfStackedChildren--; + } + else if (GetPrimaryAlignment(child) == AutoLayoutPrimaryAlignment.Stretch && !filledAsHug) + { + atLeastOneFilledChild = true; + role = AutoLayoutRole.Filled; + } + else if(GetPrimaryLength(child) is var l and >= 0 and < double.PositiveInfinity) + { + role = AutoLayoutRole.Fixed; + length = l; + } + else + { + var size = child is FrameworkElement frameworkElement ? + Math.Max(frameworkElement.GetLength(orientation), frameworkElement.GetMinLength(orientation)) : + -1; + + if (size >= 0) + { + role = AutoLayoutRole.Fixed; + length = size; + } + else + { + role = AutoLayoutRole.Hug; + } + } + + _calculatedChildren[i] = new CalculatedChildren(child, role, length); + } + + return (numberOfStackedChildren, atLeastOneFilledChild); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MeasureFixedChildren( + Orientation orientation, + double availableCounterSize, + ref double remainingSize, + ref double desiredCounterSize) + { + for (var i = 0; i < _calculatedChildren!.Length; i++) + { + var calculatedChild = _calculatedChildren[i]; + + if (calculatedChild.Role != AutoLayoutRole.Fixed) + { + continue; + } + + var fixedSize = calculatedChild.MeasuredLength; + + MeasureChild( + calculatedChild.Element, + orientation, + fixedSize, // The available size for the child is its defined fixed size + availableCounterSize, + ref desiredCounterSize); + + Decrement(ref remainingSize, fixedSize); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MeasureHugChildren( + Orientation orientation, + double availableCounterSize, + ref double remainingSize, + ref double desiredCounterSize) + { + for (var i = 0; i < _calculatedChildren!.Length; i++) + { + var calculatedChild = _calculatedChildren![i]; + + if (calculatedChild.Role != AutoLayoutRole.Hug) + { + continue; + } + + var desiredSize = MeasureChild( + calculatedChild.Element, + orientation, + double.PositiveInfinity, // We don't want the child to limit its own desired size to available one + availableCounterSize, + ref desiredCounterSize); + + calculatedChild.MeasuredLength = desiredSize; + + // We don't care about its desired size, because it's fixed, so we're using fixed size + Decrement(ref remainingSize, desiredSize); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MeasureFilledChildren( + Orientation orientation, + double availableCounterSize, + ref double remainingSize, + ref double desiredCounterSize) + { + if (double.IsInfinity(remainingSize)) + { + return false; // Filled children act as Hug children when there's infinite available size + } + + // Calculated the number of filled children using a for loop, to avoid allocating an array + var filledChildrenCount = 0; + for (var i = 0; i < _calculatedChildren!.Length; i++) + { + var child = _calculatedChildren![i]; + + if (child.Role == AutoLayoutRole.Filled) + { + filledChildrenCount++; + } + } + + if (filledChildrenCount <= 0) + { + return false; // no filled children + } + + var filledSize = remainingSize / filledChildrenCount; + EnsureZeroFloor(ref filledSize); + + for (var i = 0; i < _calculatedChildren!.Length; i++) + { + var child = _calculatedChildren![i]; + if (child.Role != AutoLayoutRole.Filled) + { + continue; + } + + MeasureChild(child.Element, orientation, filledSize, availableCounterSize, ref desiredCounterSize); + + child.MeasuredLength = filledSize; + } + + return true; // at least one filled child + } + + private static double MeasureChild( + UIElement child, + Orientation orientation, + double availableSize, + double availableCounterSize, + ref double desiredCounterSize) + { + var availableSizeForChild = orientation == Orientation.Horizontal + ? new Size(availableSize, availableCounterSize) + : new Size(availableCounterSize, availableSize); + + child.Measure(availableSizeForChild); + + double desiredSize; + if (orientation == Orientation.Horizontal) + { + desiredSize = child.DesiredSize.Width; + desiredCounterSize = Math.Max(desiredCounterSize, child.DesiredSize.Height); + } + else + { + desiredSize = child.DesiredSize.Height; + desiredCounterSize = Math.Max(desiredCounterSize, child.DesiredSize.Width); + } + + EnsureZeroFloor(ref desiredSize); + return desiredSize; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Decrement(ref double value, double decrement) + { + if (double.IsInfinity(value)) + { + return; + } + + value -= decrement; + EnsureZeroFloor(ref value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EnsureZeroFloor(ref double sizeValue) + { + if (sizeValue < 0) + { + sizeValue = 0; + } + } + + private double MeasureIndependentChildren( + Size availableSize, + Thickness borderThickness, + Orientation orientation, + ref double desiredCounterSize) + { + var resultSize = new Size(); + var anyIndependent = false; + + // Actual available size for independent children is the available size minus the border thickness + availableSize = new Size( + availableSize.Width - (borderThickness.Left + borderThickness.Right), + availableSize.Height - (borderThickness.Top + borderThickness.Bottom)); + + for (var i = 0; i < _calculatedChildren!.Length; i++) + { + var child = _calculatedChildren[i]; + + if (child.Role != AutoLayoutRole.Independent) + { + continue; + } + + anyIndependent = true; + + child.Element.Measure(availableSize); + var desiredSize = child.Element.DesiredSize; + + // Adjust resulting desired size to the largest of the children + resultSize.Width = Math.Max(resultSize.Width, desiredSize.Width); + resultSize.Height = Math.Max(resultSize.Height, desiredSize.Height); + } + + if (!anyIndependent) + { + return -1; + } + + desiredCounterSize = Math.Max( + desiredCounterSize, + orientation switch + { + Orientation.Horizontal => resultSize.Height, + Orientation.Vertical => resultSize.Width, + _ => throw new NotSupportedException() + }); + + return orientation switch + { + Orientation.Horizontal => resultSize.Width, + Orientation.Vertical => resultSize.Height, + _ => throw new NotSupportedException() + }; + } + + private CalculatedChildren[]? _calculatedChildren; + + private class CalculatedChildren + { + public CalculatedChildren(UIElement element, AutoLayoutRole role, double measuredLength = 0d) + { + Element = element; + Role = role; + MeasuredLength = measuredLength; + } + + internal UIElement Element { get; } + + /// + /// How the element is layouted in the AutoLayout. + /// + /// + /// When the element is absolutely positioned (Independent), it is not stacked with others. + /// + internal AutoLayoutRole Role { get; } + + /// + /// Measured length of the element, when it is stacked. + /// + /// + /// Will be zero when the element is absolutely positioned, because it is not stacked + /// with others. + /// + internal double MeasuredLength { get; set; } + } + + private enum AutoLayoutRole : byte + { + Hug, + Fixed, + Filled, + Independent + } +} diff --git a/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Properties.cs b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Properties.cs new file mode 100644 index 000000000..1c415dd6f --- /dev/null +++ b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.Properties.cs @@ -0,0 +1,195 @@ +using System.Diagnostics.CodeAnalysis; + +#if IS_WINUI +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#else +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#endif + +namespace Uno.Toolkit.UI; + +partial class AutoLayout +{ + // -- IsReverseZIndex DependencyProperty -- + public static readonly DependencyProperty IsReverseZIndexProperty = DependencyProperty.Register( + nameof(IsReverseZIndex), + typeof(bool), + typeof(AutoLayout), + new PropertyMetadata(default(bool), propertyChangedCallback: InvalidateArrangeCallback)); + + public bool IsReverseZIndex + { + get => (bool)GetValue(IsReverseZIndexProperty); + set => SetValue(IsReverseZIndexProperty, value); + } + + // -- Orientation DependencyProperty -- + public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(AutoLayout), + new PropertyMetadata(default(Orientation), propertyChangedCallback: InvalidateMeasureCallback)); + + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + // -- Spacing DependencyProperty -- + public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register( + nameof(Spacing), + typeof(double), + typeof(AutoLayout), + new PropertyMetadata(default(double), propertyChangedCallback: InvalidateMeasureCallback)); + + public double Spacing + { + get => (double)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + // -- Justify DependencyProperty -- + public static readonly DependencyProperty JustifyProperty = DependencyProperty.Register( + nameof(Justify), + typeof(AutoLayoutJustify), + typeof(AutoLayout), + new PropertyMetadata(default(AutoLayoutJustify), propertyChangedCallback: InvalidateArrangeCallback)); + + public AutoLayoutJustify Justify + { + get => (AutoLayoutJustify)GetValue(JustifyProperty); + set => SetValue(JustifyProperty, value); + } + + // -- PrimaryAxisAlignment DependencyProperty -- + public static readonly DependencyProperty PrimaryAxisAlignmentProperty = DependencyProperty.Register( + nameof(PrimaryAxisAlignment), + typeof(AutoLayoutAlignment), + typeof(AutoLayout), + new PropertyMetadata(default(AutoLayoutAlignment), propertyChangedCallback: InvalidateArrangeCallback)); + + public AutoLayoutAlignment PrimaryAxisAlignment + { + get => (AutoLayoutAlignment)GetValue(PrimaryAxisAlignmentProperty); + set => SetValue(PrimaryAxisAlignmentProperty, value); + } + + // -- PrimaryAlignment Attached Property -- + [DynamicDependency(nameof(GetPrimaryAlignment))] + public static readonly DependencyProperty PrimaryAlignmentProperty = DependencyProperty.RegisterAttached( + "PrimaryAlignment", + typeof(AutoLayoutPrimaryAlignment), + typeof(AutoLayout), + new PropertyMetadata(default(AutoLayoutPrimaryAlignment), propertyChangedCallback: InvalidateArrangeCallback)); + + [DynamicDependency(nameof(GetPrimaryAlignment))] + public static void SetPrimaryAlignment(DependencyObject element, AutoLayoutPrimaryAlignment value) + { + element.SetValue(PrimaryAlignmentProperty, value); + } + + [DynamicDependency(nameof(GetPrimaryAlignment))] + public static AutoLayoutPrimaryAlignment GetPrimaryAlignment(DependencyObject element) + { + return (AutoLayoutPrimaryAlignment)element.GetValue(PrimaryAlignmentProperty); + } + + // -- CounterAlignment Attached Property -- + [DynamicDependency(nameof(GetCounterAlignment))] + public static readonly DependencyProperty CounterAlignmentProperty = DependencyProperty.RegisterAttached( + "CounterAlignment", + typeof(AutoLayoutAlignment), + typeof(AutoLayout), + new PropertyMetadata(AutoLayoutAlignment.Stretch, propertyChangedCallback: InvalidateArrangeCallback)); + + [DynamicDependency(nameof(GetCounterAlignment))] + public static void SetCounterAlignment(DependencyObject element, AutoLayoutAlignment value) + { + element.SetValue(CounterAlignmentProperty, value); + } + + [DynamicDependency(nameof(SetCounterAlignment))] + public static AutoLayoutAlignment GetCounterAlignment(DependencyObject element) + { + return (AutoLayoutAlignment)element.GetValue(CounterAlignmentProperty); + } + + // -- IsIndependentLayout Attached Property -- + [DynamicDependency(nameof(GetIsIndependentLayout))] + public static readonly DependencyProperty IsIndependentLayoutProperty = DependencyProperty.RegisterAttached( + "IsIndependentLayout", + typeof(bool), + typeof(AutoLayout), + new PropertyMetadata(default(bool), propertyChangedCallback: InvalidateMeasureCallback)); + + [DynamicDependency(nameof(GetIsIndependentLayout))] + public static void SetIsIndependentLayout(DependencyObject element, bool value) + { + element.SetValue(IsIndependentLayoutProperty, value); + } + + [DynamicDependency(nameof(SetIsIndependentLayout))] + public static bool GetIsIndependentLayout(DependencyObject element) + { + return (bool)element.GetValue(IsIndependentLayoutProperty); + } + + // -- PrimaryLength Attached Property -- + [DynamicDependency(nameof(GetPrimaryLength))] + public static readonly DependencyProperty PrimaryLengthProperty = DependencyProperty.RegisterAttached( + "PrimaryLength", + typeof(double), + typeof(AutoLayout), + new PropertyMetadata(double.NaN, propertyChangedCallback: InvalidateMeasureCallback)); + + [DynamicDependency(nameof(GetPrimaryLength))] + public static void SetPrimaryLength(DependencyObject element, double value) + { + element.SetValue(PrimaryLengthProperty, value); + } + + [DynamicDependency(nameof(SetPrimaryLength))] + public static double GetPrimaryLength(DependencyObject element) + { + return (double)element.GetValue(PrimaryLengthProperty); + } + + // -- CounterLength Attached Property -- + [DynamicDependency(nameof(GetCounterLength))] + public static readonly DependencyProperty CounterLengthProperty = DependencyProperty.RegisterAttached( + "CounterLength", + typeof(double), + typeof(AutoLayout), + new PropertyMetadata(double.NaN, propertyChangedCallback: InvalidateMeasureCallback)); + + [DynamicDependency(nameof(GetCounterLength))] + public static void SetCounterLength(DependencyObject element, double value) + { + element.SetValue(CounterLengthProperty, value); + } + + [DynamicDependency(nameof(SetCounterLength))] + public static double GetCounterLength(DependencyObject element) + { + return (double)element.GetValue(CounterLengthProperty); + } + + private static void InvalidateMeasureCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is UIElement element) + { + element.InvalidateMeasure(); + } + } + + private static void InvalidateArrangeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is UIElement element) + { + element.InvalidateArrange(); + } + } +} diff --git a/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.cs b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.cs index ddf60fd30..59002548c 100644 --- a/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.cs +++ b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.cs @@ -1,606 +1,11 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -#if IS_WINUI -using Microsoft.UI.Xaml; +#if IS_WINUI using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Markup; -using Microsoft.UI.Xaml.Media; #else -using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Markup; -using Windows.UI.Xaml.Media; #endif -namespace Uno.Toolkit.UI -{ - [ContentProperty(Name = nameof(Children))] - [TemplatePart(Name = PART_RootGrid, Type = typeof(Grid))] - public partial class AutoLayout : Control - { - private Grid? _grid; - private const string PART_RootGrid = nameof(PART_RootGrid); - - public AutoLayout() - { - DefaultStyleKey = typeof(AutoLayout); - Children = new AutoLayoutChildren(); - - Loaded += OnLoaded; - - this.RegisterDisposablePropertyChangedCallback(HorizontalAlignmentProperty, (snd, e) => UpdateAutoLayout()); - this.RegisterDisposablePropertyChangedCallback(VerticalAlignmentProperty, (snd, e) => UpdateAutoLayout()); - this.RegisterDisposablePropertyChangedCallback(PaddingProperty, (snd, e) => UpdateAutoLayout()); - } - - protected override void OnApplyTemplate() - { - _grid = GetTemplateChild(PART_RootGrid) as Grid; - - base.OnApplyTemplate(); - - UpdateAutoLayout(); - } - - private static void OnLoaded(object sender, RoutedEventArgs e) - { - (sender as AutoLayout)?.UpdateAutoLayout(); - } - - public static readonly DependencyProperty IsReverseZIndexProperty = DependencyProperty.Register( - "IsReverseZIndex", typeof(bool), typeof(AutoLayout), new PropertyMetadata(default(bool), propertyChangedCallback: UpdateCallback)); - - public bool IsReverseZIndex - { - get => (bool)GetValue(IsReverseZIndexProperty); - set => SetValue(IsReverseZIndexProperty, value); - } - - private bool IsHorizontalHug => HorizontalAlignment is not HorizontalAlignment.Stretch && Width is double.NaN; - - private bool IsVerticalHug => VerticalAlignment is not VerticalAlignment.Stretch && Height is double.NaN; - - public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( - "Orientation", typeof(Orientation), typeof(AutoLayout), new PropertyMetadata(default(Orientation), propertyChangedCallback: UpdateCallback)); - - public Orientation Orientation - { - get => (Orientation)GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register( - "Spacing", typeof(double), typeof(AutoLayout), new PropertyMetadata(default(double), propertyChangedCallback: UpdateCallback)); - - public double Spacing - { - get => (double)GetValue(SpacingProperty); - set => SetValue(SpacingProperty, value); - } - - public static readonly DependencyProperty JustifyProperty = DependencyProperty.Register( - "Justify", typeof(AutoLayoutJustify), typeof(AutoLayout), new PropertyMetadata(default(AutoLayoutJustify), propertyChangedCallback: UpdateCallback)); - - public AutoLayoutJustify Justify - { - get => (AutoLayoutJustify)GetValue(JustifyProperty); - set => SetValue(JustifyProperty, value); - } - - public static readonly DependencyProperty ChildrenProperty = DependencyProperty.Register( - "Children", typeof(AutoLayoutChildren), typeof(AutoLayout), new PropertyMetadata(default(AutoLayoutChildren), propertyChangedCallback: UpdateCallback)); - - public AutoLayoutChildren Children - { - get => (AutoLayoutChildren)GetValue(ChildrenProperty); - set => SetValue(ChildrenProperty, value); - } - - [DynamicDependency(nameof(GetPrimaryAlignment))] - public static readonly DependencyProperty PrimaryAlignmentProperty = DependencyProperty.RegisterAttached( - "PrimaryAlignment", - typeof(AutoLayoutPrimaryAlignment), - typeof(AutoLayout), - new PropertyMetadata(default(AutoLayoutPrimaryAlignment), propertyChangedCallback: UpdateAttachedCallback)); - - [DynamicDependency(nameof(GetPrimaryAlignment))] - public static void SetPrimaryAlignment(DependencyObject element, AutoLayoutPrimaryAlignment value) - { - element.SetValue(PrimaryAlignmentProperty, value); - } - - [DynamicDependency(nameof(GetPrimaryAlignment))] - public static AutoLayoutPrimaryAlignment GetPrimaryAlignment(DependencyObject element) - { - return (AutoLayoutPrimaryAlignment)element.GetValue(PrimaryAlignmentProperty); - } - - [DynamicDependency(nameof(GetCounterAlignment))] - public static readonly DependencyProperty CounterAlignmentProperty = DependencyProperty.RegisterAttached( - "CounterAlignment", - typeof(AutoLayoutAlignment), - typeof(AutoLayout), - new PropertyMetadata(AutoLayoutAlignment.Stretch, propertyChangedCallback: UpdateAttachedCallback)); - - [DynamicDependency(nameof(GetCounterAlignment))] - public static void SetCounterAlignment(DependencyObject element, AutoLayoutAlignment value) - { - element.SetValue(CounterAlignmentProperty, value); - } - - [DynamicDependency(nameof(SetCounterAlignment))] - public static AutoLayoutAlignment GetCounterAlignment(DependencyObject element) - { - return (AutoLayoutAlignment)element.GetValue(CounterAlignmentProperty); - } - - [DynamicDependency(nameof(GetIsIndependentLayout))] - public static readonly DependencyProperty IsIndependentLayoutProperty = DependencyProperty.RegisterAttached( - "IsIndependentLayout", typeof(bool), typeof(AutoLayout), new PropertyMetadata(default(bool), propertyChangedCallback: UpdateAttachedCallback)); - - [DynamicDependency(nameof(GetIsIndependentLayout))] - public static void SetIsIndependentLayout(DependencyObject element, bool value) - { - element.SetValue(IsIndependentLayoutProperty, value); - } - - [DynamicDependency(nameof(SetIsIndependentLayout))] - public static bool GetIsIndependentLayout(DependencyObject element) - { - return (bool)element.GetValue(IsIndependentLayoutProperty); - } - - [DynamicDependency(nameof(GetPrimaryLength))] - public static readonly DependencyProperty PrimaryLengthProperty = DependencyProperty.RegisterAttached( - "PrimaryLength", typeof(double), typeof(AutoLayout), new PropertyMetadata(default(double), propertyChangedCallback: UpdateAttachedCallback)); - - [DynamicDependency(nameof(GetPrimaryLength))] - public static void SetPrimaryLength(DependencyObject element, double value) - { - element.SetValue(PrimaryLengthProperty, value); - } - - [DynamicDependency(nameof(SetPrimaryLength))] - public static double GetPrimaryLength(DependencyObject element) - { - return (double)element.GetValue(PrimaryLengthProperty); - } - - [DynamicDependency(nameof(GetCounterLength))] - public static readonly DependencyProperty CounterLengthProperty = DependencyProperty.RegisterAttached( - "CounterLength", typeof(double), typeof(AutoLayout), new PropertyMetadata(default(double), propertyChangedCallback: UpdateAttachedCallback)); - - [DynamicDependency(nameof(GetCounterLength))] - public static void SetCounterLength(DependencyObject element, double value) - { - element.SetValue(CounterLengthProperty, value); - } - - [DynamicDependency(nameof(SetCounterLength))] - public static double GetCounterLength(DependencyObject element) - { - return (double)element.GetValue(CounterLengthProperty); - } - - public static readonly DependencyProperty PrimaryAxisAlignmentProperty = DependencyProperty.Register( - "PrimaryAxisAlignment", - typeof(AutoLayoutAlignment), - typeof(AutoLayout), - new PropertyMetadata(default(AutoLayoutAlignment), propertyChangedCallback: UpdateCallback)); - - public AutoLayoutAlignment PrimaryAxisAlignment - { - get => (AutoLayoutAlignment)GetValue(PrimaryAxisAlignmentProperty); - set => SetValue(PrimaryAxisAlignmentProperty, value); - } - - private static void UpdateCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is AutoLayout al) - { - al.UpdateAutoLayout(); - } - } - - private static void UpdateAttachedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is FrameworkElement fe) - { - if (fe.Parent is AutoLayout al) - { - al.UpdateAutoLayout(); - } - } - } - - private void UpdateAutoLayout() - { - if (_grid == null || IsLoaded is false) - { - return; - } - - var children = Children; - var childrenCount = children.Count; - - if (childrenCount == 0) - { - _grid.Children.Clear(); - _grid.RowDefinitions.Clear(); - _grid.ColumnDefinitions.Clear(); - return; - } - - var padding = Padding; - var isVertical = Orientation is Orientation.Vertical; - var spacing = Spacing; - var hasSpace = Spacing != 0 || Justify == AutoLayoutJustify.SpaceBetween; - var hasSpaceBetween = Justify == AutoLayoutJustify.SpaceBetween; - var isSpacing = spacing != 0 && !hasSpaceBetween; - - var childrenCounterALignment = children.Any() ? GetCounterAlignment(children.First()) : AutoLayoutAlignment.Start; - - bool atLeastOneChildFillAvailableSpaceInPrimaryAxis = children.Where(child => GetPrimaryAlignment(child) is AutoLayoutPrimaryAlignment.Stretch).Any(); - bool atLeastOneChildFillAvailableSpaceInCounterAxis = children.Where(child => GetCounterAlignment(child) is AutoLayoutAlignment.Stretch).Any(); - - var childrenVerticallAlignment = Orientation == Orientation.Vertical ? - atLeastOneChildFillAvailableSpaceInPrimaryAxis ? VerticalAlignment.Stretch : PrimaryAxisAlignment.ToVerticalAlignment() : - atLeastOneChildFillAvailableSpaceInCounterAxis ? VerticalAlignment.Stretch : childrenCounterALignment.ToVerticalAlignment(); - var childrenHorizontalAlignment = Orientation == Orientation.Vertical ? - atLeastOneChildFillAvailableSpaceInCounterAxis ? HorizontalAlignment.Stretch : childrenCounterALignment.ToHorizontalAlignment() : - atLeastOneChildFillAvailableSpaceInPrimaryAxis ? HorizontalAlignment.Stretch : PrimaryAxisAlignment.ToHorizontalAlignment(); - - var independentLayoutCount = children.Where(child => GetIsIndependentLayout(child)).Count(); - var hasIndependentLayout = independentLayoutCount > 0; +namespace Uno.Toolkit.UI; - var hasPadding = !padding.Equals(default(Thickness)); - - var gridRowIndexOffSet = 0; - var gridColumnIndexOffSet = 0; - - var shouldAddInBetweenRow = (hasSpaceBetween && atLeastOneChildFillAvailableSpaceInPrimaryAxis is false) || spacing > 0; - var gridDefinitionsCount = shouldAddInBetweenRow ? ((childrenCount - independentLayoutCount) * 2) - 1 : (childrenCount - independentLayoutCount); - - var isPrimaryAxisAlignmentCenterOrEnd = hasIndependentLayout && PrimaryAxisAlignment is AutoLayoutAlignment.Center or AutoLayoutAlignment.End; - - // Inject & Move elements in the inner grid - for (var i = 0; i < childrenCount; i++) - { - var child = children[IsReverseZIndex ? childrenCount-i-1 : i]; - - if (child.Parent != null) - { - // We need to move instead of adding the element - var currentIndexInGrid = _grid.Children.IndexOf(child); - if (currentIndexInGrid == i) - { - // nothing to do, already at right place - continue; - } - - _grid.Children.Move((uint)currentIndexInGrid, (uint)i); - } - else - { - if (_grid.Children.Count > i) - { - _grid.Children.Insert(i, child); - } - else - { - _grid.Children.Add(child); - } - } - } - - // remove any extra children on grid - while (childrenCount < _grid.Children.Count) - { - _grid.Children.RemoveAt(_grid.Children.Count - 1); - } - - // Set inter-element spacing - if (hasSpace) - { - _grid.ClearValue(Grid.ColumnSpacingProperty); - _grid.ClearValue(Grid.RowSpacingProperty); - } - - if (isVertical) - { - _grid.ColumnDefinitions.Clear(); - _grid.RowDefinitions.Clear(); - // Ensure definitions is of right size - while (_grid.RowDefinitions.Count < gridDefinitionsCount) - { - _grid.RowDefinitions.Add(new RowDefinition()); - } - } - else - { - _grid.RowDefinitions.Clear(); - _grid.ColumnDefinitions.Clear(); - - // Ensure definitions is of right size - while (_grid.ColumnDefinitions.Count < gridDefinitionsCount) - { - _grid.ColumnDefinitions.Add(new ColumnDefinition()); - } - } - - if (hasPadding) - { - _grid.Padding = new Thickness(0); - - if (IsVerticalHug || childrenVerticallAlignment is VerticalAlignment.Top or VerticalAlignment.Stretch || hasSpaceBetween) - { - var topPaddingSize = spacing < 0 ? padding.Top - spacing : padding.Top; - _grid.RowDefinitions.Insert(0, new RowDefinition() { Height = new GridLength(topPaddingSize), MaxHeight = topPaddingSize }); - gridRowIndexOffSet += 1; - } - - if (IsHorizontalHug || childrenHorizontalAlignment is HorizontalAlignment.Left or HorizontalAlignment.Stretch || hasSpaceBetween) - { - var firstColumnWidthSize = spacing < 0 ? padding.Left - spacing : padding.Left; - _grid.ColumnDefinitions.Insert(0, new ColumnDefinition() { Width = new GridLength(firstColumnWidthSize), MaxWidth = firstColumnWidthSize }); - gridColumnIndexOffSet += 1; - } - } - - if (isVertical) - { - - if (hasPadding) - { - _grid.ColumnDefinitions.Insert(gridColumnIndexOffSet, new ColumnDefinition() { Width = new GridLength(1, IsHorizontalHug ? GridUnitType.Auto : GridUnitType.Star) }); - } - - var rawChildIndex = 0; - - if (hasSpaceBetween is false && atLeastOneChildFillAvailableSpaceInPrimaryAxis is false && isPrimaryAxisAlignmentCenterOrEnd) - { - _grid.RowDefinitions.Insert(gridRowIndexOffSet, new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) }); - gridRowIndexOffSet += 1; - } - // Process children - for (var i = 0; i < childrenCount; i++) - { - var child = children[i]; - - if (GetIsIndependentLayout(child)) - { - Grid.SetRow(child, 0); - // two extra count for the span. Depending of the PrimaryAxisAlignment we need to add two extra grid - Grid.SetRowSpan(child, gridDefinitionsCount + 4); - Grid.SetColumnSpan(child, gridDefinitionsCount + 4); - continue; - } - - var gridIndex = shouldAddInBetweenRow ? rawChildIndex * 2 : rawChildIndex; - //We add a grid. we need to adjust the index. - var adjustGridIndex = gridIndex + gridRowIndexOffSet; - Grid.SetRow(child, adjustGridIndex); - var gridDefinition = _grid.RowDefinitions[adjustGridIndex]; - - // Get relative alignment or default if not set + set on child element - var primaryAlignment = GetPrimaryAlignment(child); - if (primaryAlignment is AutoLayoutPrimaryAlignment.Stretch) - { - gridDefinition.Height = new GridLength(1, GridUnitType.Star); - child.VerticalAlignment = VerticalAlignment.Stretch; - } - else if (GetPrimaryLength(child) is var height and > 0) - { - gridDefinition.Height = new GridLength(height); - child.VerticalAlignment = VerticalAlignment.Stretch; - } - else - { - gridDefinition.Height = GridLength.Auto; - child.VerticalAlignment = VerticalAlignment.Top; - } - - var counterAlignment = GetCounterAlignment(child).ToHorizontalAlignment(); - - child.HorizontalAlignment = counterAlignment; - - if (gridColumnIndexOffSet > 0) - { - Grid.SetColumn(child, gridColumnIndexOffSet); - } - - if (GetCounterLength(child) is var width and > 0) - { - child.Width = width; - } - rawChildIndex++; - } - - // Process "space between" - if (hasSpace) - { - if (isSpacing) - { - if (spacing < 0) - { - _grid.RowSpacing = spacing; - _grid.ClearValue(Grid.ColumnSpacingProperty); - } - else - { - for (var i = 1 + gridRowIndexOffSet; i < gridDefinitionsCount + gridRowIndexOffSet; i += 2) - { - _grid.RowDefinitions[i].Height = new GridLength(spacing); - } - } - } - else if (hasSpaceBetween) - { - for (var i = 1 + gridRowIndexOffSet; i < gridDefinitionsCount + gridRowIndexOffSet; i += 2) - { - _grid.RowDefinitions[i].Height = new GridLength(1, GridUnitType.Star); - } - } - } - - if (hasIndependentLayout && atLeastOneChildFillAvailableSpaceInPrimaryAxis is not true && PrimaryAxisAlignment != AutoLayoutAlignment.End && !hasSpaceBetween) - { - //We need to make sure that the independent layout can span all across his parent - _grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) }); - } - - var paddingCondition = atLeastOneChildFillAvailableSpaceInPrimaryAxis || PrimaryAxisAlignment is not AutoLayoutAlignment.Start || IsVerticalHug; - - } - else // Horizontal - { - if (hasPadding) - { - _grid.RowDefinitions.Insert(gridRowIndexOffSet ,new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) }); - } - - var rawChildIndex = 0; - - if (hasSpaceBetween is false && atLeastOneChildFillAvailableSpaceInPrimaryAxis is false && isPrimaryAxisAlignmentCenterOrEnd ) - { - _grid.ColumnDefinitions.Insert(gridColumnIndexOffSet, new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); - gridColumnIndexOffSet += 1; - } - - // Process children - for (var i = 0; i < childrenCount; i++) - { - var child = children[i]; - if (GetIsIndependentLayout(child)) - { - Grid.SetColumn(child, 0); - // two extra count for the span. Depending of the PrimaryAxisAlignment we need to add two extra grid - Grid.SetColumnSpan(child, gridDefinitionsCount + 4); - Grid.SetRowSpan(child, gridDefinitionsCount + 4); - continue; - } - - var gridIndex = shouldAddInBetweenRow ? rawChildIndex * 2 : rawChildIndex; - //We add a grid. we need to adjust the index. - var adjustGridIndex = gridIndex + gridColumnIndexOffSet; - var gridDefinition = _grid.ColumnDefinitions[adjustGridIndex]; - Grid.SetColumn(child, adjustGridIndex); - - // Get relative alignment or default if not set + set on child element - var primaryAlignment = GetPrimaryAlignment(child); - if (primaryAlignment is AutoLayoutPrimaryAlignment.Stretch) - { - gridDefinition.Width = new GridLength(1, GridUnitType.Star); - child.HorizontalAlignment = HorizontalAlignment.Stretch; - } - else if (GetPrimaryLength(child) is var width and > 0) - { - gridDefinition.Width = new GridLength(width); - child.HorizontalAlignment = HorizontalAlignment.Stretch; - } - else - { - gridDefinition.Width = GridLength.Auto; - child.HorizontalAlignment = HorizontalAlignment.Left; - } - - var counterAlignment = GetCounterAlignment(child).ToVerticalAlignment(); - - child.VerticalAlignment = counterAlignment; - - - if (gridRowIndexOffSet > 0) - { - Grid.SetRow(child, 1); - } - - if (GetCounterLength(child) is var height and > 0) - { - child.Height = height; - } - rawChildIndex++; - } - - // Process "space between" - if (hasSpace) - { - if (isSpacing) - { - - if (spacing < 0) - { - _grid.ColumnSpacing = spacing; - _grid.ClearValue(Grid.RowSpacingProperty); - } - else - { - for (var i = 1 + gridColumnIndexOffSet; i < gridDefinitionsCount + gridColumnIndexOffSet; i += 2) - { - _grid.ColumnDefinitions[i].Width = new GridLength(spacing); - } - } - } - else if (hasSpaceBetween) - { - for (var i = 1 + gridColumnIndexOffSet; i < gridDefinitionsCount + gridColumnIndexOffSet; i += 2) - { - _grid.ColumnDefinitions[i].Width = new GridLength(1, GridUnitType.Star); - } - } - } - - if (hasIndependentLayout && atLeastOneChildFillAvailableSpaceInPrimaryAxis is not true && PrimaryAxisAlignment != AutoLayoutAlignment.End && !hasSpaceBetween) - { - //We need to make sure that the independent layout can span all across his parent - _grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); - } - - var paddingCondition = atLeastOneChildFillAvailableSpaceInPrimaryAxis || PrimaryAxisAlignment is not AutoLayoutAlignment.Start || IsHorizontalHug; - } - - if (hasPadding) - { - if (IsVerticalHug || childrenVerticallAlignment is VerticalAlignment.Bottom or VerticalAlignment.Stretch || hasSpaceBetween) - { - var bottomPaddingSize = spacing < 0 ? padding.Bottom - spacing : padding.Bottom; - _grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(bottomPaddingSize), MaxHeight = bottomPaddingSize }); - } - - if (IsHorizontalHug || childrenHorizontalAlignment is HorizontalAlignment.Right or HorizontalAlignment.Stretch || hasSpaceBetween) - { - var rightPaddingsize = spacing < 0 ? padding.Right - spacing : padding.Right; - _grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(rightPaddingsize), MaxWidth = rightPaddingsize }); - } - } - - var shouldUsePrimaryAxisAlignment = atLeastOneChildFillAvailableSpaceInPrimaryAxis || hasSpaceBetween || hasIndependentLayout; - - // Set container alignments - if (isVertical) - { - if (shouldUsePrimaryAxisAlignment) - { - _grid.ClearValue(VerticalAlignmentProperty); - } - else - { - _grid.VerticalAlignment = PrimaryAxisAlignment.ToVerticalAlignment(); - } - _grid.ClearValue(HorizontalAlignmentProperty); - } - else - { - if (shouldUsePrimaryAxisAlignment) - { - _grid.ClearValue(HorizontalAlignmentProperty); - } - else - { - _grid.HorizontalAlignment = PrimaryAxisAlignment.ToHorizontalAlignment(); - } - _grid.ClearValue(VerticalAlignmentProperty); - } - } - } +public sealed partial class AutoLayout : RelativePanel +{ } diff --git a/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.xaml b/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.xaml deleted file mode 100644 index 2768d8430..000000000 --- a/src/Uno.Toolkit.UI/Controls/AutoLayout/AutoLayout.xaml +++ /dev/null @@ -1,24 +0,0 @@ - - - - -