diff --git a/src/Compatibility/Core/src/Android/Renderers/PageContainer.cs b/src/Compatibility/Core/src/Android/Renderers/PageContainer.cs index e5b0370a6158..c395f1d17568 100644 --- a/src/Compatibility/Core/src/Android/Renderers/PageContainer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/PageContainer.cs @@ -46,7 +46,11 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b) (l, t, r, b) = Context.ToPixels(ipc.ContainerArea); } - pageViewGroup.Measure(r - l, b - t); + var mode = MeasureSpecMode.Exactly; + var widthSpec = mode.MakeMeasureSpec(r - l); + var heightSpec = mode.MakeMeasureSpec(b - t); + + pageViewGroup.Measure(widthSpec, heightSpec); pageViewGroup.Layout(l, t, r, b); } } diff --git a/src/Compatibility/Core/src/Android/VisualElementTracker.cs b/src/Compatibility/Core/src/Android/VisualElementTracker.cs index 660db4db1337..6ab1c43d33e7 100644 --- a/src/Compatibility/Core/src/Android/VisualElementTracker.cs +++ b/src/Compatibility/Core/src/Android/VisualElementTracker.cs @@ -96,7 +96,7 @@ public void UpdateLayout() formsViewGroup.MeasureAndLayout(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(height, MeasureSpecMode.Exactly), x, y, x + width, y + height); Performance.Stop(reference, "MeasureAndLayout"); } - else if (aview is LayoutViewGroup && width == 0 && height == 0) + else if ((aview is LayoutViewGroup || aview is PageViewGroup) && width == 0 && height == 0) { // Nothing to do here; just chill. } diff --git a/src/Compatibility/Core/src/AppHostBuilderExtensions.cs b/src/Compatibility/Core/src/AppHostBuilderExtensions.cs index 1ad2e6a24c10..7b9a128cc4b2 100644 --- a/src/Compatibility/Core/src/AppHostBuilderExtensions.cs +++ b/src/Compatibility/Core/src/AppHostBuilderExtensions.cs @@ -139,10 +139,6 @@ static MauiAppBuilder SetupDefaults(this MauiAppBuilder builder) // This is for Layouts that currently don't work when assigned to LayoutHandler handlers.TryAddCompatibilityRenderer(typeof(ContentView), typeof(DefaultRenderer)); -#if __IOS__ - handlers.TryAddCompatibilityRenderer(typeof(AbsoluteLayout), typeof(DefaultRenderer)); -#endif - DependencyService.Register(); DependencyService.Register(); diff --git a/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs b/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs index 3c5220fa0fe7..db09c69f1349 100644 --- a/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs @@ -56,8 +56,11 @@ protected override Size MeasureOverride(double widthConstraint, double heightCon // The value from ComputeDesiredSize won't account for any margins on the Content; we'll need to do that manually // And we'll use ResolveConstraints to make sure we're sticking within and explicit Height/Width values or externally // imposed constraints - var desiredWidth = ResolveConstraints(widthConstraint, Width, defaultSize.Width + contentMargin.HorizontalThickness); - var desiredHeight = ResolveConstraints(heightConstraint, Height, defaultSize.Height + contentMargin.VerticalThickness); + var width = (this as IView).Width; + var height = (this as IView).Height; + + var desiredWidth = ResolveConstraints(widthConstraint, width, defaultSize.Width + contentMargin.HorizontalThickness); + var desiredHeight = ResolveConstraints(heightConstraint, height, defaultSize.Height + contentMargin.VerticalThickness); DesiredSize = new Size(desiredWidth, desiredHeight); return DesiredSize; diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs index 4694020d6ef2..880f47355f11 100644 --- a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs @@ -1,4 +1,5 @@ -using Microsoft.Maui.Graphics; +using System; +using Microsoft.Maui.Graphics; using Microsoft.Maui.Layouts; namespace Microsoft.Maui.Controls @@ -118,8 +119,99 @@ Semantics IView.Semantics internal Semantics SetupSemantics() => _semantics ??= new Semantics(); - double IView.Width => WidthRequest; - double IView.Height => HeightRequest; + static void ValidatePositive(double value, string name) + { + if (value < 0) + { + throw new InvalidOperationException($"{name} cannot be less than zero."); + } + } + + double IView.Width + { + get + { + if (!IsSet(WidthRequestProperty)) + { + return Primitives.Dimension.Unset; + } + + // Access once up front to avoid multiple GetValue calls + var value = WidthRequest; + ValidatePositive(value, nameof(IView.Width)); + return value; + } + } + + double IView.Height + { + get + { + if (!IsSet(HeightRequestProperty)) + { + return Primitives.Dimension.Unset; + } + + // Access once up front to avoid multiple GetValue calls + var value = HeightRequest; + ValidatePositive(value, nameof(IView.Height)); + return value; + } + } + + double IView.MinimumWidth + { + get + { + if (!IsSet(MinimumWidthRequestProperty)) + { + return Primitives.Dimension.Minimum; + } + + // Access once up front to avoid multiple GetValue calls + var value = MinimumWidthRequest; + ValidatePositive(value, nameof(IView.MinimumWidth)); + return value; + } + } + + double IView.MinimumHeight + { + get + { + if (!IsSet(MinimumHeightRequestProperty)) + { + return Primitives.Dimension.Minimum; + } + + // Access once up front to avoid multiple GetValue calls + var value = MinimumHeightRequest; + ValidatePositive(value, nameof(IView.MinimumHeight)); + return value; + } + } + + double IView.MaximumWidth + { + get + { + // Access once up front to avoid multiple GetValue calls + var value = MaximumWidthRequest; + ValidatePositive(value, nameof(IView.MaximumWidth)); + return value; + } + } + + double IView.MaximumHeight + { + get + { + // Access once up front to avoid multiple GetValue calls + var value = MaximumHeightRequest; + ValidatePositive(value, nameof(IView.MaximumHeight)); + return value; + } + } Thickness IView.Margin => Thickness.Zero; } diff --git a/src/Controls/src/Core/VisualElement.cs b/src/Controls/src/Core/VisualElement.cs index e94321cc579e..15bdc76078c0 100644 --- a/src/Controls/src/Core/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement.cs @@ -272,13 +272,17 @@ void InvalidateGradientBrushRequested(object sender, EventArgs e) public static readonly BindableProperty TriggersProperty = TriggersPropertyKey.BindableProperty; - public static readonly BindableProperty WidthRequestProperty = BindableProperty.Create("WidthRequest", typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); + public static readonly BindableProperty WidthRequestProperty = BindableProperty.Create(nameof(WidthRequest), typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); - public static readonly BindableProperty HeightRequestProperty = BindableProperty.Create("HeightRequest", typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); + public static readonly BindableProperty HeightRequestProperty = BindableProperty.Create(nameof(HeightRequest), typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); - public static readonly BindableProperty MinimumWidthRequestProperty = BindableProperty.Create("MinimumWidthRequest", typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); + public static readonly BindableProperty MinimumWidthRequestProperty = BindableProperty.Create(nameof(MinimumWidthRequest), typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); - public static readonly BindableProperty MinimumHeightRequestProperty = BindableProperty.Create("MinimumHeightRequest", typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); + public static readonly BindableProperty MinimumHeightRequestProperty = BindableProperty.Create(nameof(MinimumHeightRequest), typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); + + public static readonly BindableProperty MaximumWidthRequestProperty = BindableProperty.Create(nameof(MaximumWidthRequest), typeof(double), typeof(VisualElement), double.PositiveInfinity, propertyChanged: OnRequestChanged); + + public static readonly BindableProperty MaximumHeightRequestProperty = BindableProperty.Create(nameof(MaximumHeightRequest), typeof(double), typeof(VisualElement), double.PositiveInfinity, propertyChanged: OnRequestChanged); [EditorBrowsable(EditorBrowsableState.Never)] public static readonly BindablePropertyKey IsFocusedPropertyKey = BindableProperty.CreateReadOnly("IsFocused", @@ -436,6 +440,18 @@ public double MinimumWidthRequest set { SetValue(MinimumWidthRequestProperty, value); } } + public double MaximumHeightRequest + { + get { return (double)GetValue(MaximumHeightRequestProperty); } + set { SetValue(MaximumHeightRequestProperty, value); } + } + + public double MaximumWidthRequest + { + get { return (double)GetValue(MaximumWidthRequestProperty); } + set { SetValue(MaximumWidthRequestProperty, value); } + } + public double Opacity { get { return (double)GetValue(OpacityProperty); } @@ -1038,6 +1054,10 @@ static void OnRequestChanged(BindableObject bindable, object oldvalue, object ne { fe.Handler?.UpdateValue(nameof(IView.Width)); fe.Handler?.UpdateValue(nameof(IView.Height)); + fe.Handler?.UpdateValue(nameof(IView.MinimumHeight)); + fe.Handler?.UpdateValue(nameof(IView.MinimumWidth)); + fe.Handler?.UpdateValue(nameof(IView.MaximumHeight)); + fe.Handler?.UpdateValue(nameof(IView.MaximumWidth)); } ((VisualElement)bindable).InvalidateMeasureInternal(InvalidationTrigger.SizeRequestChanged); diff --git a/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs b/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs index 8c5c1bc89154..ce173b2cdf79 100644 --- a/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs +++ b/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs @@ -41,7 +41,7 @@ public void GridInsideStackLayout() var stackLayout = new StackLayout() { IsPlatformEnabled = true }; var grid = new Grid() { IsPlatformEnabled = true, HeightRequest = 50 }; var label = new Label() { IsPlatformEnabled = true }; - var expectedSize = new Size(50, 50); + var expectedSize = new Size(100, 50); var view = Substitute.For(); view.GetDesiredSize(default, default).ReturnsForAnyArgs(expectedSize); @@ -51,7 +51,7 @@ public void GridInsideStackLayout() grid.Children.Add(label); contentPage.Content = stackLayout; - var rect = new Rectangle(0, 0, 50, 100); + var rect = new Rectangle(Point.Zero, expectedSize); (contentPage as IView).Measure(expectedSize.Width, expectedSize.Height); (contentPage as IView).Arrange(rect); diff --git a/src/Core/src/Core/IView.cs b/src/Core/src/Core/IView.cs index ed74327d9c7a..c978d4a187f6 100644 --- a/src/Core/src/Core/IView.cs +++ b/src/Core/src/Core/IView.cs @@ -60,20 +60,40 @@ public interface IView : IElement, ITransform Paint? Background { get; } /// - /// Gets the bounds of the View. + /// Gets the bounds of the View within its container. /// Rectangle Frame { get; set; } /// - /// Gets the specified width of this View. + /// Gets the specified width of the IView. /// double Width { get; } /// - /// Gets the specified height of this View. + /// Gets the specified minimum width constraint of the IView, between zero and double.PositiveInfinity. + /// + double MinimumWidth { get; } + + /// + /// Gets the specified maximum width constraint of the IView, between zero and double.PositiveInfinity. + /// + double MaximumWidth { get; } + + /// + /// Gets the specified height of the IView. /// double Height { get; } + /// + /// Gets the specified minimum height constraint of the IView, between zero and double.PositiveInfinity. + /// + double MinimumHeight { get; } + + /// + /// Gets the specified maximum height constraint of the IView, between zero and double.PositiveInfinity. + /// + double MaximumHeight { get; } + /// /// The Margin represents the distance between an view and its adjacent views. /// diff --git a/src/Core/src/Handlers/View/ViewHandler.cs b/src/Core/src/Handlers/View/ViewHandler.cs index 90748d3cda47..fc10a53cd1db 100644 --- a/src/Core/src/Handlers/View/ViewHandler.cs +++ b/src/Core/src/Handlers/View/ViewHandler.cs @@ -22,6 +22,10 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler [nameof(IView.FlowDirection)] = MapFlowDirection, [nameof(IView.Width)] = MapWidth, [nameof(IView.Height)] = MapHeight, + [nameof(IView.MinimumHeight)] = MapMinimumHeight, + [nameof(IView.MaximumHeight)] = MapMaximumHeight, + [nameof(IView.MinimumWidth)] = MapMinimumWidth, + [nameof(IView.MaximumWidth)] = MapMaximumWidth, [nameof(IView.IsEnabled)] = MapIsEnabled, [nameof(IView.Opacity)] = MapOpacity, [nameof(IView.Semantics)] = MapSemantics, @@ -138,6 +142,26 @@ public static void MapHeight(ViewHandler handler, IView view) ((NativeView?)handler.NativeView)?.UpdateHeight(view); } + public static void MapMinimumHeight(ViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateMinimumHeight(view); + } + + public static void MapMaximumHeight(ViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateMaximumHeight(view); + } + + public static void MapMinimumWidth(ViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateMinimumWidth(view); + } + + public static void MapMaximumWidth(ViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateMaximumWidth(view); + } + public static void MapIsEnabled(ViewHandler handler, IView view) { ((NativeView?)handler.NativeView)?.UpdateIsEnabled(view); diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs index f1d9ecdd22a1..0ffaf1149906 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs @@ -3,6 +3,7 @@ using Android.Content; using Android.Views; using Microsoft.Maui.Graphics; +using static Microsoft.Maui.Primitives.Dimension; namespace Microsoft.Maui.Handlers { @@ -26,7 +27,7 @@ public override void NativeArrange(Rectangle frame) { var nativeView = WrappedNativeView; - if (nativeView == null || Context == null) + if (nativeView == null || MauiContext == null || Context == null) { return; } @@ -55,8 +56,8 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra } // Create a spec to handle the native measure - var widthSpec = CreateMeasureSpec(widthConstraint, VirtualView.Width); - var heightSpec = CreateMeasureSpec(heightConstraint, VirtualView.Height); + var widthSpec = CreateMeasureSpec(widthConstraint, VirtualView.Width, VirtualView.MaximumWidth); + var heightSpec = CreateMeasureSpec(heightConstraint, VirtualView.Height, VirtualView.MaximumHeight); nativeView.Measure(widthSpec, heightSpec); @@ -64,16 +65,21 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra return Context.FromPixels(nativeView.MeasuredWidth, nativeView.MeasuredHeight); } - int CreateMeasureSpec(double constraint, double explicitSize) + int CreateMeasureSpec(double constraint, double explicitSize, double maximumSize) { var mode = MeasureSpecMode.AtMost; - if (explicitSize >= 0) + if (IsExplicitSet(explicitSize)) { // We have a set value (i.e., a Width or Height) mode = MeasureSpecMode.Exactly; constraint = explicitSize; } + else if (IsMaximumSet(maximumSize)) + { + mode = MeasureSpecMode.AtMost; + constraint = maximumSize; + } else if (double.IsInfinity(constraint)) { // We've got infinite space; we'll leave the size up to the native control diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs index 6cb2adff38ff..ed086b507f56 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs @@ -1,5 +1,6 @@ using Microsoft.Maui.Graphics; using UIKit; +using static Microsoft.Maui.Primitives.Dimension; namespace Microsoft.Maui.Handlers { @@ -46,11 +47,6 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra return new Size(widthConstraint, heightConstraint); } - var explicitWidth = VirtualView.Width; - var explicitHeight = VirtualView.Height; - var hasExplicitWidth = explicitWidth >= 0; - var hasExplicitHeight = explicitHeight >= 0; - var sizeThatFits = nativeView.SizeThatFits(new CoreGraphics.CGSize((float)widthConstraint, (float)heightConstraint)); var size = new Size( @@ -63,8 +59,37 @@ public override Size GetDesiredSize(double widthConstraint, double heightConstra size = new Size(nativeView.Frame.Width, nativeView.Frame.Height); } - return new Size(hasExplicitWidth ? explicitWidth : size.Width, - hasExplicitHeight ? explicitHeight : size.Height); + var finalWidth = ResolveConstraints(size.Width, VirtualView.Width, VirtualView.MinimumWidth, VirtualView.MaximumWidth); + var finalHeight = ResolveConstraints(size.Height, VirtualView.Height, VirtualView.MinimumHeight, VirtualView.MaximumHeight); + + return new Size(finalWidth, finalHeight); + } + + double ResolveConstraints(double measured, double exact, double min, double max) + { + var resolved = measured; + + if (IsExplicitSet(exact)) + { + // If an exact value has been specified, try to use that + resolved = exact; + } + + if (resolved > max) + { + // Apply the max value constraint (if any) + // If the exact value is in conflict with the max value, the max value should win + resolved = max; + } + + if (resolved < min) + { + // Apply the min value constraint (if any) + // If the exact or max value is in conflict with the min value, the min value should win + resolved = min; + } + + return resolved; } protected override void SetupContainer() diff --git a/src/Core/src/Layouts/AbsoluteLayoutManager.cs b/src/Core/src/Layouts/AbsoluteLayoutManager.cs index 2927b2cb4802..d8e484c3fd43 100644 --- a/src/Core/src/Layouts/AbsoluteLayoutManager.cs +++ b/src/Core/src/Layouts/AbsoluteLayoutManager.cs @@ -48,8 +48,8 @@ public override Size Measure(double widthConstraint, double heightConstraint) measuredWidth = Math.Max(measuredWidth, bounds.Left + width); } - var finalHeight = ResolveConstraints(heightConstraint, AbsoluteLayout.Height, measuredHeight); - var finalWidth = ResolveConstraints(widthConstraint, AbsoluteLayout.Width, measuredWidth); + var finalHeight = ResolveConstraints(heightConstraint, AbsoluteLayout.Height, measuredHeight, AbsoluteLayout.MinimumHeight, AbsoluteLayout.MaximumHeight); + var finalWidth = ResolveConstraints(widthConstraint, AbsoluteLayout.Width, measuredWidth, AbsoluteLayout.MinimumWidth, AbsoluteLayout.MaximumWidth); return new Size(finalWidth, finalHeight); } diff --git a/src/Core/src/Layouts/FlexLayoutManager.cs b/src/Core/src/Layouts/FlexLayoutManager.cs index 5ae3242f3aa1..a4b4997bd125 100644 --- a/src/Core/src/Layouts/FlexLayoutManager.cs +++ b/src/Core/src/Layouts/FlexLayoutManager.cs @@ -47,6 +47,9 @@ public Size Measure(double widthConstraint, double heightConstraint) height = heightConstraint; } + height = LayoutManager.ResolveConstraints(height, FlexLayout.Height, height, FlexLayout.MinimumHeight, FlexLayout.MaximumHeight); + width = LayoutManager.ResolveConstraints(width, FlexLayout.Width, width, FlexLayout.MinimumWidth, FlexLayout.MaximumWidth); + return new Size(width, height); } } diff --git a/src/Core/src/Layouts/GridLayoutManager.cs b/src/Core/src/Layouts/GridLayoutManager.cs index 046ee592c866..20986cec09f7 100644 --- a/src/Core/src/Layouts/GridLayoutManager.cs +++ b/src/Core/src/Layouts/GridLayoutManager.cs @@ -19,7 +19,13 @@ public GridLayoutManager(IGridLayout layout) : base(layout) public override Size Measure(double widthConstraint, double heightConstraint) { _gridStructure = new GridStructure(Grid, widthConstraint, heightConstraint); - return new Size(_gridStructure.MeasuredGridWidth(), _gridStructure.MeasuredGridHeight()); + + var measuredWidth = _gridStructure.MeasuredGridWidth(); + var measuredHeight = _gridStructure.MeasuredGridHeight(); + + // TODO ezhart We need tests on all the layout managers to make sure they respect min/max height/width in measurement + + return new Size(measuredWidth, measuredHeight); } public override Size ArrangeChildren(Rectangle bounds) @@ -48,6 +54,10 @@ class GridStructure readonly double _gridHeightConstraint; readonly double _explicitGridHeight; readonly double _explicitGridWidth; + readonly double _gridMaxHeight; + readonly double _gridMinHeight; + readonly double _gridMaxWidth; + readonly double _gridMinWidth; Row[] _rows { get; } Column[] _columns { get; } @@ -71,6 +81,10 @@ public GridStructure(IGridLayout grid, double widthConstraint, double heightCons _explicitGridHeight = _grid.Height; _explicitGridWidth = _grid.Width; + _gridMaxHeight = _grid.MaximumHeight; + _gridMinHeight = _grid.MinimumHeight; + _gridMaxWidth = _grid.MaximumWidth; + _gridMinWidth = _grid.MinimumWidth; // Cache these GridLayout properties so we don't have to keep looking them up via _grid // (Property access via _grid may have performance implications for some SDKs.) @@ -232,12 +246,36 @@ public double GridWidth() public double MeasuredGridHeight() { - return _explicitGridHeight > -1 ? _explicitGridHeight : GridHeight(); + var height = _explicitGridHeight > -1 ? _explicitGridHeight : GridHeight(); + + if (_gridMaxHeight >= 0 && height > _gridMaxHeight) + { + height = _gridMaxHeight; + } + + if (_gridMinHeight >= 0 && height < _gridMinHeight) + { + height = _gridMinHeight; + } + + return height; } public double MeasuredGridWidth() { - return _explicitGridWidth > -1 ? _explicitGridWidth : GridWidth(); + var width = _explicitGridWidth > -1 ? _explicitGridWidth : GridWidth(); + + if (_gridMaxWidth >= 0 && width > _gridMaxWidth) + { + width = _gridMaxWidth; + } + + if (_gridMinWidth >= 0 && width < _gridMinWidth) + { + width = _gridMinWidth; + } + + return width; } double SumDefinitions(Definition[] definitions, double spacing) diff --git a/src/Core/src/Layouts/HorizontalStackLayoutManager.cs b/src/Core/src/Layouts/HorizontalStackLayoutManager.cs index 8a1761de3212..0a4917e94ec0 100644 --- a/src/Core/src/Layouts/HorizontalStackLayoutManager.cs +++ b/src/Core/src/Layouts/HorizontalStackLayoutManager.cs @@ -35,8 +35,8 @@ public override Size Measure(double widthConstraint, double heightConstraint) measuredWidth += padding.HorizontalThickness; measuredHeight += padding.VerticalThickness; - var finalWidth = ResolveConstraints(widthConstraint, Stack.Width, measuredWidth); - var finalHeight = ResolveConstraints(heightConstraint, Stack.Height, measuredHeight); + var finalHeight = ResolveConstraints(heightConstraint, Stack.Height, measuredHeight, Stack.MinimumHeight, Stack.MaximumHeight); + var finalWidth = ResolveConstraints(widthConstraint, Stack.Width, measuredWidth, Stack.MinimumWidth, Stack.MaximumWidth); return new Size(finalWidth, finalHeight); } diff --git a/src/Core/src/Layouts/LayoutExtensions.cs b/src/Core/src/Layouts/LayoutExtensions.cs index f64ddf7346d9..ba966194764c 100644 --- a/src/Core/src/Layouts/LayoutExtensions.cs +++ b/src/Core/src/Layouts/LayoutExtensions.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Maui.Graphics; using Microsoft.Maui.Primitives; +using static Microsoft.Maui.Primitives.Dimension; namespace Microsoft.Maui.Layouts { @@ -36,7 +37,7 @@ public static Rectangle ComputeFrame(this IView view, Rectangle bounds) // We need to determine the width the element wants to consume; normally that's the element's DesiredSize.Width var consumedWidth = view.DesiredSize.Width; - if (view.HorizontalLayoutAlignment == LayoutAlignment.Fill && view.Width == -1) + if (view.HorizontalLayoutAlignment == LayoutAlignment.Fill && !IsExplicitSet(view.Width)) { // But if the element is set to fill horizontally and it doesn't have an explicitly set width, // then we want the width of the entire bounds @@ -51,7 +52,7 @@ public static Rectangle ComputeFrame(this IView view, Rectangle bounds) // But, if the element is set to fill vertically and it doesn't have an explicitly set height, // then we want the height of the entire bounds - if (view.VerticalLayoutAlignment == LayoutAlignment.Fill && view.Height == -1) + if (view.VerticalLayoutAlignment == LayoutAlignment.Fill && !IsExplicitSet(view.Height)) { consumedHeight = bounds.Height; } diff --git a/src/Core/src/Layouts/LayoutManager.cs b/src/Core/src/Layouts/LayoutManager.cs index 5411f0dce005..49430beda007 100644 --- a/src/Core/src/Layouts/LayoutManager.cs +++ b/src/Core/src/Layouts/LayoutManager.cs @@ -1,5 +1,6 @@ using System; using Microsoft.Maui.Graphics; +using static Microsoft.Maui.Primitives.Dimension; namespace Microsoft.Maui.Layouts { @@ -15,16 +16,21 @@ public LayoutManager(ILayout layout) public abstract Size Measure(double widthConstraint, double heightConstraint); public abstract Size ArrangeChildren(Rectangle bounds); - public static double ResolveConstraints(double externalConstraint, double explicitLength, double measuredLength) + public static double ResolveConstraints(double externalConstraint, double explicitLength, double measuredLength, double min = Minimum, double max = Maximum) { - if (explicitLength == -1) + var length = IsExplicitSet(explicitLength) ? explicitLength : measuredLength; + + if (max < length) + { + length = max; + } + + if (min > length) { - // No user-specified length, so the measured value will be limited by the external constraint - return Math.Min(measuredLength, externalConstraint); + length = min; } - // User-specified length wins, subject to external constraints - return Math.Min(explicitLength, externalConstraint); + return Math.Min(length, externalConstraint); } } } diff --git a/src/Core/src/Layouts/VerticalStackLayoutManager.cs b/src/Core/src/Layouts/VerticalStackLayoutManager.cs index badcfec398ba..ee1891ec7b59 100644 --- a/src/Core/src/Layouts/VerticalStackLayoutManager.cs +++ b/src/Core/src/Layouts/VerticalStackLayoutManager.cs @@ -34,8 +34,8 @@ public override Size Measure(double widthConstraint, double heightConstraint) measuredHeight += padding.VerticalThickness; measuredWidth += padding.HorizontalThickness; - var finalHeight = ResolveConstraints(heightConstraint, Stack.Height, measuredHeight); - var finalWidth = ResolveConstraints(widthConstraint, Stack.Width, measuredWidth); + var finalHeight = ResolveConstraints(heightConstraint, Stack.Height, measuredHeight, Stack.MinimumHeight, Stack.MaximumHeight); + var finalWidth = ResolveConstraints(widthConstraint, Stack.Width, measuredWidth, Stack.MinimumWidth, Stack.MaximumWidth); return new Size(finalWidth, finalHeight); } diff --git a/src/Core/src/Platform/Android/ViewExtensions.cs b/src/Core/src/Platform/Android/ViewExtensions.cs index 374c7cc9748c..e275e54b88e3 100644 --- a/src/Core/src/Platform/Android/ViewExtensions.cs +++ b/src/Core/src/Platform/Android/ViewExtensions.cs @@ -144,6 +144,46 @@ public static void UpdateHeight(this AView nativeView, IView view) } } + public static void UpdateMinimumHeight(this AView nativeView, IView view) + { + var value = (int)nativeView.Context!.ToPixels(view.MinimumHeight); + nativeView.SetMinimumHeight(value); + + if (!nativeView.IsInLayout) + { + nativeView.RequestLayout(); + } + } + + public static void UpdateMinimumWidth(this AView nativeView, IView view) + { + var value = (int)nativeView.Context!.ToPixels(view.MinimumWidth); + nativeView.SetMinimumWidth(value); + + if (!nativeView.IsInLayout) + { + nativeView.RequestLayout(); + } + } + + public static void UpdateMaximumHeight(this AView nativeView, IView view) + { + // GetDesiredSize will take the specified Height into account during the layout + if (!nativeView.IsInLayout) + { + nativeView.RequestLayout(); + } + } + + public static void UpdateMaximumWidth(this AView nativeView, IView view) + { + // GetDesiredSize will take the specified Height into account during the layout + if (!nativeView.IsInLayout) + { + nativeView.RequestLayout(); + } + } + public static void RemoveFromParent(this AView view) { if (view == null) @@ -152,6 +192,5 @@ public static void RemoveFromParent(this AView view) return; ((ViewGroup)view.Parent).RemoveView(view); } - } } diff --git a/src/Core/src/Platform/Standard/ViewExtensions.cs b/src/Core/src/Platform/Standard/ViewExtensions.cs index 5edd3697d528..9b6300d20d54 100644 --- a/src/Core/src/Platform/Standard/ViewExtensions.cs +++ b/src/Core/src/Platform/Standard/ViewExtensions.cs @@ -39,5 +39,13 @@ public static void InvalidateMeasure(this object nativeView, IView view) { } public static void UpdateWidth(this object nativeView, IView view) { } public static void UpdateHeight(this object nativeView, IView view) { } + + public static void UpdateMinimumHeight(this object nativeView, IView view) { } + + public static void UpdateMaximumHeight(this object nativeView, IView view) { } + + public static void UpdateMinimumWidth(this object nativeView, IView view) { } + + public static void UpdateMaximumWidth(this object nativeView, IView view) { } } } diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index 87778e4843df..90b923ada0ec 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -150,14 +150,36 @@ public static void InvalidateMeasure(this FrameworkElement nativeView, IView vie public static void UpdateWidth(this FrameworkElement nativeView, IView view) { - // WinUI uses NaN for "unspecified" - nativeView.Width = view.Width >= 0 ? view.Width : double.NaN; + // WinUI uses NaN for "unspecified", so as long as we're using NaN for unspecified on the xplat side, + // we can just propagate the value straight through + nativeView.Width = view.Width; } public static void UpdateHeight(this FrameworkElement nativeView, IView view) { - // WinUI uses NaN for "unspecified" - nativeView.Height = view.Height >= 0 ? view.Height : double.NaN; + // WinUI uses NaN for "unspecified", so as long as we're using NaN for unspecified on the xplat side, + // we can just propagate the value straight through + nativeView.Height = view.Height; + } + + public static void UpdateMinimumHeight(this FrameworkElement nativeView, IView view) + { + nativeView.MinHeight = view.MinimumHeight; + } + + public static void UpdateMinimumWidth(this FrameworkElement nativeView, IView view) + { + nativeView.MinWidth = view.MinimumWidth; + } + + public static void UpdateMaximumHeight(this FrameworkElement nativeView, IView view) + { + nativeView.MaxHeight = view.MaximumHeight; + } + + public static void UpdateMaximumWidth(this FrameworkElement nativeView, IView view) + { + nativeView.MaxWidth = view.MaximumWidth; } } } diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs index e99d8edacb79..ce89c3d1433f 100644 --- a/src/Core/src/Platform/iOS/ViewExtensions.cs +++ b/src/Core/src/Platform/iOS/ViewExtensions.cs @@ -3,6 +3,7 @@ using CoreAnimation; using Microsoft.Maui.Graphics; using UIKit; +using static Microsoft.Maui.Primitives.Dimension; namespace Microsoft.Maui { @@ -165,30 +166,44 @@ public static void InvalidateMeasure(this UIView nativeView, IView view) public static void UpdateWidth(this UIView nativeView, IView view) { - if (view.Width == -1) - { - // Ignore the initial set of the height; the initial layout will take care of it - return; - } - UpdateFrame(nativeView, view); } public static void UpdateHeight(this UIView nativeView, IView view) { - if (view.Height == -1) - { - // Ignore the initial set of the height; the initial layout will take care of it - return; - } + UpdateFrame(nativeView, view); + } + + public static void UpdateMinimumHeight(this UIView nativeView, IView view) + { + UpdateFrame(nativeView, view); + } + public static void UpdateMaximumHeight(this UIView nativeView, IView view) + { + UpdateFrame(nativeView, view); + } + + public static void UpdateMinimumWidth(this UIView nativeView, IView view) + { + UpdateFrame(nativeView, view); + } + + public static void UpdateMaximumWidth(this UIView nativeView, IView view) + { UpdateFrame(nativeView, view); } public static void UpdateFrame(UIView nativeView, IView view) { + if (!IsExplicitSet(view.Width) || !IsExplicitSet(view.Height)) + { + // Ignore the initial setting of the value; the initial layout will take care of it + return; + } + // Updating the frame (assuming it's an actual change) will kick off a layout update - // Handling of the default (-1) width/height will be taken care of by GetDesiredSize + // Handling of the default width/height will be taken care of by GetDesiredSize var currentFrame = nativeView.Frame; nativeView.Frame = new CoreGraphics.CGRect(currentFrame.X, currentFrame.Y, view.Width, view.Height); } diff --git a/src/Core/src/Primitives/Dimension.cs b/src/Core/src/Primitives/Dimension.cs new file mode 100644 index 000000000000..5572eafe6e16 --- /dev/null +++ b/src/Core/src/Primitives/Dimension.cs @@ -0,0 +1,19 @@ +namespace Microsoft.Maui.Primitives +{ + public static class Dimension + { + public const double Minimum = 0; + public const double Unset = double.NaN; + public const double Maximum = double.PositiveInfinity; + + public static bool IsExplicitSet(double value) + { + return !double.IsNaN(value); + } + + public static bool IsMaximumSet(double value) + { + return !double.IsPositiveInfinity(value); + } + } +} diff --git a/src/Core/tests/Benchmarks/Stubs/StubBase.cs b/src/Core/tests/Benchmarks/Stubs/StubBase.cs index 9a1bbf81541d..d0ea5532ea52 100644 --- a/src/Core/tests/Benchmarks/Stubs/StubBase.cs +++ b/src/Core/tests/Benchmarks/Stubs/StubBase.cs @@ -62,6 +62,14 @@ IElementHandler IElement.Handler public double Height { get; set; } + public double MinimumWidth { get; set; } + + public double MinimumHeight { get; set; } + + public double MaximumWidth { get; set; } + + public double MaximumHeight { get; set; } + public Thickness Margin { get; set; } public string AutomationId { get; set; } diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs index 931160a56035..b2695bf24006 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs @@ -118,6 +118,38 @@ public async Task RotationYInitializeCorrectly(double rotationY) Assert.Equal(view.RotationY, rY); } + [Theory] + [InlineData(0)] + [InlineData(100)] + public async Task MinimumHeightInitializes(double minHeight) + { + var view = new TStub() + { + MinimumHeight = minHeight + }; + + var expected = view.MinimumHeight; + var result = await GetValueAsync(view, handler => GetMinHeight(handler)); + + Assert.Equal(expected, result, 0); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + public async Task MinimumWidthInitializes(double minWidth) + { + var view = new TStub() + { + MinimumWidth = minWidth + }; + + var expected = view.MinimumWidth; + var result = await GetValueAsync(view, handler => GetMinWidth(handler)); + + Assert.Equal(expected, result, 0); + } + protected string GetAutomationId(IViewHandler viewHandler) => $"{((View)viewHandler.NativeView).GetTag(ViewExtensions.AutomationTagId)}"; @@ -198,6 +230,28 @@ double GetRotationY(IViewHandler viewHandler) return Math.Floor(nativeView.RotationY); } + double GetMinHeight(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + var nativeHeight = nativeView.MinimumHeight; + + var xplatHeight = nativeView.Context.FromPixels(nativeHeight); + + return xplatHeight; + } + + double GetMinWidth(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + var nativeWidth = nativeView.MinimumWidth; + + var xplatWidth = nativeView.Context.FromPixels(nativeWidth); + + return xplatWidth; + } + protected Visibility GetVisibility(IViewHandler viewHandler) { var nativeView = (View)viewHandler.NativeView; diff --git a/src/Core/tests/DeviceTests/Stubs/StubBase.cs b/src/Core/tests/DeviceTests/Stubs/StubBase.cs index 20a45dfd244f..cc88e8e93f9a 100644 --- a/src/Core/tests/DeviceTests/Stubs/StubBase.cs +++ b/src/Core/tests/DeviceTests/Stubs/StubBase.cs @@ -38,6 +38,14 @@ IElementHandler IElement.Handler public double Height { get; set; } = 50; + public double MaximumWidth { get; set; } = Primitives.Dimension.Maximum; + + public double MaximumHeight { get; set; } = Primitives.Dimension.Maximum; + + public double MinimumWidth { get; set; } = Primitives.Dimension.Minimum; + + public double MinimumHeight { get; set; } = Primitives.Dimension.Minimum; + public double TranslationX { get; set; } public double TranslationY { get; set; } diff --git a/src/Core/tests/UnitTests/Layouts/AbsoluteLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/AbsoluteLayoutManagerTests.cs index 32a82d5f99ab..16cc30d6c240 100644 --- a/src/Core/tests/UnitTests/Layouts/AbsoluteLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/AbsoluteLayoutManagerTests.cs @@ -4,6 +4,7 @@ using Microsoft.Maui.Controls; using Microsoft.Maui.Graphics; using Microsoft.Maui.Layouts; +using Microsoft.Maui.Primitives; using NSubstitute; using Xunit; using static Microsoft.Maui.UnitTests.Layouts.LayoutTestHelpers; @@ -16,8 +17,12 @@ public class AbsoluteLayoutManagerTests IAbsoluteLayout CreateTestLayout() { var layout = Substitute.For(); - layout.Height.Returns(-1); - layout.Width.Returns(-1); + layout.Height.Returns(Dimension.Unset); + layout.Width.Returns(Dimension.Unset); + layout.MinimumHeight.Returns(Dimension.Minimum); + layout.MinimumWidth.Returns(Dimension.Minimum); + layout.MaximumHeight.Returns(Dimension.Maximum); + layout.MaximumWidth.Returns(Dimension.Maximum); return layout; } @@ -217,5 +222,163 @@ public void RelativePositionRespectsPadding(double left, double top, var expectedRectangle = new Rectangle(expectedX, expectedY, width, height); child.Received().Arrange(Arg.Is(expectedRectangle)); } + + [Theory] + [InlineData(50, 100, 50)] + [InlineData(100, 100, 100)] + [InlineData(100, 50, 50)] + [InlineData(0, 50, 0)] + public void MeasureRespectsMaxHeight(double maxHeight, double viewHeight, double expectedHeight) + { + var abs = CreateTestLayout(); + var child = CreateTestView(); + SubstituteChildren(abs, child); + var childBounds = new Rectangle(0, 0, 100, viewHeight); + SetLayoutBounds(abs, child, childBounds); + + abs.MaximumHeight.Returns(maxHeight); + + var layoutManager = new AbsoluteLayoutManager(abs); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedHeight, measure.Height); + } + + [Theory] + [InlineData(50, 100, 50)] + [InlineData(100, 100, 100)] + [InlineData(100, 50, 50)] + [InlineData(0, 50, 0)] + public void MeasureRespectsMaxWidth(double maxWidth, double viewWidth, double expectedWidth) + { + var abs = CreateTestLayout(); + var child = CreateTestView(); + SubstituteChildren(abs, child); + var childBounds = new Rectangle(0, 0, viewWidth, 100); + SetLayoutBounds(abs, child, childBounds); + + abs.MaximumWidth.Returns(maxWidth); + + var gridLayoutManager = new AbsoluteLayoutManager(abs); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedWidth, measure.Width); + } + + [Theory] + [InlineData(50, 10, 50)] + [InlineData(100, 100, 100)] + [InlineData(10, 50, 50)] + public void MeasureRespectsMinHeight(double minHeight, double viewHeight, double expectedHeight) + { + var abs = CreateTestLayout(); + var child = CreateTestView(); + SubstituteChildren(abs, child); + var childBounds = new Rectangle(0, 0, 100, viewHeight); + SetLayoutBounds(abs, child, childBounds); + + abs.MinimumHeight.Returns(minHeight); + + var gridLayoutManager = new AbsoluteLayoutManager(abs); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedHeight, measure.Height); + } + + [Theory] + [InlineData(50, 10, 50)] + [InlineData(100, 100, 100)] + [InlineData(10, 50, 50)] + public void MeasureRespectsMinWidth(double minWidth, double viewWidth, double expectedWidth) + { + var abs = CreateTestLayout(); + var child = CreateTestView(); + SubstituteChildren(abs, child); + var childBounds = new Rectangle(0, 0, viewWidth, 100); + SetLayoutBounds(abs, child, childBounds); + + abs.MinimumWidth.Returns(minWidth); + + var gridLayoutManager = new AbsoluteLayoutManager(abs); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedWidth, measure.Width); + } + + [Fact] + public void MaxWidthDominatesWidth() + { + var abs = CreateTestLayout(); + var child = CreateTestView(); + SubstituteChildren(abs, child); + var childBounds = new Rectangle(0, 0, 100, 100); + SetLayoutBounds(abs, child, childBounds); + + abs.Width.Returns(75); + abs.MaximumWidth.Returns(50); + + var gridLayoutManager = new AbsoluteLayoutManager(abs); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The maximum value beats out the explicit value + Assert.Equal(50, measure.Width); + } + + [Fact] + public void MinWidthDominatesMaxWidth() + { + var abs = CreateTestLayout(); + var child = CreateTestView(); + SubstituteChildren(abs, child); + var childBounds = new Rectangle(0, 0, 100, 100); + SetLayoutBounds(abs, child, childBounds); + + abs.MinimumWidth.Returns(75); + abs.MaximumWidth.Returns(50); + + var gridLayoutManager = new AbsoluteLayoutManager(abs); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The minimum value should beat out the maximum value + Assert.Equal(75, measure.Width); + } + + [Fact] + public void MaxHeightDominatesHeight() + { + var abs = CreateTestLayout(); + var child = CreateTestView(); + SubstituteChildren(abs, child); + var childBounds = new Rectangle(0, 0, 100, 100); + SetLayoutBounds(abs, child, childBounds); + + abs.Height.Returns(75); + abs.MaximumHeight.Returns(50); + + var gridLayoutManager = new AbsoluteLayoutManager(abs); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The maximum value beats out the explicit value + Assert.Equal(50, measure.Height); + } + + [Fact] + public void MinHeightDominatesMaxHeight() + { + var abs = CreateTestLayout(); + var child = CreateTestView(); + SubstituteChildren(abs, child); + var childBounds = new Rectangle(0, 0, 100, 100); + SetLayoutBounds(abs, child, childBounds); + + abs.MinimumHeight.Returns(75); + abs.MaximumHeight.Returns(50); + + var gridLayoutManager = new AbsoluteLayoutManager(abs); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The minimum value should beat out the maximum value + Assert.Equal(75, measure.Height); + } } } diff --git a/src/Core/tests/UnitTests/Layouts/ConstraintTests.cs b/src/Core/tests/UnitTests/Layouts/ConstraintTests.cs index bdb9c9a3b131..b5076b627074 100644 --- a/src/Core/tests/UnitTests/Layouts/ConstraintTests.cs +++ b/src/Core/tests/UnitTests/Layouts/ConstraintTests.cs @@ -1,6 +1,8 @@ using Microsoft.Maui.Layouts; +using Microsoft.Maui.Primitives; using Xunit; + namespace Microsoft.Maui.UnitTests.Layouts { [Category(TestCategory.Core, TestCategory.Layout)] @@ -8,7 +10,7 @@ public class ConstraintTests { [Theory("When resolving constraints, external constraints take precedence")] [InlineData(100, 200, 130, 100)] - [InlineData(100, -1, 130, 100)] + [InlineData(100, Dimension.Unset, 130, 100)] public void ExternalWinsOverDesiredAndMeasured(double externalConstraint, double explicitLength, double measured, double expected) { var resolution = LayoutManager.ResolveConstraints(externalConstraint, explicitLength, measured); @@ -18,7 +20,7 @@ public void ExternalWinsOverDesiredAndMeasured(double externalConstraint, double [Fact("If external and request constraints don't apply, constrain to measured value")] public void MeasuredWinsIfNothingElseApplies() { - var resolution = LayoutManager.ResolveConstraints(double.PositiveInfinity, -1, 245); + var resolution = LayoutManager.ResolveConstraints(double.PositiveInfinity, Dimension.Unset, 245); Assert.Equal(245, resolution); } diff --git a/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs index 7e952de25a98..3bcb0e0dcd8d 100644 --- a/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs @@ -5,6 +5,7 @@ using Microsoft.Maui.Controls; using Microsoft.Maui.Graphics; using Microsoft.Maui.Layouts; +using Microsoft.Maui.Primitives; using NSubstitute; using Xunit; using static Microsoft.Maui.UnitTests.Layouts.LayoutTestHelpers; @@ -37,8 +38,13 @@ IGridLayout CreateGridLayout(int rowSpacing = 0, int colSpacing = 0, } var grid = Substitute.For(); - grid.Width.Returns(-1); - grid.Height.Returns(-1); + + grid.Height.Returns(Dimension.Unset); + grid.Width.Returns(Dimension.Unset); + grid.MinimumHeight.Returns(Dimension.Minimum); + grid.MinimumWidth.Returns(Dimension.Minimum); + grid.MaximumHeight.Returns(Dimension.Maximum); + grid.MaximumWidth.Returns(Dimension.Maximum); grid.RowSpacing.Returns(rowSpacing); grid.ColumnSpacing.Returns(colSpacing); @@ -1357,5 +1363,167 @@ public void ArrangeRespectsBounds() view.Received().Arrange(Arg.Is(expectedRectangle)); } + + [Category(GridAbsoluteSizing)] + [Theory] + [InlineData(50, 100, 50)] + [InlineData(100, 100, 100)] + [InlineData(100, 50, 50)] + [InlineData(0, 50, 0)] + [InlineData(-1, 50, 50)] + public void MeasureRespectsMaxHeight(double maxHeight, double viewHeight, double expectedHeight) + { + var grid = CreateGridLayout(); + var view = CreateTestView(new Size(100, viewHeight)); + SubstituteChildren(grid, view); + SetLocation(grid, view); + + grid.MaximumHeight.Returns(maxHeight); + + var layoutManager = new GridLayoutManager(grid); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedHeight, measure.Height); + } + + [Category(GridAbsoluteSizing)] + [Theory] + [InlineData(50, 100, 50)] + [InlineData(100, 100, 100)] + [InlineData(100, 50, 50)] + [InlineData(0, 50, 0)] + [InlineData(-1, 50, 50)] + public void MeasureRespectsMaxWidth(double maxWidth, double viewWidth, double expectedWidth) + { + var grid = CreateGridLayout(); + var view = CreateTestView(new Size(viewWidth, 100)); + SubstituteChildren(grid, view); + SetLocation(grid, view); + + grid.MaximumWidth.Returns(maxWidth); + + var layoutManager = new GridLayoutManager(grid); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedWidth, measure.Width); + } + + [Category(GridAbsoluteSizing)] + [Theory] + [InlineData(50, 10, 50)] + [InlineData(100, 100, 100)] + [InlineData(10, 50, 50)] + [InlineData(-1, 50, 50)] + public void MeasureRespectsMinHeight(double minHeight, double viewHeight, double expectedHeight) + { + var grid = CreateGridLayout(); + var view = CreateTestView(new Size(100, viewHeight)); + SubstituteChildren(grid, view); + SetLocation(grid, view); + + grid.MinimumHeight.Returns(minHeight); + + var layoutManager = new GridLayoutManager(grid); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedHeight, measure.Height); + } + + [Category(GridAbsoluteSizing)] + [Theory] + [InlineData(50, 10, 50)] + [InlineData(100, 100, 100)] + [InlineData(10, 50, 50)] + [InlineData(-1, 50, 50)] + public void MeasureRespectsMinWidth(double minWidth, double viewWidth, double expectedWidth) + { + var grid = CreateGridLayout(); + var view = CreateTestView(new Size(viewWidth, 100)); + SubstituteChildren(grid, view); + SetLocation(grid, view); + + grid.MinimumWidth.Returns(minWidth); + + var layoutManager = new GridLayoutManager(grid); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedWidth, measure.Width); + } + + [Fact] + [Category(GridAbsoluteSizing)] + public void MaxWidthDominatesWidth() + { + var grid = CreateGridLayout(); + var view = CreateTestView(new Size(100, 100)); + SubstituteChildren(grid, view); + SetLocation(grid, view); + + grid.Width.Returns(75); + grid.MaximumWidth.Returns(50); + + var layoutManager = new GridLayoutManager(grid); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The maximum value beats out the explicit value + Assert.Equal(50, measure.Width); + } + + [Fact] + [Category(GridAbsoluteSizing)] + public void MinWidthDominatesMaxWidth() + { + var grid = CreateGridLayout(); + var view = CreateTestView(new Size(100, 100)); + SubstituteChildren(grid, view); + SetLocation(grid, view); + + grid.MinimumWidth.Returns(75); + grid.MaximumWidth.Returns(50); + + var layoutManager = new GridLayoutManager(grid); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The minimum value should beat out the maximum value + Assert.Equal(75, measure.Width); + } + + [Fact] + [Category(GridAbsoluteSizing)] + public void MaxHeightDominatesHeight() + { + var grid = CreateGridLayout(); + var view = CreateTestView(new Size(100, 100)); + SubstituteChildren(grid, view); + SetLocation(grid, view); + + grid.Height.Returns(75); + grid.MaximumHeight.Returns(50); + + var layoutManager = new GridLayoutManager(grid); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The maximum value beats out the explicit value + Assert.Equal(50, measure.Height); + } + + [Fact] + [Category(GridAbsoluteSizing)] + public void MinHeightDominatesMaxHeight() + { + var grid = CreateGridLayout(); + var view = CreateTestView(new Size(100, 100)); + SubstituteChildren(grid, view); + SetLocation(grid, view); + + grid.MinimumHeight.Returns(75); + grid.MaximumHeight.Returns(50); + + var layoutManager = new GridLayoutManager(grid); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The minimum value should beat out the maximum value + Assert.Equal(75, measure.Height); + } } } diff --git a/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs index f165aa87d241..1d94492a130e 100644 --- a/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs @@ -69,7 +69,7 @@ public void SpacingArrangementTwoItems(int spacing) [Theory] [InlineData(150, 100, 100)] [InlineData(150, 200, 200)] - [InlineData(1250, -1, 1250)] + [InlineData(1250, Dimension.Unset, 1250)] public void StackAppliesWidth(double viewWidth, double stackWidth, double expectedWidth) { var view = LayoutTestHelpers.CreateTestView(new Size(viewWidth, 100)); @@ -229,5 +229,130 @@ public void ArrangeRespectsBounds() stack[0].Received().Arrange(Arg.Is(expectedRectangle0)); } + + [Theory] + [InlineData(50, 100, 50)] + [InlineData(100, 100, 100)] + [InlineData(100, 50, 50)] + [InlineData(0, 50, 0)] + public void MeasureRespectsMaxHeight(double maxHeight, double viewHeight, double expectedHeight) + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: viewHeight); + stack.MaximumHeight.Returns(maxHeight); + + var layoutManager = new HorizontalStackLayoutManager(stack); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedHeight, measure.Height); + } + + [Theory] + [InlineData(50, 100, 50)] + [InlineData(100, 100, 100)] + [InlineData(100, 50, 50)] + [InlineData(0, 50, 0)] + public void MeasureRespectsMaxWidth(double maxWidth, double viewWidth, double expectedWidth) + { + var stack = BuildStack(viewCount: 1, viewWidth: viewWidth, viewHeight: 100); + + stack.MaximumWidth.Returns(maxWidth); + + var gridLayoutManager = new HorizontalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedWidth, measure.Width); + } + + [Theory] + [InlineData(50, 10, 50)] + [InlineData(100, 100, 100)] + [InlineData(10, 50, 50)] + public void MeasureRespectsMinHeight(double minHeight, double viewHeight, double expectedHeight) + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: viewHeight); + + stack.MinimumHeight.Returns(minHeight); + + var gridLayoutManager = new HorizontalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedHeight, measure.Height); + } + + [Theory] + [InlineData(50, 10, 50)] + [InlineData(100, 100, 100)] + [InlineData(10, 50, 50)] + public void MeasureRespectsMinWidth(double minWidth, double viewWidth, double expectedWidth) + { + var stack = BuildStack(viewCount: 1, viewWidth: viewWidth, viewHeight: 100); + + stack.MinimumWidth.Returns(minWidth); + + var gridLayoutManager = new HorizontalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedWidth, measure.Width); + } + + [Fact] + public void MaxWidthDominatesWidth() + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: 100); + + stack.Width.Returns(75); + stack.MaximumWidth.Returns(50); + + var gridLayoutManager = new HorizontalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The maximum value beats out the explicit value + Assert.Equal(50, measure.Width); + } + + [Fact] + public void MinWidthDominatesMaxWidth() + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: 100); + + stack.MinimumWidth.Returns(75); + stack.MaximumWidth.Returns(50); + + var gridLayoutManager = new HorizontalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The minimum value should beat out the maximum value + Assert.Equal(75, measure.Width); + } + + [Fact] + public void MaxHeightDominatesHeight() + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: 100); + + stack.Height.Returns(75); + stack.MaximumHeight.Returns(50); + + var gridLayoutManager = new HorizontalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The maximum value beats out the explicit value + Assert.Equal(50, measure.Height); + } + + [Fact] + public void MinHeightDominatesMaxHeight() + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: 100); + + stack.MinimumHeight.Returns(75); + stack.MaximumHeight.Returns(50); + + var gridLayoutManager = new HorizontalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The minimum value should beat out the maximum value + Assert.Equal(75, measure.Height); + } } } diff --git a/src/Core/tests/UnitTests/Layouts/LayoutExtensionTests.cs b/src/Core/tests/UnitTests/Layouts/LayoutExtensionTests.cs index 6629978da5eb..03a785006544 100644 --- a/src/Core/tests/UnitTests/Layouts/LayoutExtensionTests.cs +++ b/src/Core/tests/UnitTests/Layouts/LayoutExtensionTests.cs @@ -16,8 +16,8 @@ public void FrameExcludesMargin() var element = Substitute.For(); var margin = new Thickness(20); element.Margin.Returns(margin); - element.Width.Returns(-1); - element.Height.Returns(-1); + element.Width.Returns(Dimension.Unset); + element.Height.Returns(Dimension.Unset); var bounds = new Rectangle(0, 0, 100, 100); var frame = element.ComputeFrame(bounds); @@ -128,8 +128,8 @@ public void FrameAccountsForHorizontalLayoutAlignment(LayoutAlignment layoutAlig element.Margin.Returns(margin); element.DesiredSize.Returns(viewSizeIncludingMargins); element.HorizontalLayoutAlignment.Returns(layoutAlignment); - element.Width.Returns(-1); - element.Height.Returns(-1); + element.Width.Returns(Dimension.Unset); + element.Height.Returns(Dimension.Unset); element.FlowDirection.Returns(FlowDirection.LeftToRight); var frame = element.ComputeFrame(new Rectangle(offset.X, offset.Y, widthConstraint, heightConstraint)); @@ -152,8 +152,8 @@ public void FrameAccountsForVerticalLayoutAlignment(LayoutAlignment layoutAlignm element.Margin.Returns(margin); element.DesiredSize.Returns(viewSizeIncludingMargins); element.VerticalLayoutAlignment.Returns(layoutAlignment); - element.Width.Returns(-1); - element.Height.Returns(-1); + element.Width.Returns(Dimension.Unset); + element.Height.Returns(Dimension.Unset); element.FlowDirection.Returns(FlowDirection.LeftToRight); var frame = element.ComputeFrame(new Rectangle(offset.X, offset.Y, widthConstraint, heightConstraint)); @@ -211,8 +211,8 @@ public void FrameAccountsForHorizontalLayoutAlignmentRtl(LayoutAlignment layoutA element.DesiredSize.Returns(viewSizeIncludingMargins); element.FlowDirection.Returns(FlowDirection.RightToLeft); element.HorizontalLayoutAlignment.Returns(layoutAlignment); - element.Width.Returns(-1); - element.Height.Returns(-1); + element.Width.Returns(Dimension.Unset); + element.Height.Returns(Dimension.Unset); var frame = element.ComputeFrame(new Rectangle(offset.X, offset.Y, widthConstraint, heightConstraint)); diff --git a/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs index d3f284a6358f..7a606ca1a0ee 100644 --- a/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs @@ -2,6 +2,7 @@ using Microsoft.Maui; using Microsoft.Maui.Graphics; using NSubstitute; +using Microsoft.Maui.Primitives; using static Microsoft.Maui.UnitTests.Layouts.LayoutTestHelpers; namespace Microsoft.Maui.UnitTests.Layouts @@ -11,8 +12,12 @@ public abstract class StackLayoutManagerTests protected IStackLayout CreateTestLayout() { var stack = Substitute.For(); - stack.Height.Returns(-1); - stack.Width.Returns(-1); + stack.Height.Returns(Dimension.Unset); + stack.Width.Returns(Dimension.Unset); + stack.MinimumHeight.Returns(Dimension.Minimum); + stack.MinimumWidth.Returns(Dimension.Minimum); + stack.MaximumHeight.Returns(Dimension.Maximum); + stack.MaximumWidth.Returns(Dimension.Maximum); stack.Spacing.Returns(0); return stack; diff --git a/src/Core/tests/UnitTests/Layouts/VerticalStackLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/VerticalStackLayoutManagerTests.cs index 1dffd53204b0..ae535040d9b5 100644 --- a/src/Core/tests/UnitTests/Layouts/VerticalStackLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/VerticalStackLayoutManagerTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Microsoft.Maui.Graphics; using Microsoft.Maui.Layouts; +using Microsoft.Maui.Primitives; using NSubstitute; using Xunit; using static Microsoft.Maui.UnitTests.Layouts.LayoutTestHelpers; @@ -63,7 +64,7 @@ public void SpacingArrangementTwoItems(int spacing) [Theory] [InlineData(150, 100, 100)] [InlineData(150, 200, 200)] - [InlineData(1250, -1, 1250)] + [InlineData(1250, Dimension.Unset, 1250)] public void StackAppliesHeight(double viewHeight, double stackHeight, double expectedHeight) { var view = LayoutTestHelpers.CreateTestView(new Size(100, viewHeight)); @@ -186,5 +187,130 @@ public void ArrangeRespectsBounds() stack[0].Received().Arrange(Arg.Is(expectedRectangle0)); } + + [Theory] + [InlineData(50, 100, 50)] + [InlineData(100, 100, 100)] + [InlineData(100, 50, 50)] + [InlineData(0, 50, 0)] + public void MeasureRespectsMaxHeight(double maxHeight, double viewHeight, double expectedHeight) + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: viewHeight); + stack.MaximumHeight.Returns(maxHeight); + + var layoutManager = new VerticalStackLayoutManager(stack); + var measure = layoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedHeight, measure.Height); + } + + [Theory] + [InlineData(50, 100, 50)] + [InlineData(100, 100, 100)] + [InlineData(100, 50, 50)] + [InlineData(0, 50, 0)] + public void MeasureRespectsMaxWidth(double maxWidth, double viewWidth, double expectedWidth) + { + var stack = BuildStack(viewCount: 1, viewWidth: viewWidth, viewHeight: 100); + + stack.MaximumWidth.Returns(maxWidth); + + var gridLayoutManager = new VerticalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedWidth, measure.Width); + } + + [Theory] + [InlineData(50, 10, 50)] + [InlineData(100, 100, 100)] + [InlineData(10, 50, 50)] + public void MeasureRespectsMinHeight(double minHeight, double viewHeight, double expectedHeight) + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: viewHeight); + + stack.MinimumHeight.Returns(minHeight); + + var gridLayoutManager = new VerticalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedHeight, measure.Height); + } + + [Theory] + [InlineData(50, 10, 50)] + [InlineData(100, 100, 100)] + [InlineData(10, 50, 50)] + public void MeasureRespectsMinWidth(double minWidth, double viewWidth, double expectedWidth) + { + var stack = BuildStack(viewCount: 1, viewWidth: viewWidth, viewHeight: 100); + + stack.MinimumWidth.Returns(minWidth); + + var gridLayoutManager = new VerticalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + Assert.Equal(expectedWidth, measure.Width); + } + + [Fact] + public void MaxWidthDominatesWidth() + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: 100); + + stack.Width.Returns(75); + stack.MaximumWidth.Returns(50); + + var gridLayoutManager = new VerticalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The maximum value beats out the explicit value + Assert.Equal(50, measure.Width); + } + + [Fact] + public void MinWidthDominatesMaxWidth() + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: 100); + + stack.MinimumWidth.Returns(75); + stack.MaximumWidth.Returns(50); + + var gridLayoutManager = new VerticalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The minimum value should beat out the maximum value + Assert.Equal(75, measure.Width); + } + + [Fact] + public void MaxHeightDominatesHeight() + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: 100); + + stack.Height.Returns(75); + stack.MaximumHeight.Returns(50); + + var gridLayoutManager = new VerticalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The maximum value beats out the explicit value + Assert.Equal(50, measure.Height); + } + + [Fact] + public void MinHeightDominatesMaxHeight() + { + var stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: 100); + + stack.MinimumHeight.Returns(75); + stack.MaximumHeight.Returns(50); + + var gridLayoutManager = new VerticalStackLayoutManager(stack); + var measure = gridLayoutManager.Measure(double.PositiveInfinity, double.PositiveInfinity); + + // The minimum value should beat out the maximum value + Assert.Equal(75, measure.Height); + } } } diff --git a/src/Core/tests/UnitTests/TestClasses/ViewStub.cs b/src/Core/tests/UnitTests/TestClasses/ViewStub.cs index 711886abe509..3123926d4773 100644 --- a/src/Core/tests/UnitTests/TestClasses/ViewStub.cs +++ b/src/Core/tests/UnitTests/TestClasses/ViewStub.cs @@ -37,6 +37,14 @@ IElementHandler IElement.Handler public double Height { get; set; } + public double MinimumHeight { get; set; } + + public double MinimumWidth { get; set; } + + public double MaximumHeight { get; set; } + + public double MaximumWidth { get; set; } + public Thickness Margin { get; set; } public string AutomationId { get; set; }