diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index fa7386780f..28d551e4b0 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/src/Avalonia.Controls/WrapPanel.cs
@@ -38,6 +38,18 @@ namespace Avalonia.Controls
///
public class WrapPanel : Panel, INavigableContainer
{
+ ///
+ /// Defines the dependency property.
+ ///
+ public static readonly StyledProperty ItemSpacingProperty =
+ AvaloniaProperty.Register(nameof(ItemSpacing));
+
+ ///
+ /// Defines the dependency property.
+ ///
+ public static readonly StyledProperty LineSpacingProperty =
+ AvaloniaProperty.Register(nameof(LineSpacing));
+
///
/// Defines the property.
///
@@ -67,12 +79,30 @@ namespace Avalonia.Controls
///
static WrapPanel()
{
- AffectsMeasure(OrientationProperty, ItemWidthProperty, ItemHeightProperty);
+ AffectsMeasure(ItemSpacingProperty, LineSpacingProperty, OrientationProperty, ItemWidthProperty, ItemHeightProperty);
AffectsArrange(ItemsAlignmentProperty);
}
///
- /// Gets or sets the orientation in which child controls will be layed out.
+ /// Gets or sets the spacing between lines.
+ ///
+ public double ItemSpacing
+ {
+ get => GetValue(ItemSpacingProperty);
+ set => SetValue(ItemSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the spacing between items.
+ ///
+ public double LineSpacing
+ {
+ get => GetValue(LineSpacingProperty);
+ set => SetValue(LineSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the orientation in which child controls will be laid out.
///
public Orientation Orientation
{
@@ -164,6 +194,8 @@ namespace Avalonia.Controls
{
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
+ double itemSpacing = ItemSpacing;
+ double lineSpacing = LineSpacing;
var orientation = Orientation;
var children = Children;
var curLineSize = new UVSize(orientation);
@@ -171,45 +203,46 @@ namespace Avalonia.Controls
var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height);
bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight);
+ bool itemExists = false;
+ bool lineExists = false;
var childConstraint = new Size(
itemWidthSet ? itemWidth : constraint.Width,
itemHeightSet ? itemHeight : constraint.Height);
- for (int i = 0, count = children.Count; i < count; i++)
+ for (int i = 0, count = children.Count; i < count; ++i)
{
var child = children[i];
// Flow passes its own constraint to children
child.Measure(childConstraint);
// This is the size of the child in UV space
- var sz = new UVSize(orientation,
+ UVSize childSize = new UVSize(orientation,
itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height);
- if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) // Need to switch to another line
+ var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
+ if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvConstraint.U)) // Need to switch to another line
{
panelSize.U = Max(curLineSize.U, panelSize.U);
- panelSize.V += curLineSize.V;
- curLineSize = sz;
+ panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
+ curLineSize = childSize;
- if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) // The element is wider then the constraint - give it a separate line
- {
- panelSize.U = Max(sz.U, panelSize.U);
- panelSize.V += sz.V;
- curLineSize = new UVSize(orientation);
- }
+ itemExists = child.IsVisible;
+ lineExists = true;
}
else // Continue to accumulate a line
{
- curLineSize.U += sz.U;
- curLineSize.V = Max(sz.V, curLineSize.V);
+ curLineSize.U += childSize.U + nextSpacing;
+ curLineSize.V = Max(childSize.V, curLineSize.V);
+
+ itemExists |= child.IsVisible; // keep true
}
}
// The last line size, if any should be added
panelSize.U = Max(curLineSize.U, panelSize.U);
- panelSize.V += curLineSize.V;
+ panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0);
// Go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height);
@@ -220,89 +253,91 @@ namespace Avalonia.Controls
{
double itemWidth = ItemWidth;
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 = orientation == Orientation.Horizontal ? itemWidth : itemHeight;
+ double itemU = isHorizontal ? itemWidth : itemHeight;
var curLineSize = new UVSize(orientation);
var uvFinalSize = new UVSize(orientation, finalSize.Width, finalSize.Height);
bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight);
- bool useItemU = orientation == Orientation.Horizontal ? itemWidthSet : itemHeightSet;
+ bool itemExists = false;
+ bool lineExists = false;
- for (int i = 0; i < children.Count; i++)
+ for (int i = 0; i < children.Count; ++i)
{
var child = children[i];
- var sz = new UVSize(orientation,
+ var childSize = new UVSize(orientation,
itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height);
- if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) // Need to switch to another line
+ var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0;
+ if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvFinalSize.U)) // Need to switch to another line
{
- ArrangeLine(accumulatedV, curLineSize.V, firstInLine, i, useItemU, itemU, uvFinalSize.U);
-
- accumulatedV += curLineSize.V;
- curLineSize = sz;
+ 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
+ curLineSize = childSize;
- 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, uvFinalSize.U);
-
- accumulatedV += sz.V;
- curLineSize = new UVSize(orientation);
- }
firstInLine = i;
+
+ itemExists = child.IsVisible;
+ lineExists = true;
}
else // Continue to accumulate a line
{
- curLineSize.U += sz.U;
- curLineSize.V = Max(sz.V, curLineSize.V);
+ curLineSize.U += childSize.U + nextSpacing;
+ curLineSize.V = Max(childSize.V, curLineSize.V);
+
+ itemExists |= child.IsVisible; // keep true
}
}
// Arrange the last line, if any
if (firstInLine < children.Count)
{
- ArrangeLine(accumulatedV, curLineSize.V, firstInLine, children.Count, useItemU, itemU, uvFinalSize.U);
+ accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first
+ ArrangeLine(curLineSize.V, firstInLine, children.Count);
}
return finalSize;
- }
- 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)
+ void ArrangeLine(double lineV, int start, int end)
{
- double totalU = 0;
- for (int i = start; i < end; i++)
+ bool useItemU = isHorizontal ? itemWidthSet : itemHeightSet;
+ double u = 0;
+ if (ItemsAlignment != WrapPanelItemsAlignment.Start)
{
- totalU += GetChildU(i);
+ double totalU = -itemSpacing;
+ for (int i = start; i < end; ++i)
+ {
+ totalU += GetChildU(i) + (!children[i].IsVisible ? 0 : itemSpacing);
+ }
+
+ u = ItemsAlignment switch
+ {
+ WrapPanelItemsAlignment.Center => (uvFinalSize.U - totalU) / 2,
+ WrapPanelItemsAlignment.End => uvFinalSize.U - totalU,
+ WrapPanelItemsAlignment.Start => 0,
+ _ => throw new ArgumentOutOfRangeException(nameof(ItemsAlignment), ItemsAlignment, null),
+ };
}
- u = ItemsAlignment switch
+ for (int i = start; i < end; ++i)
{
- WrapPanelItemsAlignment.Center => (panelU - totalU) / 2,
- WrapPanelItemsAlignment.End => panelU - totalU,
- WrapPanelItemsAlignment.Start => 0,
- _ => throw new NotImplementedException(),
- };
- }
+ 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);
+ }
- for (int i = start; i < end; i++)
- {
- double layoutSlotU = GetChildU(i);
- children[i].Arrange(isHorizontal ? new(u, v, layoutSlotU, lineV) : new(v, u, lineV, layoutSlotU));
- u += layoutSlotU;
+ return;
+ double GetChildU(int i) => useItemU ? itemU :
+ isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height;
}
-
- 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 c1066962a6..fa48e68953 100644
--- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs
@@ -13,10 +13,10 @@ namespace Avalonia.Controls.UnitTests
{
Width = 100,
Children =
- {
- new Border { Height = 50, Width = 100 },
- new Border { Height = 50, Width = 100 },
- }
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ }
};
target.Measure(Size.Infinity);
@@ -34,10 +34,10 @@ namespace Avalonia.Controls.UnitTests
{
Width = 200,
Children =
- {
- new Border { Height = 50, Width = 100 },
- new Border { Height = 50, Width = 100 },
- }
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ }
};
target.Measure(Size.Infinity);
@@ -110,10 +110,10 @@ namespace Avalonia.Controls.UnitTests
Orientation = Orientation.Vertical,
Height = 120,
Children =
- {
- new Border { Height = 50, Width = 100 },
- new Border { Height = 50, Width = 100 },
- }
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ }
};
target.Measure(Size.Infinity);
@@ -132,10 +132,10 @@ namespace Avalonia.Controls.UnitTests
Orientation = Orientation.Vertical,
Height = 60,
Children =
- {
- new Border { Height = 50, Width = 100 },
- new Border { Height = 50, Width = 100 },
- }
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ }
};
target.Measure(Size.Infinity);
@@ -146,6 +146,83 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds);
}
+ [Fact]
+ public void Lays_Out_Horizontally_On_Separate_Lines_With_Spacing()
+ {
+ var target = new WrapPanel
+ {
+ Width = 100,
+ ItemSpacing = 10,
+ LineSpacing = 20,
+ Children =
+ {
+ new Border { Height = 50, Width = 60 }, // line 0
+ new Border { Height = 50, Width = 30 }, // line 0
+ new Border { Height = 50, Width = 70 }, // line 1
+ new Border { Height = 50, Width = 30 }, // line 2
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(100, 190), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 60, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(70, 0, 30, 50), target.Children[1].Bounds);
+ Assert.Equal(new Rect(0, 70, 70, 50), target.Children[2].Bounds);
+ Assert.Equal(new Rect(0, 140, 30, 50), target.Children[3].Bounds);
+ }
+
+ [Fact]
+ public void Lays_Out_Horizontally_On_Separate_Lines_With_Spacing_Invisible()
+ {
+ var target = new WrapPanel
+ {
+ ItemSpacing = 10,
+ Children =
+ {
+ new Border { Height = 50, Width = 60 }, // line 0
+ new Border { Height = 50, Width = 30 , IsVisible = false }, // line 0
+ new Border { Height = 50, Width = 50 }, // line 0
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(120, 50), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 60, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(70, 0, 50, 50), target.Children[2].Bounds);
+ }
+
+ [Fact]
+ public void Lays_Out_Horizontally_On_Separate_Lines_With_Spacing_Vertical()
+ {
+ var target = new WrapPanel
+ {
+ Height = 100,
+ Orientation = Orientation.Vertical,
+ ItemSpacing = 10,
+ LineSpacing = 20,
+ Children =
+ {
+ new Border { Width = 50, Height = 60 }, // line 0
+ new Border { Width = 50, Height = 30 }, // line 0
+ new Border { Width = 50, Height = 70 }, // line 1
+ new Border { Width = 50, Height = 30 }, // line 2
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(190, 100), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 50, 60), target.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 70, 50, 30), target.Children[1].Bounds);
+ Assert.Equal(new Rect(70, 0, 50, 70), target.Children[2].Bounds);
+ Assert.Equal(new Rect(140, 0, 50, 30), target.Children[3].Bounds);
+ }
+
[Fact]
public void Applies_ItemWidth_And_ItemHeight_Properties()
{
@@ -156,10 +233,10 @@ namespace Avalonia.Controls.UnitTests
ItemWidth = 20,
ItemHeight = 15,
Children =
- {
- new Border(),
- new Border(),
- }
+ {
+ new Border(),
+ new Border(),
+ }
};
target.Measure(Size.Infinity);
@@ -170,6 +247,34 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds);
}
+ [Fact]
+ public void Zero_Size_Visible_Child()
+ {
+ var target = new WrapPanel()
+ {
+ Orientation = Orientation.Horizontal,
+ Width = 50,
+ ItemSpacing = 10,
+ LineSpacing = 10,
+ Children =
+ {
+ new Border(), // line 0
+ new Border // line 1
+ {
+ Width = 50,
+ Height = 50
+ },
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(50, 60), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 0, 0), target.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 10, 50, 50), target.Children[1].Bounds);
+ }
+
[Fact]
void ItemWidth_Trigger_InvalidateMeasure()
{