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()
{