From 2e6c571fc9a90a07d00131cce8b8e7f40a09a2f8 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:57:58 +0100 Subject: [PATCH] Added WrapItemsAlignment (#17792) --- src/Avalonia.Controls/WrapPanel.cs | 74 +++++++++++++++---- .../WrapPanelTests.cs | 55 ++++++++++++++ 2 files changed, 116 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 1d6c78260d..fa7386780f 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -3,6 +3,7 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. +using System; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Utilities; @@ -11,6 +12,24 @@ using static System.Math; namespace Avalonia.Controls { + public enum WrapPanelItemsAlignment + { + /// + /// Items are laid out so the first one in each column/row touches the top/left of the panel. + /// + Start, + + /// + /// Items are laid out so that each column/row is centred vertically/horizontally within the panel. + /// + Center, + + /// + /// Items are laid out so the last one in each column/row touches the bottom/right of the panel. + /// + End, + } + /// /// Positions child elements in sequential position from left to right, /// breaking content to the next line at the edge of the containing box. @@ -25,6 +44,12 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), defaultValue: Orientation.Horizontal); + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemsAlignmentProperty = + AvaloniaProperty.Register(nameof(ItemsAlignment), defaultValue: WrapPanelItemsAlignment.Start); + /// /// Defines the property. /// @@ -43,6 +68,7 @@ namespace Avalonia.Controls static WrapPanel() { AffectsMeasure(OrientationProperty, ItemWidthProperty, ItemHeightProperty); + AffectsArrange(ItemsAlignmentProperty); } /// @@ -54,6 +80,15 @@ namespace Avalonia.Controls set => SetValue(OrientationProperty, value); } + /// + /// Gets or sets the alignment of items in the WrapPanel. + /// + public WrapPanelItemsAlignment ItemsAlignment + { + get => GetValue(ItemsAlignmentProperty); + set => SetValue(ItemsAlignmentProperty, value); + } + /// /// Gets or sets the width of all items in the WrapPanel. /// @@ -140,7 +175,7 @@ namespace Avalonia.Controls var childConstraint = new Size( itemWidthSet ? itemWidth : constraint.Width, itemHeightSet ? itemHeight : constraint.Height); - + for (int i = 0, count = children.Count; i < count; i++) { var child = children[i]; @@ -205,7 +240,7 @@ namespace Avalonia.Controls if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) // Need to switch to another line { - ArrangeLine(accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU); + ArrangeLine(accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU, uvFinalSize.U); accumulatedV += curLineSize.V; curLineSize = sz; @@ -213,7 +248,7 @@ namespace Avalonia.Controls if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) // The element is wider then the constraint - give it a separate line { // Switch to next line which only contain one element - ArrangeLine(accumulatedV, sz.V, i, ++i, useItemU, itemU); + ArrangeLine(accumulatedV, sz.V, i, ++i, useItemU, itemU, uvFinalSize.U); accumulatedV += sz.V; curLineSize = new UVSize(orientation); @@ -230,31 +265,44 @@ namespace Avalonia.Controls // Arrange the last line, if any if (firstInLine < children.Count) { - ArrangeLine(accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU); + ArrangeLine(accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU, uvFinalSize.U); } return finalSize; } - private void ArrangeLine(double v, double lineV, int start, int end, bool useItemU, double itemU) + private void ArrangeLine(double v, double lineV, int start, int end, bool useItemU, double itemU, double panelU) { var orientation = Orientation; var children = Children; double u = 0; bool isHorizontal = orientation == Orientation.Horizontal; + if (ItemsAlignment != WrapPanelItemsAlignment.Start) + { + double totalU = 0; + for (int i = start; i < end; i++) + { + totalU += GetChildU(i); + } + + u = ItemsAlignment switch + { + WrapPanelItemsAlignment.Center => (panelU - totalU) / 2, + WrapPanelItemsAlignment.End => panelU - totalU, + WrapPanelItemsAlignment.Start => 0, + _ => throw new NotImplementedException(), + }; + } + for (int i = start; i < end; i++) { - var child = children[i]; - var childSize = new UVSize(orientation, child.DesiredSize.Width, child.DesiredSize.Height); - double layoutSlotU = useItemU ? itemU : childSize.U; - child.Arrange(new Rect( - isHorizontal ? u : v, - isHorizontal ? v : u, - isHorizontal ? layoutSlotU : lineV, - isHorizontal ? lineV : layoutSlotU)); + double layoutSlotU = GetChildU(i); + children[i].Arrange(isHorizontal ? new(u, v, layoutSlotU, lineV) : new(v, u, lineV, layoutSlotU)); u += layoutSlotU; } + + double GetChildU(int i) => useItemU ? itemU : isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height; } private struct UVSize diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index 94f1b30fc9..c1066962a6 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Layout; using Xunit; @@ -47,6 +48,60 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds); } + public static TheoryData GetItemsAlignmentValues() + { + var data = new TheoryData(); + foreach (var orientation in Enum.GetValues()) + { + foreach (var alignment in Enum.GetValues()) + { + data.Add(orientation, alignment); + } + } + return data; + } + + [Theory, MemberData(nameof(GetItemsAlignmentValues))] + public void Lays_Out_With_Items_Alignment(Orientation orientation, WrapPanelItemsAlignment itemsAlignment) + { + var target = new WrapPanel() + { + Width = 200, + Height = 200, + Orientation = orientation, + ItemsAlignment = itemsAlignment, + Children = + { + new Border { Height = 50, Width = 50 }, + new Border { Height = 50, Width = 50 }, + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(200, 200), target.Bounds.Size); + + var rowBounds = target.Children[0].Bounds.Union(target.Children[1].Bounds); + + Assert.Equal(orientation switch + { + Orientation.Horizontal => new(100, 50), + Orientation.Vertical => new(50, 100), + _ => throw new NotImplementedException() + }, rowBounds.Size); + + Assert.Equal((orientation, itemsAlignment) switch + { + (_, WrapPanelItemsAlignment.Start) => new(0, 0), + (Orientation.Horizontal, WrapPanelItemsAlignment.Center) => new(50, 0), + (Orientation.Vertical, WrapPanelItemsAlignment.Center) => new(0, 50), + (Orientation.Horizontal, WrapPanelItemsAlignment.End) => new(100, 0), + (Orientation.Vertical, WrapPanelItemsAlignment.End) => new(0, 100), + _ => throw new NotImplementedException(), + }, rowBounds.Position); + } + [Fact] public void Lays_Out_Vertically_Children_On_A_Single_Line() {