Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added WrapPanel item alignment #17792

Merged
merged 1 commit into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 61 additions & 13 deletions src/Avalonia.Controls/WrapPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,6 +12,24 @@

namespace Avalonia.Controls
{
public enum WrapPanelItemsAlignment
{
/// <summary>
/// Items are laid out so the first one in each column/row touches the top/left of the panel.
/// </summary>
Start,

/// <summary>
/// Items are laid out so that each column/row is centred vertically/horizontally within the panel.
/// </summary>
Center,

/// <summary>
/// Items are laid out so the last one in each column/row touches the bottom/right of the panel.
/// </summary>
End,
}

/// <summary>
/// Positions child elements in sequential position from left to right,
/// breaking content to the next line at the edge of the containing box.
Expand All @@ -25,6 +44,12 @@ public class WrapPanel : Panel, INavigableContainer
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<WrapPanel, Orientation>(nameof(Orientation), defaultValue: Orientation.Horizontal);

/// <summary>
/// Defines the <see cref="ItemsAlignment"/> property.
/// </summary>
public static readonly StyledProperty<WrapPanelItemsAlignment> ItemsAlignmentProperty =
AvaloniaProperty.Register<WrapPanel, WrapPanelItemsAlignment>(nameof(ItemsAlignment), defaultValue: WrapPanelItemsAlignment.Start);

/// <summary>
/// Defines the <see cref="ItemWidth"/> property.
/// </summary>
Expand All @@ -43,6 +68,7 @@ public class WrapPanel : Panel, INavigableContainer
static WrapPanel()
{
AffectsMeasure<WrapPanel>(OrientationProperty, ItemWidthProperty, ItemHeightProperty);
AffectsArrange<WrapPanel>(ItemsAlignmentProperty);
}

/// <summary>
Expand All @@ -54,6 +80,15 @@ public Orientation Orientation
set => SetValue(OrientationProperty, value);
}

/// <summary>
/// Gets or sets the alignment of items in the WrapPanel.
/// </summary>
public WrapPanelItemsAlignment ItemsAlignment
{
get => GetValue(ItemsAlignmentProperty);
set => SetValue(ItemsAlignmentProperty, value);
}

/// <summary>
/// Gets or sets the width of all items in the WrapPanel.
/// </summary>
Expand Down Expand Up @@ -140,7 +175,7 @@ protected override Size MeasureOverride(Size constraint)
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];
Expand Down Expand Up @@ -205,15 +240,15 @@ protected override Size ArrangeOverride(Size finalSize)

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;

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);
Expand All @@ -230,31 +265,44 @@ protected override Size ArrangeOverride(Size finalSize)
// 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
Expand Down
55 changes: 55 additions & 0 deletions tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Avalonia.Layout;
using Xunit;

Expand Down Expand Up @@ -47,6 +48,60 @@ public void Lays_Out_Horizontally_On_A_Single_Line()
Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds);
}

public static TheoryData<Orientation, WrapPanelItemsAlignment> GetItemsAlignmentValues()
{
var data = new TheoryData<Orientation, WrapPanelItemsAlignment>();
foreach (var orientation in Enum.GetValues<Orientation>())
{
foreach (var alignment in Enum.GetValues<WrapPanelItemsAlignment>())
{
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()
{
Expand Down