Selaa lähdekoodia

[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 <[email protected]>
Co-authored-by: Betta_Fish <[email protected]>
Co-authored-by: Jumar Macato <[email protected]>
Poker 6 kuukautta sitten
vanhempi
sitoutus
14c9b27ef9
2 muutettua tiedostoa jossa 222 lisäystä ja 82 poistoa
  1. 97 62
      src/Avalonia.Controls/WrapPanel.cs
  2. 125 20
      tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

+ 97 - 62
src/Avalonia.Controls/WrapPanel.cs

@@ -38,6 +38,18 @@ namespace Avalonia.Controls
     /// </summary>
     public class WrapPanel : Panel, INavigableContainer
     {
+        /// <summary>
+        /// Defines the <see cref="ItemSpacing"/> dependency property.
+        /// </summary>
+        public static readonly StyledProperty<double> ItemSpacingProperty =
+            AvaloniaProperty.Register<WrapPanel, double>(nameof(ItemSpacing));
+
+        /// <summary>
+        /// Defines the <see cref="LineSpacing"/> dependency property.
+        /// </summary>
+        public static readonly StyledProperty<double> LineSpacingProperty =
+            AvaloniaProperty.Register<WrapPanel, double>(nameof(LineSpacing));
+
         /// <summary>
         /// Defines the <see cref="Orientation"/> property.
         /// </summary>
@@ -67,12 +79,30 @@ namespace Avalonia.Controls
         /// </summary>
         static WrapPanel()
         {
-            AffectsMeasure<WrapPanel>(OrientationProperty, ItemWidthProperty, ItemHeightProperty);
+            AffectsMeasure<WrapPanel>(ItemSpacingProperty, LineSpacingProperty, OrientationProperty, ItemWidthProperty, ItemHeightProperty);
             AffectsArrange<WrapPanel>(ItemsAlignmentProperty);
         }
 
         /// <summary>
-        /// Gets or sets the orientation in which child controls will be layed out.
+        /// Gets or sets the spacing between lines.
+        /// </summary>
+        public double ItemSpacing
+        {
+            get => GetValue(ItemSpacingProperty);
+            set => SetValue(ItemSpacingProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the spacing between items.
+        /// </summary>
+        public double LineSpacing
+        {
+            get => GetValue(LineSpacingProperty);
+            set => SetValue(LineSpacingProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the orientation in which child controls will be laid out.
         /// </summary>
         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

+ 125 - 20
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()
         {