Bläddra i källkod

Fix layout in ScrollContentPresenter.

- Share common layout logic between `Border`, `ContentPresenter` and `ScrollContentPresenter`
- Added a bunch of tests for things not previously convered
- Fix `ScrollContentPresenter` child layout
Steven Kirk 7 år sedan
förälder
incheckning
46bcbacc53

+ 22 - 16
src/Avalonia.Controls/Border.cs

@@ -108,18 +108,7 @@ namespace Avalonia.Controls
         /// <returns>The desired size of the control.</returns>
         protected override Size MeasureOverride(Size availableSize)
         {
-            var child = Child;
-            var padding = Padding + new Thickness(BorderThickness);
-
-            if (child != null)
-            {
-                child.Measure(availableSize.Deflate(padding));
-                return child.DesiredSize.Inflate(padding);
-            }
-            else
-            {
-                return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
-            }
+            return MeasureOverrideImpl(availableSize, Child, Padding, BorderThickness);
         }
 
         /// <summary>
@@ -129,15 +118,32 @@ namespace Avalonia.Controls
         /// <returns>The space taken.</returns>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            var child = Child;
-
-            if (child != null)
+            if (Child != null)
             {
                 var padding = Padding + new Thickness(BorderThickness);
-                child.Arrange(new Rect(finalSize).Deflate(padding));
+                Child.Arrange(new Rect(finalSize).Deflate(padding));
             }
 
             return finalSize;
         }
+
+        internal static Size MeasureOverrideImpl(
+            Size availableSize,
+            IControl child,
+            Thickness padding,
+            double borderThickness)
+        {
+            padding += new Thickness(borderThickness);
+
+            if (child != null)
+            {
+                child.Measure(availableSize.Deflate(padding));
+                return child.DesiredSize.Inflate(padding);
+            }
+            else
+            {
+                return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
+            }
+        }
     }
 }

+ 76 - 51
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -2,14 +2,12 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Reactive.Linq;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Metadata;
-using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Presenters
 {
@@ -340,94 +338,121 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         protected override Size MeasureOverride(Size availableSize)
         {
-            var child = Child;
-            var padding = Padding + new Thickness(BorderThickness);
+            return Border.MeasureOverrideImpl(availableSize, Child, Padding, BorderThickness);
+        }
+
+        /// <inheritdoc/>
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            return ArrangeOverrideImpl(finalSize, new Vector());
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Content"/> property changes.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            _createdChild = false;
 
-            if (child != null)
+            if (((ILogical)this).IsAttachedToLogicalTree)
             {
-                child.Measure(availableSize.Deflate(padding));
-                return child.DesiredSize.Inflate(padding);
+                UpdateChild();
             }
-            else
+            else if (Child != null)
             {
-                return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
+                VisualChildren.Remove(Child);
+                LogicalChildren.Remove(Child);
+                Child = null;
+                _dataTemplate = null;
             }
+
+            InvalidateMeasure();
         }
 
-        /// <inheritdoc/>
-        protected override Size ArrangeOverride(Size finalSize)
+        internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
         {
-            var child = Child;
-
-            if (child != null)
+            if (Child != null)
             {
-                var padding = Padding + new Thickness(BorderThickness);
-                var sizeMinusPadding = finalSize.Deflate(padding);
-                var size = sizeMinusPadding;
-                var horizontalAlignment = HorizontalContentAlignment;
-                var verticalAlignment = VerticalContentAlignment;
-                var originX = padding.Left;
-                var originY = padding.Top;
-
-                if (horizontalAlignment != HorizontalAlignment.Stretch)
+                var padding = Padding;
+                var borderThickness = BorderThickness;
+                var horizontalContentAlignment = HorizontalContentAlignment;
+                var verticalContentAlignment = VerticalContentAlignment;
+                var useLayoutRounding = UseLayoutRounding;
+                var availableSizeMinusMargins = new Size(
+                    Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness),
+                    Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness));
+                var size = availableSizeMinusMargins;
+                var scale = GetLayoutScale();
+                var originX = offset.X + padding.Left + borderThickness;
+                var originY = offset.Y + padding.Top + borderThickness;
+
+                if (horizontalContentAlignment != HorizontalAlignment.Stretch)
                 {
-                    size = size.WithWidth(child.DesiredSize.Width);
+                    size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
                 }
 
-                if (verticalAlignment != VerticalAlignment.Stretch)
+                if (verticalContentAlignment != VerticalAlignment.Stretch)
                 {
-                    size = size.WithHeight(child.DesiredSize.Height);
+                    size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
                 }
 
-                switch (horizontalAlignment)
+                size = LayoutHelper.ApplyLayoutConstraints(Child, size);
+
+                if (useLayoutRounding)
+                {
+                    size = new Size(
+                        Math.Ceiling(size.Width * scale) / scale,
+                        Math.Ceiling(size.Height * scale) / scale);
+                    availableSizeMinusMargins = new Size(
+                        Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
+                        Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
+                }
+
+                switch (horizontalContentAlignment)
                 {
-                    case HorizontalAlignment.Stretch:
                     case HorizontalAlignment.Center:
-                        originX += (sizeMinusPadding.Width - size.Width) / 2;
+                    case HorizontalAlignment.Stretch:
+                        originX += (availableSizeMinusMargins.Width - size.Width) / 2;
                         break;
                     case HorizontalAlignment.Right:
-                        originX = size.Width - child.DesiredSize.Width;
+                        originX += availableSizeMinusMargins.Width - size.Width;
                         break;
                 }
 
-                switch (verticalAlignment)
+                switch (verticalContentAlignment)
                 {
-                    case VerticalAlignment.Stretch:
                     case VerticalAlignment.Center:
-                        originY += (sizeMinusPadding.Height - size.Height) / 2;
+                    case VerticalAlignment.Stretch:
+                        originY += (availableSizeMinusMargins.Height - size.Height) / 2;
                         break;
                     case VerticalAlignment.Bottom:
-                        originY = size.Height - child.DesiredSize.Height;
+                        originY += availableSizeMinusMargins.Height - size.Height;
                         break;
                 }
 
-                child.Arrange(new Rect(originX, originY, size.Width, size.Height));
+                if (useLayoutRounding)
+                {
+                    originX = Math.Floor(originX * scale) / scale;
+                    originY = Math.Floor(originY * scale) / scale;
+                }
+
+                Child.Arrange(new Rect(originX, originY, size.Width, size.Height));
             }
 
             return finalSize;
         }
 
-        /// <summary>
-        /// Called when the <see cref="Content"/> property changes.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
+        private double GetLayoutScale()
         {
-            _createdChild = false;
+            var result = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
 
-            if (((ILogical)this).IsAttachedToLogicalTree)
+            if (result == 0 || double.IsNaN(result) || double.IsInfinity(result))
             {
-                UpdateChild();
-            }
-            else if (Child != null)
-            {
-                VisualChildren.Remove(Child);
-                LogicalChildren.Remove(Child);
-                Child = null;
-                _dataTemplate = null;
+                throw new Exception($"Invalid LayoutScaling returned from {VisualRoot.GetType()}");
             }
 
-            InvalidateMeasure();
+            return result;
         }
 
         private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e)

+ 19 - 47
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@@ -48,7 +48,6 @@ namespace Avalonia.Controls.Presenters
             ScrollViewer.CanScrollHorizontallyProperty.AddOwner<ScrollContentPresenter>();
 
         private Size _extent;
-        private Size _measuredExtent;
         private Vector _offset;
         private IDisposable _logicalScrollSubscription;
         private Size _viewport;
@@ -176,63 +175,36 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         protected override Size MeasureOverride(Size availableSize)
         {
-            var child = Child;
-
-            if (child != null)
+            if (_logicalScrollSubscription != null || Child == null)
             {
-                var measureSize = availableSize;
-
-                if (_logicalScrollSubscription == null)
-                {
-                    measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
+                return base.MeasureOverride(availableSize);
+            }
 
-                    if (!CanScrollHorizontally)
-                    {
-                        measureSize = measureSize.WithWidth(availableSize.Width);
-                    }
-                }
+            var constraint = new Size(
+                CanScrollHorizontally ? double.PositiveInfinity : availableSize.Width,
+                double.PositiveInfinity);
 
-                child.Measure(measureSize);
-                var size = child.DesiredSize;
-                _measuredExtent = size;
-                return size.Constrain(availableSize);
-            }
-            else
-            {
-                return Extent = new Size();
-            }
+            Child.Measure(constraint);
+            return Child.DesiredSize.Constrain(availableSize);
         }
 
         /// <inheritdoc/>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable;
-            var logicalScroll = _logicalScrollSubscription != null;
-
-            if (!logicalScroll)
-            {
-                Viewport = finalSize;
-                Extent = _measuredExtent;
-
-                if (child != null)
-                {
-                    var size = new Size(
-                        CanScrollHorizontally ?
-                            Math.Max(finalSize.Width, child.DesiredSize.Width) :
-                            Math.Min(finalSize.Width, child.DesiredSize.Width),
-                        Math.Max(finalSize.Height, child.DesiredSize.Height));
-
-                    child.Arrange(new Rect((Point)(-Offset), size));
-                    return finalSize;
-                }
-            }
-            else if (child != null)
+            if (_logicalScrollSubscription != null || Child == null)
             {
-                child.Arrange(new Rect(finalSize));
-                return finalSize;
+                return base.ArrangeOverride(finalSize);
             }
 
-            return new Size();
+            var size = new Size(
+                CanScrollHorizontally ?
+                    Math.Max(Child.DesiredSize.Width, finalSize.Width) :
+                    finalSize.Width,
+                Math.Max(Child.DesiredSize.Height, finalSize.Height));
+            ArrangeOverrideImpl(size, -Offset);
+            Viewport = finalSize;
+            Extent = Child.Bounds.Size;
+            return finalSize;
         }
 
         /// <inheritdoc/>

+ 194 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs

@@ -0,0 +1,194 @@
+// 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.Presenters;
+using Avalonia.Layout;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Presenters
+{
+    public class ContentPresenterTests_Layout
+    {
+        [Theory]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Stretch, 0, 0, 100, 100)]
+        [InlineData(HorizontalAlignment.Left, VerticalAlignment.Stretch, 0, 0, 16, 100)]
+        [InlineData(HorizontalAlignment.Right, VerticalAlignment.Stretch, 84, 0, 16, 100)]
+        [InlineData(HorizontalAlignment.Center, VerticalAlignment.Stretch, 42, 0, 16, 100)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Top, 0, 0, 100, 16)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Bottom, 0, 84, 100, 16)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Center, 0, 42, 100, 16)]
+        public void Content_Alignment_Is_Applied_To_Child_Bounds(
+            HorizontalAlignment h,
+            VerticalAlignment v,
+            double expectedX,
+            double expectedY,
+            double expectedWidth,
+            double expectedHeight)
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                HorizontalContentAlignment = h,
+                VerticalContentAlignment = v,
+                Content = content = new Border
+                {
+                    MinWidth = 16,
+                    MinHeight = 16,
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
+        }
+
+        [Theory]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Stretch, 10, 10, 80, 80)]
+        [InlineData(HorizontalAlignment.Left, VerticalAlignment.Stretch, 10, 10, 16, 80)]
+        [InlineData(HorizontalAlignment.Right, VerticalAlignment.Stretch, 74, 10, 16, 80)]
+        [InlineData(HorizontalAlignment.Center, VerticalAlignment.Stretch, 42, 10, 16, 80)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Top, 10, 10, 80, 16)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Bottom, 10, 74, 80, 16)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Center, 10, 42, 80, 16)]
+        public void Content_Alignment_And_Padding_Are_Applied_To_Child_Bounds(
+            HorizontalAlignment h,
+            VerticalAlignment v,
+            double expectedX,
+            double expectedY,
+            double expectedWidth,
+            double expectedHeight)
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                HorizontalContentAlignment = h,
+                VerticalContentAlignment = v,
+                Padding = new Thickness(10),
+                Content = content = new Border
+                {
+                    MinWidth = 16,
+                    MinHeight = 16,
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
+        }
+
+        [Fact]
+        public void Content_Can_Be_Stretched()
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                Content = content = new Border
+                {
+                    MinWidth = 16,
+                    MinHeight = 16,
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Rect(0, 0, 100, 100), content.Bounds);
+        }
+
+        [Fact]
+        public void Content_Can_Be_Right_Aligned()
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                Content = content = new Border
+                {
+                    MinWidth = 16,
+                    MinHeight = 16,
+                    HorizontalAlignment = HorizontalAlignment.Right
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Rect(84, 0, 16, 100), content.Bounds);
+        }
+
+        [Fact]
+        public void Content_Can_Be_Bottom_Aligned()
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                Content = content = new Border
+                {
+                    MinWidth = 16,
+                    MinHeight = 16,
+                    VerticalAlignment = VerticalAlignment.Bottom,
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Rect(0, 84, 100, 16), content.Bounds);
+        }
+
+        [Fact]
+        public void Content_Can_Be_TopLeft_Aligned()
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                Content = content = new Border
+                {
+                    MinWidth = 16,
+                    MinHeight = 16,
+                    HorizontalAlignment = HorizontalAlignment.Right,
+                    VerticalAlignment = VerticalAlignment.Top,
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds);
+        }
+
+        [Fact]
+        public void Content_Can_Be_TopRight_Aligned()
+        {
+            Border content;
+            var target = new ContentPresenter
+            {
+                Content = content = new Border
+                {
+                    MinWidth = 16,
+                    MinHeight = 16,
+                    HorizontalAlignment = HorizontalAlignment.Right,
+                    VerticalAlignment = VerticalAlignment.Top,
+                },
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds);
+        }
+
+        [Fact]
+        public void Padding_Is_Applied_To_TopLeft_Aligned_Content()
+        {
+        }
+    }
+}

+ 2 - 2
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@@ -204,7 +204,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
             scroll.Arrange(new Rect(0, 0, 100, 100));
 
             Assert.Equal(20, target.Panel.Children.Count);
-            Assert.Equal(new Size(10, 200), scroll.Extent);
+            Assert.Equal(new Size(100, 200), scroll.Extent);
             Assert.Equal(new Size(100, 100), scroll.Viewport);
 
             target.VirtualizationMode = ItemVirtualizationMode.Simple;
@@ -266,7 +266,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
             scroll.Arrange(new Rect(0, 0, 100, 100));
 
             Assert.Equal(20, target.Panel.Children.Count);
-            Assert.Equal(new Size(10, 200), scroll.Extent);
+            Assert.Equal(new Size(100, 200), scroll.Extent);
             Assert.Equal(new Size(100, 100), scroll.Viewport);
         }
 

+ 72 - 57
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@@ -12,55 +12,32 @@ namespace Avalonia.Controls.UnitTests.Presenters
 {
     public class ScrollContentPresenterTests
     {
-        [Fact]
-        public void Content_Can_Be_Left_Aligned()
-        {
-            Border content;
-            var target = new ScrollContentPresenter
-            {
-                Content = content = new Border
-                {
-                    Padding = new Thickness(8),
-                    HorizontalAlignment = HorizontalAlignment.Left
-                },
-            };
-
-            target.UpdateChild();
-            target.Measure(new Size(100, 100));
-            target.Arrange(new Rect(0, 0, 100, 100));
-
-            Assert.Equal(new Rect(0, 0, 16, 100), content.Bounds);
-        }
-
-        [Fact]
-        public void Content_Can_Be_Stretched()
-        {
-            Border content;
-            var target = new ScrollContentPresenter
-            {
-                Content = content = new Border
-                {
-                    Padding = new Thickness(8),
-                },
-            };
-
-            target.UpdateChild();
-            target.Measure(new Size(100, 100));
-            target.Arrange(new Rect(0, 0, 100, 100));
-
-            Assert.Equal(new Rect(0, 0, 100, 100), content.Bounds);
-        }
-
-        [Fact]
-        public void Content_Can_Be_Right_Aligned()
+        [Theory]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Stretch, 10, 10, 80, 80)]
+        [InlineData(HorizontalAlignment.Left, VerticalAlignment.Stretch, 10, 10, 16, 80)]
+        [InlineData(HorizontalAlignment.Right, VerticalAlignment.Stretch, 74, 10, 16, 80)]
+        [InlineData(HorizontalAlignment.Center, VerticalAlignment.Stretch, 42, 10, 16, 80)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Top, 10, 10, 80, 16)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Bottom, 10, 74, 80, 16)]
+        [InlineData(HorizontalAlignment.Stretch, VerticalAlignment.Center, 10, 42, 80, 16)]
+        public void Alignment_And_Padding_Are_Applied_To_Child_Bounds(
+            HorizontalAlignment h,
+            VerticalAlignment v,
+            double expectedX,
+            double expectedY,
+            double expectedWidth,
+            double expectedHeight)
         {
             Border content;
             var target = new ScrollContentPresenter
             {
+                Padding = new Thickness(10),
                 Content = content = new Border
                 {
-                    Padding = new Thickness(8),
-                    HorizontalAlignment = HorizontalAlignment.Right
+                    MinWidth = 16,
+                    MinHeight = 16,
+                    HorizontalAlignment = h,
+                    VerticalAlignment = v,
                 },
             };
 
@@ -68,19 +45,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Measure(new Size(100, 100));
             target.Arrange(new Rect(0, 0, 100, 100));
 
-            Assert.Equal(new Rect(84, 0, 16, 100), content.Bounds);
+            Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
         }
 
         [Fact]
-        public void Content_Can_Be_Bottom_Aligned()
+        public void DesiredSize_Is_Content_Size_When_Smaller_Than_AvailableSize()
         {
-            Border content;
             var target = new ScrollContentPresenter
             {
-                Content = content = new Border
+                Padding = new Thickness(10),
+                Content = new Border
                 {
-                    Padding = new Thickness(8),
-                    VerticalAlignment = VerticalAlignment.Bottom,
+                    MinWidth = 16,
+                    MinHeight = 16,
                 },
             };
 
@@ -88,20 +65,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Measure(new Size(100, 100));
             target.Arrange(new Rect(0, 0, 100, 100));
 
-            Assert.Equal(new Rect(0, 84, 100, 16), content.Bounds);
+            Assert.Equal(new Size(16, 16), target.DesiredSize);
         }
 
         [Fact]
-        public void Content_Can_Be_TopRight_Aligned()
+        public void DesiredSize_Is_AvailableSize_When_Content_Larger_Than_AvailableSize()
         {
-            Border content;
             var target = new ScrollContentPresenter
             {
-                Content = content = new Border
+                Padding = new Thickness(10),
+                Content = new Border
                 {
-                    Padding = new Thickness(8),
-                    HorizontalAlignment = HorizontalAlignment.Right,
-                    VerticalAlignment = VerticalAlignment.Top,
+                    MinWidth = 160,
+                    MinHeight = 160,
                 },
             };
 
@@ -109,7 +85,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
             target.Measure(new Size(100, 100));
             target.Arrange(new Rect(0, 0, 100, 100));
 
-            Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds);
+            Assert.Equal(new Size(100, 100), target.DesiredSize);
         }
 
         [Fact]
@@ -201,6 +177,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Equal(new[] { "Viewport", "Extent" }, set);
         }
 
+        [Fact]
+        public void Should_Correctly_Arrange_Child_Larger_Than_Viewport()
+        {
+            var child = new Canvas { MinWidth = 150, MinHeight = 150 };
+            var target = new ScrollContentPresenter { Content = child, };
+
+            target.UpdateChild();
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Size(150, 150), child.Bounds.Size);
+        }
+
         [Fact]
         public void Arrange_Should_Constrain_Child_Width_When_CanScrollHorizontally_False()
         {
@@ -227,6 +216,32 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Equal(100, child.Bounds.Width);
         }
 
+        [Fact]
+        public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False()
+        {
+            var child = new WrapPanel
+            {
+                Children =
+                {
+                    new Border { Width = 40, Height = 50 },
+                    new Border { Width = 40, Height = 50 },
+                    new Border { Width = 40, Height = 50 },
+                }
+            };
+
+            var target = new ScrollContentPresenter
+            {
+                Content = child,
+                CanScrollHorizontally = false,
+            };
+
+            target.UpdateChild();
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Size(100, 100), target.Extent);
+        }
+
         [Fact]
         public void Setting_Offset_Should_Invalidate_Arrange()
         {

+ 110 - 0
tests/Avalonia.Layout.UnitTests/LayoutableTests.cs

@@ -7,6 +7,97 @@ namespace Avalonia.Layout.UnitTests
 {
     public class LayoutableTests
     {
+        [Theory]
+        [InlineData(0, 0, 0, 0, 100, 100)]
+        [InlineData(10, 0, 0, 0, 90, 100)]
+        [InlineData(10, 0, 5, 0, 85, 100)]
+        [InlineData(0, 10, 0, 0, 100, 90)]
+        [InlineData(0, 10, 0, 5, 100, 85)]
+        [InlineData(4, 4, 6, 7, 90, 89)]
+        public void Margin_Is_Applied_To_MeasureOverride_Size(
+            double l,
+            double t,
+            double r,
+            double b,
+            double expectedWidth,
+            double expectedHeight)
+        {
+            var target = new TestLayoutable
+            {
+                Margin = new Thickness(l, t, r, b),
+            };
+
+            target.Measure(new Size(100, 100));
+
+            Assert.Equal(new Size(expectedWidth, expectedHeight), target.MeasureSize);
+        }
+
+        [Theory]
+        [InlineData(HorizontalAlignment.Stretch, 100)]
+        [InlineData(HorizontalAlignment.Left, 10)]
+        [InlineData(HorizontalAlignment.Center, 10)]
+        [InlineData(HorizontalAlignment.Right, 10)]
+        public void HorizontalAlignment_Is_Applied_To_ArrangeOverride_Size(
+            HorizontalAlignment h,
+            double expectedWidth)
+        {
+            var target = new TestLayoutable
+            {
+                HorizontalAlignment = h,
+            };
+
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Size(expectedWidth, 100), target.ArrangeSize);
+        }
+
+        [Theory]
+        [InlineData(VerticalAlignment.Stretch, 100)]
+        [InlineData(VerticalAlignment.Top, 10)]
+        [InlineData(VerticalAlignment.Center, 10)]
+        [InlineData(VerticalAlignment.Bottom, 10)]
+        public void VerticalAlignment_Is_Applied_To_ArrangeOverride_Size(
+            VerticalAlignment v,
+            double expectedHeight)
+        {
+            var target = new TestLayoutable
+            {
+                VerticalAlignment = v,
+            };
+
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Size(100, expectedHeight), target.ArrangeSize);
+        }
+
+        [Theory]
+        [InlineData(0, 0, 0, 0, 100, 100)]
+        [InlineData(10, 0, 0, 0, 90, 100)]
+        [InlineData(10, 0, 5, 0, 85, 100)]
+        [InlineData(0, 10, 0, 0, 100, 90)]
+        [InlineData(0, 10, 0, 5, 100, 85)]
+        [InlineData(4, 4, 6, 7, 90, 89)]
+        public void Margin_Is_Applied_To_ArrangeOverride_Size(
+            double l,
+            double t,
+            double r,
+            double b,
+            double expectedWidth,
+            double expectedHeight)
+        {
+            var target = new TestLayoutable
+            {
+                Margin = new Thickness(l, t, r, b),
+            };
+
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(0, 0, 100, 100));
+
+            Assert.Equal(new Size(expectedWidth, expectedHeight), target.ArrangeSize);
+        }
+
         [Fact]
         public void Only_Calls_LayoutManager_InvalidateMeasure_Once()
         {
@@ -86,5 +177,24 @@ namespace Avalonia.Layout.UnitTests
             AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
             return result;
         }
+
+        private class TestLayoutable : Layoutable
+        {
+            public Size ArrangeSize { get; private set; }
+            public Size MeasureResult { get; set; } = new Size(10, 10);
+            public Size MeasureSize { get; private set; }
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                MeasureSize = availableSize;
+                return MeasureResult;
+            }
+
+            protected override Size ArrangeOverride(Size finalSize)
+            {
+                ArrangeSize = finalSize;
+                return base.ArrangeOverride(finalSize);
+            }
+        }
     }
 }