diff --git a/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs
new file mode 100644
index 0000000000..6d529d9a59
--- /dev/null
+++ b/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs
@@ -0,0 +1,87 @@
+using Avalonia.Layout;
+
+namespace Avalonia.Controls.Utils;
+
+internal interface IOrientationBasedMeasures
+{
+ Orientation ScrollOrientation { get; }
+
+ bool IsVertical => ScrollOrientation is Orientation.Vertical;
+}
+
+internal static class OrientationBasedMeasuresExt
+{
+ extension(IOrientationBasedMeasures m)
+ {
+ ///
+ /// The length of non-scrolling direction
+ ///
+ ///
+ ///
+ public double Major(Size size) =>
+ m.IsVertical ? size.Height : size.Width;
+
+ ///
+ /// The length of scrolling direction
+ ///
+ ///
+ ///
+ public double Minor(Size size) =>
+ m.IsVertical ? size.Width : size.Height;
+
+ public T Minor(T width, T height) =>
+ m.IsVertical ? width : height;
+
+ public T Major(T width, T height) =>
+ m.IsVertical ? height : width;
+
+ public void SetMajor(ref Size size, double value) =>
+ size = m.IsVertical ? size.WithHeight(value) : size.WithWidth(value);
+
+ public void SetMinor(ref Size size, double value) =>
+ size = m.IsVertical ? size.WithWidth(value) : size.WithHeight(value);
+
+ public double MajorSize(Rect rect) =>
+ m.IsVertical ? rect.Height : rect.Width;
+
+ public void SetMajorSize(ref Rect rect, double value) =>
+ rect = m.IsVertical ? rect.WithHeight(value) : rect.WithWidth(value);
+
+ public double MinorSize(Rect rect) =>
+ m.IsVertical ? rect.Width : rect.Height;
+
+ public void SetMinorSize(ref Rect rect, double value) =>
+ rect = m.IsVertical ? rect.WithWidth(value) : rect.WithHeight(value);
+
+ public double MajorStart(Rect rect) =>
+ m.IsVertical ? rect.Y : rect.X;
+
+ public double MajorEnd(Rect rect) =>
+ m.IsVertical ? rect.Bottom : rect.Right;
+
+ public double MinorStart(Rect rect) =>
+ m.IsVertical ? rect.X : rect.Y;
+
+ public void SetMinorStart(ref Rect rect, double value) =>
+ rect = m.IsVertical ? rect.WithX(value) : rect.WithY(value);
+
+ public void SetMajorStart(ref Rect rect, double value) =>
+ rect = m.IsVertical ? rect.WithY(value) : rect.WithX(value);
+
+ public double MinorEnd(Rect rect) =>
+ m.IsVertical ? rect.Right : rect.Bottom;
+
+ public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize) =>
+ m.IsVertical ?
+ new Rect(minor, major, minorSize, majorSize) :
+ new Rect(major, minor, majorSize, minorSize);
+
+ public Point MinorMajorPoint(double minor, double major) =>
+ m.IsVertical ?
+ new Point(minor, major) : new Point(major, minor);
+
+ public Size MinorMajorSize(double minor, double major) =>
+ m.IsVertical ?
+ new Size(minor, major) : new Size(major, minor);
+ }
+}
diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index a08695874e..7da3479c5f 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/src/Avalonia.Controls/WrapPanel.cs
@@ -4,6 +4,8 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
+using System.Diagnostics;
+using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
@@ -28,6 +30,32 @@ namespace Avalonia.Controls
/// Items are laid out so the last one in each column/row touches the bottom/right of the panel.
///
End,
+
+ ///
+ /// Items are laid out with equal spacing between them within each column/row.
+ ///
+ ///
+ /// is become the minimum spacing between items,
+ ///
+ Justify,
+
+ ///
+ /// Items are stretched evenly to fill the entire height/width of each column/row (last column/row excluded).
+ ///
+ ///
+ /// or is become the minimum size of items,
+ ///
+ Stretch,
+
+ /*
+ ///
+ /// Items are stretched evenly to fill the entire height/width of each column/row.
+ ///
+ ///
+ /// or is become the minimum size of items,
+ ///
+ StretchAll
+ */
}
///
@@ -36,7 +64,7 @@ namespace Avalonia.Controls
/// Subsequent ordering happens sequentially from top to bottom or from right to left,
/// depending on the value of the property.
///
- public class WrapPanel : Panel, INavigableContainer
+ public class WrapPanel : Panel, INavigableContainer, IOrientationBasedMeasures
{
///
/// Defines the dependency property.
@@ -137,6 +165,20 @@ namespace Avalonia.Controls
set => SetValue(ItemHeightProperty, value);
}
+ private Orientation ScrollOrientation { get; set; } = Orientation.Vertical;
+
+ Orientation IOrientationBasedMeasures.ScrollOrientation => ScrollOrientation;
+
+ ///
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ if (change.Property == OrientationProperty)
+ ScrollOrientation = Orientation is Orientation.Horizontal ?
+ Orientation.Vertical :
+ Orientation.Horizontal;
+ }
+
///
/// Gets the next control in the specified direction.
///
@@ -183,10 +225,8 @@ namespace Avalonia.Controls
{
return children[index];
}
- else
- {
- return null;
- }
+
+ return null;
}
///
@@ -196,36 +236,39 @@ namespace Avalonia.Controls
double itemHeight = ItemHeight;
double itemSpacing = ItemSpacing;
double lineSpacing = LineSpacing;
- var orientation = Orientation;
- var children = Children;
- var curLineSize = new UVSize(orientation);
- var panelSize = new UVSize(orientation);
- var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height);
+ var curLineSize = new Size();
+ var panelSize = new Size();
bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight);
bool itemExists = false;
bool lineExists = false;
+ var itemsAlignment = ItemsAlignment;
+ // If we have infinite space on minor axis, we always use Start alignment to avoid strange behavior
+ if (this.Minor(constraint) is double.PositiveInfinity)
+ itemsAlignment = WrapPanelItemsAlignment.Start;
+ // Justify/StretchAll need to measure with full constraint on minor axis
+ if (itemsAlignment is WrapPanelItemsAlignment.Justify /*or WrapPanelItemsAlignment.StretchAll*/)
+ this.SetMinor(ref panelSize, this.Minor(constraint));
var childConstraint = new Size(
itemWidthSet ? itemWidth : constraint.Width,
itemHeightSet ? itemHeight : constraint.Height);
- for (int i = 0, count = children.Count; i < count; ++i)
+ foreach (var child in Children)
{
- var child = children[i];
// Flow passes its own constraint to children
child.Measure(childConstraint);
- // This is the size of the child in UV space
- UVSize childSize = new UVSize(orientation,
+ var childSize = new Size(
itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height);
var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
- if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvConstraint.U)) // Need to switch to another line
+ if (MathUtilities.GreaterThan(this.Minor(curLineSize) + this.Minor(childSize) + nextSpacing, this.Minor(constraint))) // Need to switch to another line
{
- panelSize.U = Max(curLineSize.U, panelSize.U);
- panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
+ panelSize = this.MinorMajorSize(
+ Max(this.Minor(curLineSize), this.Minor(panelSize)),
+ this.Major(panelSize) + this.Major(curLineSize) + (lineExists ? lineSpacing : 0));
curLineSize = childSize;
itemExists = child.IsVisible;
@@ -233,19 +276,24 @@ namespace Avalonia.Controls
}
else // Continue to accumulate a line
{
- curLineSize.U += childSize.U + nextSpacing;
- curLineSize.V = Max(childSize.V, curLineSize.V);
+ curLineSize = this.MinorMajorSize(
+ this.Minor(curLineSize) + this.Minor(childSize) + nextSpacing,
+ Max(this.Major(childSize), this.Major(curLineSize)));
itemExists |= child.IsVisible; // keep true
}
}
+ // Stretch need to measure with full constraint on minor axis if multiple lines
+ if (lineExists && itemsAlignment is WrapPanelItemsAlignment.Stretch)
+ this.SetMinor(ref panelSize, this.Minor(constraint));
+
// The last line size, if any should be added
- panelSize.U = Max(curLineSize.U, panelSize.U);
- panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
+ panelSize = this.MinorMajorSize(
+ Max(this.Minor(curLineSize), this.Minor(panelSize)),
+ this.Major(panelSize) + this.Major(curLineSize) + (lineExists ? lineSpacing : 0));
- // Go from UV space to W/H space
- return new Size(panelSize.Width, panelSize.Height);
+ return panelSize;
}
///
@@ -255,32 +303,33 @@ namespace Avalonia.Controls
double itemHeight = ItemHeight;
double itemSpacing = ItemSpacing;
double lineSpacing = LineSpacing;
- var orientation = Orientation;
- bool isHorizontal = orientation == Orientation.Horizontal;
var children = Children;
int firstInLine = 0;
- double accumulatedV = 0;
- double itemU = isHorizontal ? itemWidth : itemHeight;
- var curLineSize = new UVSize(orientation);
- var uvFinalSize = new UVSize(orientation, finalSize.Width, finalSize.Height);
+ double accumulatedMajor = 0;
+ double itemMinor = this.Minor(itemWidth, itemHeight);
+ var curLineSize = new Size();
bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight);
bool itemExists = false;
bool lineExists = false;
+ var itemsAlignment = ItemsAlignment;
+ // If we have infinite space on minor axis, we always use Start alignment to avoid strange behavior
+ if (this.Minor(finalSize) is double.PositiveInfinity)
+ itemsAlignment = WrapPanelItemsAlignment.Start;
for (int i = 0; i < children.Count; ++i)
{
var child = children[i];
- var childSize = new UVSize(orientation,
+ var childSize = new Size(
itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height);
var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
- if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvFinalSize.U)) // Need to switch to another line
+ if (MathUtilities.GreaterThan(this.Minor(curLineSize) + this.Minor(childSize) + nextSpacing, this.Minor(finalSize))) // Need to switch to another line
{
- accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
- ArrangeLine(curLineSize.V, firstInLine, i);
- accumulatedV += curLineSize.V; // add the height of the line just arranged
+ accumulatedMajor += lineExists ? lineSpacing : 0; // add spacing to arrange line first
+ ArrangeLine(this.Major(curLineSize), firstInLine, i);
+ accumulatedMajor += this.Major(curLineSize); // add the height of the line just arranged
curLineSize = childSize;
firstInLine = i;
@@ -290,8 +339,9 @@ namespace Avalonia.Controls
}
else // Continue to accumulate a line
{
- curLineSize.U += childSize.U + nextSpacing;
- curLineSize.V = Max(childSize.V, curLineSize.V);
+ curLineSize = this.MinorMajorSize(
+ this.Minor(curLineSize) + this.Minor(childSize) + nextSpacing,
+ Max(this.Major(childSize), this.Major(curLineSize)));
itemExists |= child.IsVisible; // keep true
}
@@ -300,75 +350,72 @@ namespace Avalonia.Controls
// Arrange the last line, if any
if (firstInLine < children.Count)
{
- accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
- ArrangeLine(curLineSize.V, firstInLine, children.Count);
+ accumulatedMajor += lineExists ? lineSpacing : 0; // add spacing to arrange line first
+ ArrangeLine(this.Major(curLineSize), firstInLine, children.Count);
}
return finalSize;
- void ArrangeLine(double lineV, int start, int end)
+ void ArrangeLine(double lineMajor, int start, int endExcluded)
{
- bool useItemU = isHorizontal ? itemWidthSet : itemHeightSet;
- double u = 0;
- if (ItemsAlignment != WrapPanelItemsAlignment.Start)
+ bool useItemMinor = this.Minor(itemWidthSet, itemHeightSet);
+ double minorStart = 0d;
+ var minorSpacing = itemSpacing;
+ // Count of spacings between items
+ var minorSpacingCount = -1;
+ double totalMinor = 0d;
+ double minorStretchRatio = 1d;
+ var tempItemsAlignment = itemsAlignment;
+ if (itemsAlignment is WrapPanelItemsAlignment.Stretch && endExcluded == children.Count)
+ // Don't stretch the last line
+ tempItemsAlignment = WrapPanelItemsAlignment.Start;
+
+ if (tempItemsAlignment is not WrapPanelItemsAlignment.Start)
{
- double totalU = -itemSpacing;
- for (int i = start; i < end; ++i)
+ for (int i = start; i < endExcluded; ++i)
{
- totalU += GetChildU(i) + (!children[i].IsVisible ? 0 : itemSpacing);
+ totalMinor += GetChildMinor(i);
+ if (children[i].IsVisible)
+ ++minorSpacingCount;
}
+ }
- u = ItemsAlignment switch
- {
- WrapPanelItemsAlignment.Center => (uvFinalSize.U - totalU) / 2,
- WrapPanelItemsAlignment.End => uvFinalSize.U - totalU,
- WrapPanelItemsAlignment.Start => 0,
- _ => throw new ArgumentOutOfRangeException(nameof(ItemsAlignment), ItemsAlignment, null),
- };
+ Debug.Assert(this.Minor(finalSize) >= totalMinor + minorSpacing * minorSpacingCount);
+
+ switch (tempItemsAlignment)
+ {
+ case WrapPanelItemsAlignment.Start:
+ break;
+ case WrapPanelItemsAlignment.Center:
+ totalMinor += minorSpacing * minorSpacingCount;
+ minorStart = (this.Minor(finalSize) - totalMinor) / 2;
+ break;
+ case WrapPanelItemsAlignment.End:
+ totalMinor += minorSpacing * minorSpacingCount;
+ minorStart = this.Minor(finalSize) - totalMinor;
+ break;
+ case WrapPanelItemsAlignment.Justify:
+ var totalMinorSpacing = this.Minor(finalSize) - totalMinor - 0.01; // small epsilon to avoid rounding issues
+ if (minorSpacingCount > 0)
+ minorSpacing = totalMinorSpacing / minorSpacingCount;
+ break;
+ case WrapPanelItemsAlignment.Stretch /*or WrapPanelItemsAlignment.StretchAll*/:
+ var finalMinorWithoutSpacing = this.Minor(finalSize) - minorSpacing * minorSpacingCount - 0.01; // small epsilon to avoid rounding issues
+ minorStretchRatio = finalMinorWithoutSpacing / totalMinor;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(itemsAlignment), itemsAlignment, null);
}
- for (int i = start; i < end; ++i)
+ for (int i = start; i < endExcluded; ++i)
{
- double layoutSlotU = GetChildU(i);
- children[i].Arrange(isHorizontal ? new(u, accumulatedV, layoutSlotU, lineV) : new(accumulatedV, u, lineV, layoutSlotU));
- u += layoutSlotU + (!children[i].IsVisible ? 0 : itemSpacing);
+ double layoutSlotMinor = GetChildMinor(i) * minorStretchRatio;
+ children[i].Arrange(this.MinorMajorRect(minorStart, accumulatedMajor, layoutSlotMinor, lineMajor));
+ minorStart += layoutSlotMinor + (children[i].IsVisible ? minorSpacing : 0);
}
return;
- double GetChildU(int i) => useItemU ? itemU :
- isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height;
- }
- }
-
- private struct UVSize
- {
- internal UVSize(Orientation orientation, double width, double height)
- {
- U = V = 0d;
- _orientation = orientation;
- Width = width;
- Height = height;
- }
-
- internal UVSize(Orientation orientation)
- {
- U = V = 0d;
- _orientation = orientation;
- }
-
- internal double U;
- internal double V;
- private Orientation _orientation;
-
- internal double Width
- {
- get => _orientation == Orientation.Horizontal ? U : V;
- set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
- }
- internal double Height
- {
- get => _orientation == Orientation.Horizontal ? V : U;
- set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
+ double GetChildMinor(int i) => useItemMinor ? itemMinor : this.Minor(children[i].DesiredSize);
}
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
index 530d7a4956..cbe8b925ae 100644
--- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Numerics;
using Avalonia.Layout;
using Avalonia.UnitTests;
using Xunit;
@@ -65,42 +66,74 @@ namespace Avalonia.Controls.UnitTests
[Theory, MemberData(nameof(GetItemsAlignmentValues))]
public void Lays_Out_With_Items_Alignment(Orientation orientation, WrapPanelItemsAlignment itemsAlignment)
{
- var target = new WrapPanel()
+ var lineHeight = 50d;
+ 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 },
- }
+ UseLayoutRounding = false
};
+ if (orientation is Orientation.Horizontal)
+ {
+ target.ItemHeight = lineHeight;
+ target.Children.Add(new Border { MinWidth = 50 });
+ target.Children.Add(new Border { MinWidth = 100 });
+ target.Children.Add(new Border { MinWidth = 150 });
+ }
+ else
+ {
+ target.ItemWidth = lineHeight;
+ target.Children.Add(new Border { MinHeight = 50 });
+ target.Children.Add(new Border { MinHeight = 100 });
+ target.Children.Add(new Border { MinHeight = 150 });
+ }
+
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);
+ var row1Bounds = target.Children[0].Bounds.Union(target.Children[1].Bounds);
+ var row2Bounds = target.Children[2].Bounds;
+
+ // fix layout rounding
+ row1Bounds = new Rect(
+ Math.Round(row1Bounds.X),
+ Math.Round(row1Bounds.Y),
+ Math.Round(row1Bounds.Width),
+ Math.Round(row1Bounds.Height));
+
+ if (orientation is Orientation.Vertical)
+ {
+ // X <=> Y, Width <=> Height
+ var reflectionMatrix = new Matrix4x4(
+ 0, 1, 0, 0, // X' = Y
+ 1, 0, 0, 0, // Y' = X
+ 0, 0, 1, 0, // Z' = Z
+ 0, 0, 0, 1 // W' = W
+ );
+ row1Bounds = row1Bounds.TransformToAABB(reflectionMatrix);
+ row2Bounds = row2Bounds.TransformToAABB(reflectionMatrix);
+ }
- Assert.Equal(orientation switch
+ Assert.Equal(itemsAlignment switch
{
- Orientation.Horizontal => new(100, 50),
- Orientation.Vertical => new(50, 100),
- _ => throw new NotImplementedException()
- }, rowBounds.Size);
+ WrapPanelItemsAlignment.Stretch or WrapPanelItemsAlignment.Justify /*or WrapPanelItemsAlignment.StretchAll*/ => new(0, 0, 200, lineHeight),
+ WrapPanelItemsAlignment.Center => new(25, 0, 150, lineHeight),
+ WrapPanelItemsAlignment.End => new(50, 0, 150, lineHeight),
+ _ => new(0, 0, 150, lineHeight)
+ }, row1Bounds);
- Assert.Equal((orientation, itemsAlignment) switch
+ Assert.Equal(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);
+ // WrapPanelItemsAlignment.StretchAll => new(0, lineHeight, 200, lineHeight),
+ WrapPanelItemsAlignment.Center => new(25, lineHeight, 150, lineHeight),
+ WrapPanelItemsAlignment.End => new(50, lineHeight, 150, lineHeight),
+ _ => new(0, 50, 150, 50)
+ }, row2Bounds);
}
[Fact]