From 14c9b27ef96caf44e43485f03952b8d9e1333641 Mon Sep 17 00:00:00 2001 From: Poker Date: Fri, 28 Mar 2025 23:36:31 +0800 Subject: [PATCH] [WrapPanel] Add Spacing Properties (#18079) * add Spacing properties for WrapPanel * add Unit Tests * remove spacing for items having IsVisble=false * refactor --------- Co-authored-by: Julien Lebosquain Co-authored-by: Betta_Fish Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com> --- src/Avalonia.Controls/WrapPanel.cs | 159 +++++++++++------- .../WrapPanelTests.cs | 145 +++++++++++++--- 2 files changed, 222 insertions(+), 82 deletions(-) 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() {