Browse Source

Merge pull request #2431 from AvaloniaUI/fixes/2350-stackpanel-layout

Fix StackPanel layout
Steven Kirk 6 years ago
parent
commit
e8a6908c3b

+ 5 - 6
src/Avalonia.Controls/Image.cs

@@ -81,23 +81,22 @@ namespace Avalonia.Controls
         protected override Size MeasureOverride(Size availableSize)
         {
             var source = Source;
+            var result = new Size();
 
             if (source != null)
             {
                 Size sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height);
                 if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height))
                 {
-                    return sourceSize;
+                    result = sourceSize;
                 }
                 else
                 {
-                    return Stretch.CalculateSize(availableSize, sourceSize);
+                    result = Stretch.CalculateSize(availableSize, sourceSize);
                 }
             }
-            else
-            {
-                return new Size();
-            }
+
+            return result.Constrain(availableSize);
         }
 
         /// <inheritdoc/>

+ 15 - 39
src/Avalonia.Controls/StackPanel.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Linq;
 using Avalonia.Input;
+using Avalonia.Layout;
 
 namespace Avalonia.Controls
 {
@@ -219,30 +220,16 @@ namespace Avalonia.Controls
                 measuredWidth -= (hasVisibleChild ? spacing : 0);
             }
 
-            return new Size(measuredWidth, measuredHeight);
+            return new Size(measuredWidth, measuredHeight).Constrain(availableSize);
         }
 
-        /// <summary>
-        /// Arranges the control's children.
-        /// </summary>
-        /// <param name="finalSize">The size allocated to the control.</param>
-        /// <returns>The space taken.</returns>
+        /// <inheritdoc/>
         protected override Size ArrangeOverride(Size finalSize)
         {
             var orientation = Orientation;
-            double arrangedWidth = finalSize.Width;
-            double arrangedHeight = finalSize.Height;
-            double spacing = Spacing;
-            bool hasVisibleChild = Children.Any(c => c.IsVisible);
-
-            if (Orientation == Orientation.Vertical)
-            {
-                arrangedHeight = 0;
-            }
-            else
-            {
-                arrangedWidth = 0;
-            }
+            var spacing = Spacing;
+            var finalRect = new Rect(finalSize);
+            var pos = 0.0;
 
             foreach (Control child in Children)
             {
@@ -251,32 +238,21 @@ namespace Avalonia.Controls
 
                 if (orientation == Orientation.Vertical)
                 {
-                    double width = Math.Max(childWidth, arrangedWidth);
-                    Rect childFinal = new Rect(0, arrangedHeight, width, childHeight);
-                    ArrangeChild(child, childFinal, finalSize, orientation);
-                    arrangedWidth = Math.Max(arrangedWidth, childWidth);
-                    arrangedHeight += childHeight + (child.IsVisible ? spacing : 0);
+                    var rect = new Rect(0, pos, childWidth, childHeight)
+                        .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top);
+                    ArrangeChild(child, rect, finalSize, orientation);
+                    pos += childHeight + spacing;
                 }
                 else
                 {
-                    double height = Math.Max(childHeight, arrangedHeight);
-                    Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height);
-                    ArrangeChild(child, childFinal, finalSize, orientation);
-                    arrangedWidth += childWidth + (child.IsVisible ? spacing : 0);
-                    arrangedHeight = Math.Max(arrangedHeight, childHeight);
+                    var rect = new Rect(pos, 0, childWidth, childHeight)
+                        .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment);
+                    ArrangeChild(child, rect, finalSize, orientation);
+                    pos += childWidth + spacing;
                 }
             }
 
-            if (orientation == Orientation.Vertical)
-            {
-                arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? spacing : 0), finalSize.Height);
-            }
-            else
-            {
-                arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? spacing : 0), finalSize.Width);
-            }
-
-            return new Size(arrangedWidth, arrangedHeight);
+            return finalSize;
         }
 
         internal virtual void ArrangeChild(

+ 1 - 1
src/Avalonia.Controls/Viewbox.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Controls
 
                 var scale = GetScale(availableSize, childSize, Stretch);
 
-                return childSize * scale;
+                return (childSize * scale).Constrain(availableSize);
             }
 
             return new Size();

+ 62 - 0
src/Avalonia.Layout/LayoutExtensions.cs

@@ -0,0 +1,62 @@
+using System;
+
+namespace Avalonia.Layout
+{
+    /// <summary>
+    /// Extension methods for layout types.
+    /// </summary>
+    public static class LayoutExtensions
+    {
+        /// <summary>
+        /// Aligns a rect in a constraining rect according to horizontal and vertical alignment
+        /// settings.
+        /// </summary>
+        /// <param name="rect">The rect to align.</param>
+        /// <param name="constraint">The constraining rect.</param>
+        /// <param name="horizontalAlignment">The horizontal alignment.</param>
+        /// <param name="verticalAlignment">The vertical alignment.</param>
+        /// <returns></returns>
+        public static Rect Align(
+            this Rect rect,
+            Rect constraint,
+            HorizontalAlignment horizontalAlignment,
+            VerticalAlignment verticalAlignment)
+        {
+            switch (horizontalAlignment)
+            {
+                case HorizontalAlignment.Center:
+                    rect = rect.WithX((constraint.Width - rect.Width) / 2);
+                    break;
+                case HorizontalAlignment.Right:
+                    rect = rect.WithX(constraint.Width - rect.Width);
+                    break;
+                case HorizontalAlignment.Stretch:
+                    rect = new Rect(
+                        0,
+                        rect.Y,
+                        Math.Max(constraint.Width, rect.Width),
+                        rect.Height);
+                    break;
+            }
+
+            switch (verticalAlignment)
+            {
+                case VerticalAlignment.Center:
+                    rect = rect.WithY((constraint.Height - rect.Height) / 2);
+                    break;
+                case VerticalAlignment.Bottom:
+                    rect = rect.WithY(constraint.Height - rect.Height);
+                    break;
+                case VerticalAlignment.Stretch:
+                    rect = new Rect(
+                        rect.X,
+                        0,
+                        rect.Width,
+                        Math.Max(constraint.Height, rect.Height));
+                    break;
+            }
+
+            return rect;
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Layout/Layoutable.cs

@@ -314,7 +314,7 @@ namespace Avalonia.Layout
                 try
                 {
                     _measuring = true;
-                    desiredSize = MeasureCore(availableSize).Constrain(availableSize);
+                    desiredSize = MeasureCore(availableSize);
                 }
                 finally
                 {

+ 159 - 1
tests/Avalonia.Controls.UnitTests/StackPanelTests.cs

@@ -1,7 +1,8 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using Avalonia.Controls;
+using System.Linq;
+using Avalonia.Layout;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -147,6 +148,151 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(new Rect(50, 0, 50, 120), target.Children[2].Bounds);
         }
 
+        [Fact]
+        public void Arranges_Vertical_Children_With_Correct_Bounds()
+        {
+            var target = new StackPanel
+            {
+                Orientation = Orientation.Vertical,
+                Children =
+                {
+                    new TestControl
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Left,
+                        MeasureSize = new Size(50, 10),
+                    },
+                    new TestControl
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Left,
+                        MeasureSize = new Size(150, 10),
+                    },
+                    new TestControl
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Center,
+                        MeasureSize = new Size(50, 10),
+                    },
+                    new TestControl
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Center,
+                        MeasureSize = new Size(150, 10),
+                    },
+                    new TestControl
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Right,
+                        MeasureSize = new Size(50, 10),
+                    },
+                    new TestControl
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Right,
+                        MeasureSize = new Size(150, 10),
+                    },
+                    new TestControl
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Stretch,
+                        MeasureSize = new Size(50, 10),
+                    },
+                    new TestControl
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Stretch,
+                        MeasureSize = new Size(150, 10),
+                    },
+                }
+            };
+
+            target.Measure(new Size(100, 150));
+            Assert.Equal(new Size(100, 80), target.DesiredSize);
+
+            target.Arrange(new Rect(target.DesiredSize));
+
+            var bounds = target.Children.Select(x => x.Bounds).ToArray();
+
+            Assert.Equal(
+                new[]
+                {
+                    new Rect(0, 0, 50, 10),
+                    new Rect(0, 10, 150, 10),
+                    new Rect(25, 20, 50, 10),
+                    new Rect(-25, 30, 150, 10),
+                    new Rect(50, 40, 50, 10),
+                    new Rect(-50, 50, 150, 10),
+                    new Rect(0, 60, 100, 10),
+                    new Rect(0, 70, 150, 10),
+
+                }, bounds);
+        }
+
+        [Fact]
+        public void Arranges_Horizontal_Children_With_Correct_Bounds()
+        {
+            var target = new StackPanel
+            {
+                Orientation = Orientation.Horizontal,
+                Children =
+                {
+                    new TestControl
+                    {
+                        VerticalAlignment = VerticalAlignment.Top,
+                        MeasureSize = new Size(10, 50),
+                    },
+                    new TestControl
+                    {
+                        VerticalAlignment = VerticalAlignment.Top,
+                        MeasureSize = new Size(10, 150),
+                    },
+                    new TestControl
+                    {
+                        VerticalAlignment = VerticalAlignment.Center,
+                        MeasureSize = new Size(10, 50),
+                    },
+                    new TestControl
+                    {
+                        VerticalAlignment = VerticalAlignment.Center,
+                        MeasureSize = new Size(10, 150),
+                    },
+                    new TestControl
+                    {
+                        VerticalAlignment = VerticalAlignment.Bottom,
+                        MeasureSize = new Size(10, 50),
+                    },
+                    new TestControl
+                    {
+                        VerticalAlignment = VerticalAlignment.Bottom,
+                        MeasureSize = new Size(10, 150),
+                    },
+                    new TestControl
+                    {
+                        VerticalAlignment = VerticalAlignment.Stretch,
+                        MeasureSize = new Size(10, 50),
+                    },
+                    new TestControl
+                    {
+                        VerticalAlignment = VerticalAlignment.Stretch,
+                        MeasureSize = new Size(10, 150),
+                    },
+                }
+            };
+
+            target.Measure(new Size(150, 100));
+            Assert.Equal(new Size(80, 100), target.DesiredSize);
+
+            target.Arrange(new Rect(target.DesiredSize));
+
+            var bounds = target.Children.Select(x => x.Bounds).ToArray();
+
+            Assert.Equal(
+                new[]
+                {
+                    new Rect(0, 0, 10, 50),
+                    new Rect(10, 0, 10, 150),
+                    new Rect(20, 25, 10, 50),
+                    new Rect(30, -25, 10, 150),
+                    new Rect(40, 50, 10, 50),
+                    new Rect(50, -50, 10, 150),
+                    new Rect(60, 0, 10, 100),
+                    new Rect(70, 0, 10, 150),
+                }, bounds);
+        }
+
         [Theory]
         [InlineData(Orientation.Horizontal)]
         [InlineData(Orientation.Vertical)]
@@ -185,5 +331,17 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Equal(sizeWithTwoChildren, sizeWithThreeChildren);
         }
+
+        private class TestControl : Control
+        {
+            public Size MeasureConstraint { get; private set; }
+            public Size MeasureSize { get; set; }
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                MeasureConstraint = availableSize;
+                return MeasureSize;
+            }
+        }
     }
 }