From 66c813c420b330d135a6c08598ff0556201bcc1e Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Tue, 25 Jun 2019 13:33:53 +0200 Subject: [PATCH 1/5] Initial port of WPF StackPanel --- src/Avalonia.Controls/StackPanel.cs | 148 ++++++++++++++++------------ 1 file changed, 83 insertions(+), 65 deletions(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c29faa1b4d..59c3c33942 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -1,10 +1,10 @@ -// 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. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Linq; using Avalonia.Input; -using Avalonia.Layout; namespace Avalonia.Controls { @@ -155,106 +155,124 @@ namespace Avalonia.Controls } /// - /// Measures the control. + /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content). + /// Children in this dimension are encouraged to be as large as they like. In the other dimension, + /// StackPanel will assume the maximum size of its children. /// - /// The available size. - /// The desired size of the control. + /// Constraint + /// Desired size protected override Size MeasureOverride(Size availableSize) { - double childAvailableWidth = double.PositiveInfinity; - double childAvailableHeight = double.PositiveInfinity; + Size stackDesiredSize = new Size(); + var children = Children; + Size layoutSlotSize = availableSize; + bool fHorizontal = (Orientation == Orientation.Horizontal); + double spacing = Spacing; + bool hasVisibleChild = false; - if (Orientation == Orientation.Vertical) + // + // Initialize child sizing and iterator data + // Allow children as much size as they want along the stack. + // + if (fHorizontal) { - childAvailableWidth = availableSize.Width; - - if (!double.IsNaN(Width)) - { - childAvailableWidth = Width; - } - - childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth); - childAvailableWidth = Math.Max(childAvailableWidth, MinWidth); + layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity); } else { - childAvailableHeight = availableSize.Height; + layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity); + } - if (!double.IsNaN(Height)) - { - childAvailableHeight = Height; - } + // + // Iterate through children. + // While we still supported virtualization, this was hidden in a child iterator (see source history). + // + for (int i = 0, count = children.Count; i < count; ++i) + { + // Get next child. + var child = children[i]; - childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight); - childAvailableHeight = Math.Max(childAvailableHeight, MinHeight); - } + if (child == null) + { continue; } - double measuredWidth = 0; - double measuredHeight = 0; - double spacing = Spacing; - bool hasVisibleChild = Children.Any(c => c.IsVisible); + bool isVisible = child.IsVisible; - foreach (Control child in Children) - { - child.Measure(new Size(childAvailableWidth, childAvailableHeight)); - Size size = child.DesiredSize; + if (isVisible && !hasVisibleChild) + { + hasVisibleChild = true; + } - if (Orientation == Orientation.Vertical) + // Measure the child. + child.Measure(layoutSlotSize); + Size childDesiredSize = child.DesiredSize; + + // Accumulate child size. + if (fHorizontal) { - measuredHeight += size.Height + (child.IsVisible ? spacing : 0); - measuredWidth = Math.Max(measuredWidth, size.Width); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width); + stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height)); } else { - measuredWidth += size.Width + (child.IsVisible ? spacing : 0); - measuredHeight = Math.Max(measuredHeight, size.Height); + stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height); } } - if (Orientation == Orientation.Vertical) + if (fHorizontal) { - measuredHeight -= (hasVisibleChild ? spacing : 0); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0)); } else - { - measuredWidth -= (hasVisibleChild ? spacing : 0); + { + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } - return new Size(measuredWidth, measuredHeight).Constrain(availableSize); + // TODO: In WPF `.Constrain(availableSize)` is not used. + //return stackDesiredSize; + return stackDesiredSize.Constrain(availableSize); } - /// + /// + /// Content arrangement. + /// + /// Arrange size protected override Size ArrangeOverride(Size finalSize) { - var orientation = Orientation; + var children = Children; + bool fHorizontal = (Orientation == Orientation.Horizontal); + Rect rcChild = new Rect(finalSize); + double previousChildSize = 0.0; var spacing = Spacing; - var finalRect = new Rect(finalSize); - var pos = 0.0; - foreach (Control child in Children) + // + // Arrange and Position Children. + // + for (int i = 0, count = children.Count; i < count; ++i) { - if (!child.IsVisible) - { - continue; - } + var child = children[i]; - double childWidth = child.DesiredSize.Width; - double childHeight = child.DesiredSize.Height; + if (child == null) + { continue; } - if (orientation == Orientation.Vertical) + if (fHorizontal) { - var rect = new Rect(0, pos, childWidth, childHeight) - .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top); - ArrangeChild(child, rect, finalSize, orientation); - pos += childHeight + spacing; + rcChild = rcChild.WithX(rcChild.X + previousChildSize); + previousChildSize = child.DesiredSize.Width; + rcChild = rcChild.WithWidth(previousChildSize); + rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height)); + previousChildSize += spacing; } else { - var rect = new Rect(pos, 0, childWidth, childHeight) - .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment); - ArrangeChild(child, rect, finalSize, orientation); - pos += childWidth + spacing; + rcChild = rcChild.WithY(rcChild.Y + previousChildSize); + previousChildSize = child.DesiredSize.Height; + rcChild = rcChild.WithHeight(previousChildSize); + rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width)); + previousChildSize += spacing; } + + child.Arrange(rcChild); } return finalSize; From c99f553c182f2d5c92a71a5fa6fbf551c4082ad3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 30 Jul 2019 15:48:34 +0200 Subject: [PATCH 2/5] Fix merge error. `Orientation` was moved to `Avalonia.Layout`. --- src/Avalonia.Controls/StackPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index e7e0eb3f3d..d0841ea55d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls { From 580bf42afa22c4cfc41382fc48dd18d07428f1fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 30 Jul 2019 15:53:28 +0200 Subject: [PATCH 3/5] Call ArrangeChild in StackPanel. So that derived classes can override the arrangement (needed for `VirtualizingStackPanel`). --- src/Avalonia.Controls/StackPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index d0841ea55d..f378f47e64 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -273,7 +273,7 @@ namespace Avalonia.Controls previousChildSize += spacing; } - child.Arrange(rcChild); + ArrangeChild(child, rcChild, finalSize, Orientation); } return finalSize; From 51ec592e4aab3f711e6f2e8327f06bd1524bf1ba Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 30 Jul 2019 17:01:19 +0200 Subject: [PATCH 4/5] Added failing test for #2746. --- .../Avalonia.Controls.UnitTests/GridTests.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index df804d5d8c..2b9197e20b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1357,5 +1357,36 @@ namespace Avalonia.Controls.UnitTests PrintColumnDefinitions(grid); Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth)); } + + [Fact] + public void Correct_Grid_Bounds_When_Child_Control_Has_DesiredSize_Larger_Than_Available_Space() + { + // Issue #2746 + var grid = new Grid + { + RowDefinitions = RowDefinitions.Parse("Auto"), + Children = + { + new TestControl + { + MeasureSize = new Size(150, 150), + } + } + }; + + var parent = new Decorator { Child = grid }; + + parent.Measure(new Size(100, 100)); + parent.Arrange(new Rect(grid.DesiredSize)); + + Assert.Equal(new Size(100, 100), grid.Bounds.Size); + } + + private class TestControl : Control + { + public Size MeasureSize { get; set; } + + protected override Size MeasureOverride(Size availableSize) => MeasureSize; + } } -} \ No newline at end of file +} From b4d7d03afd7eb18e871823f24bf8ee8474bf632d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 30 Jul 2019 17:04:08 +0200 Subject: [PATCH 5/5] Constrain to availableSize in MeasureCore. #2431 erroneously removed the `.Constrain(availableSize)` call in `Layoutable.Measure`. Now that the WPF source is available, I can see i that WPF does in fact constrain measure to availableSize and Grid relies on this. Put constraint back in, undo the changes to the controls changed in #2431 (`StackPanel`, `Image`) and update the expected test results based on cross-checks with WPF in https://github.com/wieslawsoltes/WpfUnitTests/pull/1. --- src/Avalonia.Controls/Image.cs | 2 +- src/Avalonia.Controls/StackPanel.cs | 4 +--- src/Avalonia.Layout/Layoutable.cs | 3 +++ .../StackPanelTests.cs | 16 ++++++++-------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index fa6f5787be..ff6cd482df 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls } } - return result.Constrain(availableSize); + return result; } /// diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index f378f47e64..bd3441078d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -229,9 +229,7 @@ namespace Avalonia.Controls stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } - // TODO: In WPF `.Constrain(availableSize)` is not used. - //return stackDesiredSize; - return stackDesiredSize.Constrain(availableSize); + return stackDesiredSize; } /// diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 662f48ec44..bd248d6d44 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -534,6 +534,9 @@ namespace Avalonia.Layout height = Math.Min(height, MaxHeight); height = Math.Max(height, MinHeight); + width = Math.Min(width, availableSize.Width); + height = Math.Min(height, availableSize.Height); + if (UseLayoutRounding) { var scale = GetLayoutScale(); diff --git a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs index 722ad1c8ee..db113f0569 100644 --- a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs @@ -210,13 +210,13 @@ namespace Avalonia.Controls.UnitTests new[] { new Rect(0, 0, 50, 10), - new Rect(0, 10, 150, 10), + new Rect(0, 10, 100, 10), new Rect(25, 20, 50, 10), - new Rect(-25, 30, 150, 10), + new Rect(0, 30, 100, 10), new Rect(50, 40, 50, 10), - new Rect(-50, 50, 150, 10), + new Rect(0, 50, 100, 10), new Rect(0, 60, 100, 10), - new Rect(0, 70, 150, 10), + new Rect(0, 70, 100, 10), }, bounds); } @@ -283,13 +283,13 @@ namespace Avalonia.Controls.UnitTests new[] { new Rect(0, 0, 10, 50), - new Rect(10, 0, 10, 150), + new Rect(10, 0, 10, 100), new Rect(20, 25, 10, 50), - new Rect(30, -25, 10, 150), + new Rect(30, 0, 10, 100), new Rect(40, 50, 10, 50), - new Rect(50, -50, 10, 150), + new Rect(50, 0, 10, 100), new Rect(60, 0, 10, 100), - new Rect(70, 0, 10, 150), + new Rect(70, 0, 10, 100), }, bounds); }