From 9713172b94181f5816adb87086d30643385c5f82 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Wed, 18 Aug 2021 18:57:40 -0600 Subject: [PATCH 01/19] Min/max height/width for Android/iOS/Windows Fixes #709 --- .../VisualElement/VisualElement.Impl.cs | 4 + src/Controls/src/Core/VisualElement.cs | 28 ++- src/Core/src/Core/IView.cs | 26 ++- src/Core/src/Handlers/View/ViewHandler.cs | 24 +++ .../Handlers/View/ViewHandlerOfT.Android.cs | 11 +- .../src/Handlers/View/ViewHandlerOfT.iOS.cs | 38 +++- src/Core/src/Layouts/AbsoluteLayoutManager.cs | 4 +- src/Core/src/Layouts/FlexLayoutManager.cs | 3 + src/Core/src/Layouts/GridLayoutManager.cs | 44 ++++- .../Layouts/HorizontalStackLayoutManager.cs | 4 +- src/Core/src/Layouts/LayoutManager.cs | 17 +- .../src/Layouts/VerticalStackLayoutManager.cs | 4 +- .../src/Platform/Android/ViewExtensions.cs | 54 +++++- .../src/Platform/Standard/ViewExtensions.cs | 8 + .../src/Platform/Windows/ViewExtensions.cs | 20 +++ src/Core/src/Platform/iOS/ViewExtensions.cs | 48 ++++- .../Handlers/HandlerTestBase.Android.cs | 56 ++++++ src/Core/tests/DeviceTests/Stubs/StubBase.cs | 8 + .../Layouts/AbsoluteLayoutManagerTests.cs | 162 +++++++++++++++++ .../Layouts/GridLayoutManagerTests.cs | 166 ++++++++++++++++++ .../HorizontalStackLayoutManagerTests.cs | 129 ++++++++++++++ .../Layouts/StackLayoutManagerTests.cs | 4 + .../VerticalStackLayoutManagerTests.cs | 129 ++++++++++++++ .../tests/UnitTests/TestClasses/ViewStub.cs | 8 + 24 files changed, 964 insertions(+), 35 deletions(-) diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs index df1a039ef8f6..e78b339604b1 100644 --- a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs @@ -118,6 +118,10 @@ internal Semantics SetupSemantics() => double IView.Width => WidthRequest; double IView.Height => HeightRequest; + double IView.MinimumWidth => MinimumWidthRequest == -1 ? 0 : MinimumWidthRequest; + double IView.MinimumHeight => MinimumHeightRequest == -1 ? 0 : MinimumHeightRequest; + double IView.MaximumWidth => MaximumWidthRequest; + double IView.MaximumHeight => MaximumHeightRequest; Thickness IView.Margin => Thickness.Zero; } diff --git a/src/Controls/src/Core/VisualElement.cs b/src/Controls/src/Core/VisualElement.cs index 1919d1cf5a9a..c6dc96acbfac 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), -1d, propertyChanged: OnRequestChanged); + + public static readonly BindableProperty MaximumHeightRequestProperty = BindableProperty.Create(nameof(MaximumHeightRequest), typeof(double), typeof(VisualElement), -1d, propertyChanged: OnRequestChanged); [EditorBrowsable(EditorBrowsableState.Never)] public static readonly BindablePropertyKey IsFocusedPropertyKey = BindableProperty.CreateReadOnly("IsFocused", @@ -435,6 +439,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); } @@ -1037,6 +1053,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/Core/src/Core/IView.cs b/src/Core/src/Core/IView.cs index ed74327d9c7a..d3a0087791b7 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. + /// + double MinimumWidth { get; } + + /// + /// Gets the specified maximum width constraint of the IView. + /// + double MaximumWidth { get; } + + /// + /// Gets the specified height of the IView. /// double Height { get; } + /// + /// Gets the specified minimum height constraint of the IView. + /// + double MinimumHeight { get; } + + /// + /// Gets the specified maximum height constraint of the IView. + /// + 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 3f5c5fbaeb50..712a2f618af4 100644 --- a/src/Core/src/Handlers/View/ViewHandler.cs +++ b/src/Core/src/Handlers/View/ViewHandler.cs @@ -21,6 +21,10 @@ public abstract partial class ViewHandler : ElementHandler, IViewHandler [nameof(IView.Background)] = MapBackground, [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, @@ -137,6 +141,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..5fa916430a9e 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs @@ -55,8 +55,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,7 +64,7 @@ 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; @@ -74,6 +74,11 @@ int CreateMeasureSpec(double constraint, double explicitSize) mode = MeasureSpecMode.Exactly; constraint = explicitSize; } + else if (maximumSize >= 0) + { + 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..c1e1c7b8ee98 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs @@ -46,11 +46,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 +58,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 (exact >= 0) + { + // If an exact value has been specified, try to use that + resolved = exact; + } + + if (max >= 0 && 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 (min >= 0 && 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/LayoutManager.cs b/src/Core/src/Layouts/LayoutManager.cs index 5411f0dce005..e48491c40cf8 100644 --- a/src/Core/src/Layouts/LayoutManager.cs +++ b/src/Core/src/Layouts/LayoutManager.cs @@ -15,16 +15,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 = -1, double max = -1) { - if (explicitLength == -1) + var length = explicitLength >= 0 ? explicitLength : measuredLength; + + if (max >= 0 && max < length) + { + length = max; + } + + if (min >= 0 && 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 cff6e059ffe7..3819768bca4b 100644 --- a/src/Core/src/Platform/Android/ViewExtensions.cs +++ b/src/Core/src/Platform/Android/ViewExtensions.cs @@ -115,6 +115,59 @@ public static void UpdateHeight(this AView nativeView, IView view) } } + public static void UpdateMinimumHeight(this AView nativeView, IView view) + { + var xplatMinHeight = view.MinimumHeight; + + if (xplatMinHeight < 0) + { + xplatMinHeight = 0; + } + + var value = (int)nativeView.Context!.ToPixels(xplatMinHeight); + nativeView.SetMinimumHeight(value); + + if (!nativeView.IsInLayout) + { + nativeView.RequestLayout(); + } + } + + public static void UpdateMinimumWidth(this AView nativeView, IView view) + { + var xplatMinWidth = view.MinimumWidth; + if (xplatMinWidth < 0) + { + xplatMinWidth = 0; + } + + var value = (int)nativeView.Context!.ToPixels(xplatMinWidth); + 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) @@ -123,6 +176,5 @@ public static void RemoveFromParent(this AView view) return; ((ViewGroup)view.Parent).RemoveView(view); } - } } \ No newline at end of file diff --git a/src/Core/src/Platform/Standard/ViewExtensions.cs b/src/Core/src/Platform/Standard/ViewExtensions.cs index 2e629bf1bb2e..0d88d83d632c 100644 --- a/src/Core/src/Platform/Standard/ViewExtensions.cs +++ b/src/Core/src/Platform/Standard/ViewExtensions.cs @@ -37,5 +37,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) { } } } \ No newline at end of file diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index e8bdc1493d62..09c764939a1f 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -128,5 +128,25 @@ public static void UpdateHeight(this FrameworkElement nativeView, IView view) // WinUI uses NaN for "unspecified" nativeView.Height = view.Height >= 0 ? view.Height : double.NaN; } + + public static void UpdateMinimumHeight(this FrameworkElement nativeView, IView view) + { + nativeView.MinHeight = view.MinimumHeight >= 0 ? view.MinimumHeight : 0; + } + + public static void UpdateMinimumWidth(this FrameworkElement nativeView, IView view) + { + nativeView.MinWidth = view.MinimumWidth >= 0 ? view.MinimumWidth : 0; + } + + public static void UpdateMaximumHeight(this FrameworkElement nativeView, IView view) + { + nativeView.MaxHeight = view.MaximumHeight >= 0 ? view.MaximumHeight : double.PositiveInfinity; + } + + public static void UpdateMaximumWidth(this FrameworkElement nativeView, IView view) + { + nativeView.MaxWidth = view.MaximumWidth >= 0 ? view.MaximumWidth : double.PositiveInfinity; + } } } \ No newline at end of file diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs index ead7b7775340..0765971c014b 100644 --- a/src/Core/src/Platform/iOS/ViewExtensions.cs +++ b/src/Core/src/Platform/iOS/ViewExtensions.cs @@ -149,7 +149,7 @@ 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 + // Ignore the initial setting of the value; the initial layout will take care of it return; } @@ -160,7 +160,51 @@ 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 + // Ignore the initial setting of the value; the initial layout will take care of it + return; + } + + UpdateFrame(nativeView, view); + } + + public static void UpdateMinimumHeight(this UIView nativeView, IView view) + { + if (view.MinimumHeight == -1) + { + // Ignore the initial setting of the value; the initial layout will take care of it + return; + } + + UpdateFrame(nativeView, view); + } + + public static void UpdateMaximumHeight(this UIView nativeView, IView view) + { + if (view.MaximumHeight == -1) + { + // Ignore the initial setting of the value; the initial layout will take care of it + return; + } + + UpdateFrame(nativeView, view); + } + + public static void UpdateMinimumWidth(this UIView nativeView, IView view) + { + if (view.MaximumWidth == -1) + { + // Ignore the initial setting of the value; the initial layout will take care of it + return; + } + + UpdateFrame(nativeView, view); + } + + public static void UpdateMaximumWidth(this UIView nativeView, IView view) + { + if (view.MaximumWidth == -1) + { + // Ignore the initial setting of the value; the initial layout will take care of it return; } diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs index 454c86e1ab55..07556c61f6f9 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs @@ -118,6 +118,40 @@ public async Task RotationYInitializeCorrectly(double rotationY) Assert.Equal(view.RotationY, rY); } + [Theory] + [InlineData(0)] + [InlineData(100)] + [InlineData(-1)] + public async Task MinimumHeightInitializes(double minHeight) + { + var view = new TStub() + { + MinimumHeight = minHeight + }; + + var expected = view.MinimumHeight == -1 ? 0 : view.MinimumHeight; + var result = await GetValueAsync(view, handler => GetMinHeight(handler)); + + Assert.Equal(expected, result, 4); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + [InlineData(-1)] + public async Task MinimumWidthInitializes(double minWidth) + { + var view = new TStub() + { + MinimumWidth = minWidth + }; + + var expected = view.MinimumWidth == -1 ? 0 : view.MinimumWidth; + var result = await GetValueAsync(view, handler => GetMinWidth(handler)); + + Assert.Equal(expected, result, 4); + } + protected string GetAutomationId(IViewHandler viewHandler) => $"{((View)viewHandler.NativeView).GetTag(ViewExtensions.AutomationTagId)}"; @@ -188,6 +222,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 66c433484eea..14c88f52a657 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; } = -1; + + public double MaximumHeight { get; set; } = -1; + + public double MinimumWidth { get; set; } = -1; + + public double MinimumHeight { get; set; } = -1; + 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 1504341eed8b..d10feb963670 100644 --- a/src/Core/tests/UnitTests/Layouts/AbsoluteLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/AbsoluteLayoutManagerTests.cs @@ -217,5 +217,167 @@ 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)] + [InlineData(-1, 50, 50)] + 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)] + [InlineData(-1, 50, 50)] + 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)] + [InlineData(-1, 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)] + [InlineData(-1, 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/GridLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs index 7e952de25a98..44da96d1c54d 100644 --- a/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/GridLayoutManagerTests.cs @@ -39,6 +39,10 @@ IGridLayout CreateGridLayout(int rowSpacing = 0, int colSpacing = 0, var grid = Substitute.For(); grid.Width.Returns(-1); grid.Height.Returns(-1); + grid.MaximumWidth.Returns(-1); + grid.MinimumHeight.Returns(-1); + grid.MinimumWidth.Returns(-1); + grid.MaximumHeight.Returns(-1); grid.RowSpacing.Returns(rowSpacing); grid.ColumnSpacing.Returns(colSpacing); @@ -1357,5 +1361,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 2819b3dabae5..ecbcb5032bc3 100644 --- a/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs @@ -228,5 +228,134 @@ 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)] + [InlineData(-1, 50, 50)] + 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)] + [InlineData(-1, 50, 50)] + 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)] + [InlineData(-1, 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)] + [InlineData(-1, 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/StackLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs index d3f284a6358f..6a96a84df8c9 100644 --- a/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs @@ -13,6 +13,10 @@ protected IStackLayout CreateTestLayout() var stack = Substitute.For(); stack.Height.Returns(-1); stack.Width.Returns(-1); + stack.MinimumHeight.Returns(-1); + stack.MinimumWidth.Returns(-1); + stack.MaximumHeight.Returns(-1); + stack.MaximumWidth.Returns(-1); 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..b8b0b46bde24 100644 --- a/src/Core/tests/UnitTests/Layouts/VerticalStackLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/VerticalStackLayoutManagerTests.cs @@ -186,5 +186,134 @@ 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)] + [InlineData(-1, 50, 50)] + 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)] + [InlineData(-1, 50, 50)] + 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)] + [InlineData(-1, 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)] + [InlineData(-1, 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; } From e8b2f144457435b35dd95d5c9e1925283c9491af Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Wed, 25 Aug 2021 20:03:01 -0600 Subject: [PATCH 02/19] Set up constants for Core min/max/explicit default values --- .../VisualElement/VisualElement.Impl.cs | 58 ++++++++++++++++--- src/Controls/src/Core/VisualElement.cs | 4 +- src/Core/src/Core/IView.cs | 8 +-- .../Handlers/View/ViewHandlerOfT.Android.cs | 5 +- .../src/Handlers/View/ViewHandlerOfT.iOS.cs | 7 ++- src/Core/src/Layouts/LayoutManager.cs | 9 +-- .../src/Platform/Android/ViewExtensions.cs | 19 ++---- .../src/Platform/Windows/ViewExtensions.cs | 8 +-- src/Core/src/Platform/iOS/ViewExtensions.cs | 24 -------- src/Core/src/Primitives/Measure.cs | 19 ++++++ src/Core/tests/DeviceTests/Stubs/StubBase.cs | 8 +-- .../Layouts/AbsoluteLayoutManagerTests.cs | 13 +++-- .../UnitTests/Layouts/ConstraintTests.cs | 6 +- .../Layouts/GridLayoutManagerTests.cs | 14 +++-- .../HorizontalStackLayoutManagerTests.cs | 6 +- .../Layouts/StackLayoutManagerTests.cs | 13 +++-- .../VerticalStackLayoutManagerTests.cs | 7 +-- 17 files changed, 129 insertions(+), 99 deletions(-) create mode 100644 src/Core/src/Primitives/Measure.cs diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs index e78b339604b1..d74bcb1a95c6 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 @@ -116,12 +117,55 @@ Semantics IView.Semantics internal Semantics SetupSemantics() => _semantics ??= new Semantics(); - double IView.Width => WidthRequest; - double IView.Height => HeightRequest; - double IView.MinimumWidth => MinimumWidthRequest == -1 ? 0 : MinimumWidthRequest; - double IView.MinimumHeight => MinimumHeightRequest == -1 ? 0 : MinimumHeightRequest; - double IView.MaximumWidth => MaximumWidthRequest; - double IView.MaximumHeight => MaximumHeightRequest; + double IView.Width + { + get + { + if (!IsSet(WidthProperty)) + { + return Primitives.Dimension.Unset; + } + + // Access once up front to avoid multiple GetValue calls + var widthRequest = WidthRequest; + + if (widthRequest < 0) + { + throw new InvalidOperationException($"{nameof(IView.Width)} cannot be less than zero."); + } + + return widthRequest; + } + } + + double IView.Height + { + get + { + if (!IsSet(HeightProperty)) + { + return Primitives.Dimension.Unset; + } + + // Access once up front to avoid multiple GetValue calls + var heightRequest = HeightRequest; + + if (heightRequest < 0) + { + throw new InvalidOperationException($"{nameof(IView.Height)} cannot be less than zero."); + } + + return heightRequest; + } + } + + // Default value from VisualElement is -1 (legacy); translating to Core we'll use a reasonable value of zero + double IView.MinimumWidth => MinimumWidthRequest < 0 ? 0 : MinimumWidthRequest; + double IView.MinimumHeight => MinimumHeightRequest < 0 ? 0 : MinimumHeightRequest; + + // Default value from VisualElement is PositiveInfinity + double IView.MaximumWidth => MaximumWidthRequest < 0 ? 0 : MaximumWidthRequest; + double IView.MaximumHeight => MaximumHeightRequest < 0 ? 0 : MaximumHeightRequest; Thickness IView.Margin => Thickness.Zero; } diff --git a/src/Controls/src/Core/VisualElement.cs b/src/Controls/src/Core/VisualElement.cs index c6dc96acbfac..fe82f1b6d565 100644 --- a/src/Controls/src/Core/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement.cs @@ -280,9 +280,9 @@ void InvalidateGradientBrushRequested(object sender, EventArgs e) 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), -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), -1d, 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", diff --git a/src/Core/src/Core/IView.cs b/src/Core/src/Core/IView.cs index d3a0087791b7..c978d4a187f6 100644 --- a/src/Core/src/Core/IView.cs +++ b/src/Core/src/Core/IView.cs @@ -70,12 +70,12 @@ public interface IView : IElement, ITransform double Width { get; } /// - /// Gets the specified minimum width constraint of the IView. + /// 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. + /// Gets the specified maximum width constraint of the IView, between zero and double.PositiveInfinity. /// double MaximumWidth { get; } @@ -85,12 +85,12 @@ public interface IView : IElement, ITransform double Height { get; } /// - /// Gets the specified minimum height constraint of the IView. + /// 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. + /// Gets the specified maximum height constraint of the IView, between zero and double.PositiveInfinity. /// double MaximumHeight { get; } diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs index 5fa916430a9e..bf1057edc09c 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.Measure; namespace Microsoft.Maui.Handlers { @@ -68,13 +69,13 @@ 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 (maximumSize >= 0) + else if (IsMaximumSet(maximumSize)) { mode = MeasureSpecMode.AtMost; constraint = maximumSize; diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs index c1e1c7b8ee98..32b63e26cb38 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.Measure; namespace Microsoft.Maui.Handlers { @@ -68,20 +69,20 @@ double ResolveConstraints(double measured, double exact, double min, double max) { var resolved = measured; - if (exact >= 0) + if (IsExplicitSet(exact)) { // If an exact value has been specified, try to use that resolved = exact; } - if (max >= 0 && resolved > max) + 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 (min >= 0 && resolved < min) + 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 diff --git a/src/Core/src/Layouts/LayoutManager.cs b/src/Core/src/Layouts/LayoutManager.cs index e48491c40cf8..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,16 @@ 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, double min = -1, double max = -1) + public static double ResolveConstraints(double externalConstraint, double explicitLength, double measuredLength, double min = Minimum, double max = Maximum) { - var length = explicitLength >= 0 ? explicitLength : measuredLength; + var length = IsExplicitSet(explicitLength) ? explicitLength : measuredLength; - if (max >= 0 && max < length) + if (max < length) { length = max; } - if (min >= 0 && min > length) + if (min > length) { length = min; } diff --git a/src/Core/src/Platform/Android/ViewExtensions.cs b/src/Core/src/Platform/Android/ViewExtensions.cs index 3819768bca4b..46134897f7ad 100644 --- a/src/Core/src/Platform/Android/ViewExtensions.cs +++ b/src/Core/src/Platform/Android/ViewExtensions.cs @@ -117,14 +117,7 @@ public static void UpdateHeight(this AView nativeView, IView view) public static void UpdateMinimumHeight(this AView nativeView, IView view) { - var xplatMinHeight = view.MinimumHeight; - - if (xplatMinHeight < 0) - { - xplatMinHeight = 0; - } - - var value = (int)nativeView.Context!.ToPixels(xplatMinHeight); + var value = (int)nativeView.Context!.ToPixels(view.MinimumHeight); nativeView.SetMinimumHeight(value); if (!nativeView.IsInLayout) @@ -135,13 +128,7 @@ public static void UpdateMinimumHeight(this AView nativeView, IView view) public static void UpdateMinimumWidth(this AView nativeView, IView view) { - var xplatMinWidth = view.MinimumWidth; - if (xplatMinWidth < 0) - { - xplatMinWidth = 0; - } - - var value = (int)nativeView.Context!.ToPixels(xplatMinWidth); + var value = (int)nativeView.Context!.ToPixels(view.MinimumWidth); nativeView.SetMinimumWidth(value); if (!nativeView.IsInLayout) @@ -152,6 +139,8 @@ public static void UpdateMinimumWidth(this AView nativeView, IView view) public static void UpdateMaximumHeight(this AView nativeView, IView view) { + var xplatHeight = view.MaximumHeight; + // GetDesiredSize will take the specified Height into account during the layout if (!nativeView.IsInLayout) { diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index 09c764939a1f..6a6d4fda10ec 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -131,22 +131,22 @@ public static void UpdateHeight(this FrameworkElement nativeView, IView view) public static void UpdateMinimumHeight(this FrameworkElement nativeView, IView view) { - nativeView.MinHeight = view.MinimumHeight >= 0 ? view.MinimumHeight : 0; + nativeView.MinHeight = view.MinimumHeight; } public static void UpdateMinimumWidth(this FrameworkElement nativeView, IView view) { - nativeView.MinWidth = view.MinimumWidth >= 0 ? view.MinimumWidth : 0; + nativeView.MinWidth = view.MinimumWidth; } public static void UpdateMaximumHeight(this FrameworkElement nativeView, IView view) { - nativeView.MaxHeight = view.MaximumHeight >= 0 ? view.MaximumHeight : double.PositiveInfinity; + nativeView.MaxHeight = view.MaximumHeight; } public static void UpdateMaximumWidth(this FrameworkElement nativeView, IView view) { - nativeView.MaxWidth = view.MaximumWidth >= 0 ? view.MaximumWidth : double.PositiveInfinity; + nativeView.MaxWidth = view.MaximumWidth; } } } \ No newline at end of file diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs index 0765971c014b..d1e803bad59c 100644 --- a/src/Core/src/Platform/iOS/ViewExtensions.cs +++ b/src/Core/src/Platform/iOS/ViewExtensions.cs @@ -169,45 +169,21 @@ public static void UpdateHeight(this UIView nativeView, IView view) public static void UpdateMinimumHeight(this UIView nativeView, IView view) { - if (view.MinimumHeight == -1) - { - // Ignore the initial setting of the value; the initial layout will take care of it - return; - } - UpdateFrame(nativeView, view); } public static void UpdateMaximumHeight(this UIView nativeView, IView view) { - if (view.MaximumHeight == -1) - { - // Ignore the initial setting of the value; the initial layout will take care of it - return; - } - UpdateFrame(nativeView, view); } public static void UpdateMinimumWidth(this UIView nativeView, IView view) { - if (view.MaximumWidth == -1) - { - // Ignore the initial setting of the value; the initial layout will take care of it - return; - } - UpdateFrame(nativeView, view); } public static void UpdateMaximumWidth(this UIView nativeView, IView view) { - if (view.MaximumWidth == -1) - { - // Ignore the initial setting of the value; the initial layout will take care of it - return; - } - UpdateFrame(nativeView, view); } diff --git a/src/Core/src/Primitives/Measure.cs b/src/Core/src/Primitives/Measure.cs new file mode 100644 index 000000000000..5572eafe6e16 --- /dev/null +++ b/src/Core/src/Primitives/Measure.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/DeviceTests/Stubs/StubBase.cs b/src/Core/tests/DeviceTests/Stubs/StubBase.cs index 14c88f52a657..961d753a58f2 100644 --- a/src/Core/tests/DeviceTests/Stubs/StubBase.cs +++ b/src/Core/tests/DeviceTests/Stubs/StubBase.cs @@ -38,13 +38,13 @@ IElementHandler IElement.Handler public double Height { get; set; } = 50; - public double MaximumWidth { get; set; } = -1; + public double MaximumWidth { get; set; } = Primitives.Dimension.Maximum; - public double MaximumHeight { get; set; } = -1; + public double MaximumHeight { get; set; } = Primitives.Dimension.Maximum; - public double MinimumWidth { get; set; } = -1; + public double MinimumWidth { get; set; } = Primitives.Dimension.Minimum; - public double MinimumHeight { get; set; } = -1; + public double MinimumHeight { get; set; } = Primitives.Dimension.Minimum; public double TranslationX { get; set; } diff --git a/src/Core/tests/UnitTests/Layouts/AbsoluteLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/AbsoluteLayoutManagerTests.cs index d10feb963670..a9afbb03ecf2 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; } @@ -223,7 +228,6 @@ public void RelativePositionRespectsPadding(double left, double top, [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 abs = CreateTestLayout(); @@ -245,7 +249,6 @@ public void MeasureRespectsMaxHeight(double maxHeight, double viewHeight, double [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 abs = CreateTestLayout(); @@ -266,7 +269,6 @@ public void MeasureRespectsMaxWidth(double maxWidth, double viewWidth, double ex [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 abs = CreateTestLayout(); @@ -287,7 +289,6 @@ public void MeasureRespectsMinHeight(double minHeight, double viewHeight, double [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 abs = CreateTestLayout(); 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 44da96d1c54d..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,12 +38,13 @@ IGridLayout CreateGridLayout(int rowSpacing = 0, int colSpacing = 0, } var grid = Substitute.For(); - grid.Width.Returns(-1); - grid.Height.Returns(-1); - grid.MaximumWidth.Returns(-1); - grid.MinimumHeight.Returns(-1); - grid.MinimumWidth.Returns(-1); - grid.MaximumHeight.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); diff --git a/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs index ecbcb5032bc3..b8af0b4d9db7 100644 --- a/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs +++ b/src/Core/tests/UnitTests/Layouts/HorizontalStackLayoutManagerTests.cs @@ -68,7 +68,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)); @@ -234,7 +234,6 @@ public void ArrangeRespectsBounds() [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 stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: viewHeight); @@ -251,7 +250,6 @@ public void MeasureRespectsMaxHeight(double maxHeight, double viewHeight, double [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 stack = BuildStack(viewCount: 1, viewWidth: viewWidth, viewHeight: 100); @@ -268,7 +266,6 @@ public void MeasureRespectsMaxWidth(double maxWidth, double viewWidth, double ex [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 stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: viewHeight); @@ -285,7 +282,6 @@ public void MeasureRespectsMinHeight(double minHeight, double viewHeight, double [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 stack = BuildStack(viewCount: 1, viewWidth: viewWidth, viewHeight: 100); diff --git a/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs b/src/Core/tests/UnitTests/Layouts/StackLayoutManagerTests.cs index 6a96a84df8c9..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,12 +12,12 @@ public abstract class StackLayoutManagerTests protected IStackLayout CreateTestLayout() { var stack = Substitute.For(); - stack.Height.Returns(-1); - stack.Width.Returns(-1); - stack.MinimumHeight.Returns(-1); - stack.MinimumWidth.Returns(-1); - stack.MaximumHeight.Returns(-1); - stack.MaximumWidth.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 b8b0b46bde24..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)); @@ -192,7 +193,6 @@ public void ArrangeRespectsBounds() [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 stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: viewHeight); @@ -209,7 +209,6 @@ public void MeasureRespectsMaxHeight(double maxHeight, double viewHeight, double [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 stack = BuildStack(viewCount: 1, viewWidth: viewWidth, viewHeight: 100); @@ -226,7 +225,6 @@ public void MeasureRespectsMaxWidth(double maxWidth, double viewWidth, double ex [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 stack = BuildStack(viewCount: 1, viewWidth: 100, viewHeight: viewHeight); @@ -243,7 +241,6 @@ public void MeasureRespectsMinHeight(double minHeight, double viewHeight, double [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 stack = BuildStack(viewCount: 1, viewWidth: viewWidth, viewHeight: 100); From 2a9e4f5b8577264b0b50dee6e99b732e8febb34e Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Wed, 25 Aug 2021 20:14:34 -0600 Subject: [PATCH 03/19] Fix up device tests and directly set Windows height/width --- src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs | 2 +- src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs | 2 +- src/Core/src/Platform/Windows/ViewExtensions.cs | 10 ++++++---- .../DeviceTests/Handlers/HandlerTestBase.Android.cs | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs index bf1057edc09c..81ae92af43b2 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs @@ -3,7 +3,7 @@ using Android.Content; using Android.Views; using Microsoft.Maui.Graphics; -using static Microsoft.Maui.Primitives.Measure; +using static Microsoft.Maui.Primitives.Dimension; namespace Microsoft.Maui.Handlers { diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs index 32b63e26cb38..ed086b507f56 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.iOS.cs @@ -1,6 +1,6 @@ using Microsoft.Maui.Graphics; using UIKit; -using static Microsoft.Maui.Primitives.Measure; +using static Microsoft.Maui.Primitives.Dimension; namespace Microsoft.Maui.Handlers { diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index 6a6d4fda10ec..e4061ccc24a2 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -119,14 +119,16 @@ 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) diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs index 07556c61f6f9..f84df4ac61b5 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs @@ -132,7 +132,7 @@ public async Task MinimumHeightInitializes(double minHeight) var expected = view.MinimumHeight == -1 ? 0 : view.MinimumHeight; var result = await GetValueAsync(view, handler => GetMinHeight(handler)); - Assert.Equal(expected, result, 4); + Assert.Equal(expected, result, 0); } [Theory] @@ -149,7 +149,7 @@ public async Task MinimumWidthInitializes(double minWidth) var expected = view.MinimumWidth == -1 ? 0 : view.MinimumWidth; var result = await GetValueAsync(view, handler => GetMinWidth(handler)); - Assert.Equal(expected, result, 4); + Assert.Equal(expected, result, 0); } protected string GetAutomationId(IViewHandler viewHandler) => From 77d52f27abde0e0c8a75d6e3f4bf37d64cc31ea9 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Wed, 25 Aug 2021 20:33:16 -0600 Subject: [PATCH 04/19] Add implementation to benchmark stub; make negative dimension exceptions consistent --- .../VisualElement/VisualElement.Impl.cs | 76 ++++++++++++++----- src/Core/tests/Benchmarks/Stubs/StubBase.cs | 8 ++ 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs index d74bcb1a95c6..5478189de3c6 100644 --- a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs @@ -117,6 +117,14 @@ Semantics IView.Semantics internal Semantics SetupSemantics() => _semantics ??= new Semantics(); + static void ValidatePositive(double value, string name) + { + if (value < 0) + { + throw new InvalidOperationException($"{name} cannot be less than zero."); + } + } + double IView.Width { get @@ -127,14 +135,9 @@ double IView.Width } // Access once up front to avoid multiple GetValue calls - var widthRequest = WidthRequest; - - if (widthRequest < 0) - { - throw new InvalidOperationException($"{nameof(IView.Width)} cannot be less than zero."); - } - - return widthRequest; + var value = WidthRequest; + ValidatePositive(value, nameof(IView.Width)); + return value; } } @@ -148,24 +151,57 @@ double IView.Height } // Access once up front to avoid multiple GetValue calls - var heightRequest = HeightRequest; - - if (heightRequest < 0) - { - throw new InvalidOperationException($"{nameof(IView.Height)} cannot be less than zero."); - } - - return heightRequest; + var value = HeightRequest; + ValidatePositive(value, nameof(IView.Height)); + return value; } } // Default value from VisualElement is -1 (legacy); translating to Core we'll use a reasonable value of zero - double IView.MinimumWidth => MinimumWidthRequest < 0 ? 0 : MinimumWidthRequest; - double IView.MinimumHeight => MinimumHeightRequest < 0 ? 0 : MinimumHeightRequest; + double IView.MinimumWidth + { + get + { + // Access once up front to avoid multiple GetValue calls + var value = MinimumWidthRequest; + ValidatePositive(value, nameof(IView.MinimumWidth)); + return value; + } + } + + double IView.MinimumHeight + { + get + { + // Access once up front to avoid multiple GetValue calls + var value = MinimumHeightRequest; + ValidatePositive(value, nameof(IView.MinimumHeight)); + return value; + } + } // Default value from VisualElement is PositiveInfinity - double IView.MaximumWidth => MaximumWidthRequest < 0 ? 0 : MaximumWidthRequest; - double IView.MaximumHeight => MaximumHeightRequest < 0 ? 0 : MaximumHeightRequest; + 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/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; } From b09b57ee9a5367796c520f13e82b1b284df8df68 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Wed, 25 Aug 2021 20:43:57 -0600 Subject: [PATCH 05/19] Check for unset values in min height/width VisualElement properties --- .../HandlerImpl/VisualElement/VisualElement.Impl.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs index 5478189de3c6..c17490b3e340 100644 --- a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs @@ -162,6 +162,11 @@ 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)); @@ -173,6 +178,11 @@ 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)); From 3cd0fb1e485a6257e803398a5e4ee85de3ceba9f Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Wed, 25 Aug 2021 20:45:09 -0600 Subject: [PATCH 06/19] Remove now irrelevant comments --- .../src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs index c17490b3e340..183f3b8c2338 100644 --- a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs @@ -157,7 +157,6 @@ double IView.Height } } - // Default value from VisualElement is -1 (legacy); translating to Core we'll use a reasonable value of zero double IView.MinimumWidth { get @@ -190,7 +189,6 @@ double IView.MinimumHeight } } - // Default value from VisualElement is PositiveInfinity double IView.MaximumWidth { get From edd47199748812eaddd3688ed302c5777666e31a Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Wed, 25 Aug 2021 22:11:17 -0600 Subject: [PATCH 07/19] Use correct property --- .../src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs index 183f3b8c2338..c830a06561d9 100644 --- a/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/VisualElement/VisualElement.Impl.cs @@ -129,7 +129,7 @@ double IView.Width { get { - if (!IsSet(WidthProperty)) + if (!IsSet(WidthRequestProperty)) { return Primitives.Dimension.Unset; } @@ -145,7 +145,7 @@ double IView.Height { get { - if (!IsSet(HeightProperty)) + if (!IsSet(HeightRequestProperty)) { return Primitives.Dimension.Unset; } From c0039ab497e8171d8c831950688f0b9ba05740b6 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 00:11:57 -0600 Subject: [PATCH 08/19] Remove the tests that no longer make any sense --- .../tests/DeviceTests/Handlers/HandlerTestBase.Android.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs index f84df4ac61b5..c80085eb4456 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs @@ -121,7 +121,6 @@ public async Task RotationYInitializeCorrectly(double rotationY) [Theory] [InlineData(0)] [InlineData(100)] - [InlineData(-1)] public async Task MinimumHeightInitializes(double minHeight) { var view = new TStub() @@ -129,7 +128,7 @@ public async Task MinimumHeightInitializes(double minHeight) MinimumHeight = minHeight }; - var expected = view.MinimumHeight == -1 ? 0 : view.MinimumHeight; + var expected = view.MinimumHeight; var result = await GetValueAsync(view, handler => GetMinHeight(handler)); Assert.Equal(expected, result, 0); @@ -138,7 +137,6 @@ public async Task MinimumHeightInitializes(double minHeight) [Theory] [InlineData(0)] [InlineData(100)] - [InlineData(-1)] public async Task MinimumWidthInitializes(double minWidth) { var view = new TStub() @@ -146,7 +144,7 @@ public async Task MinimumWidthInitializes(double minWidth) MinimumWidth = minWidth }; - var expected = view.MinimumWidth == -1 ? 0 : view.MinimumWidth; + var expected = view.MinimumWidth; var result = await GetValueAsync(view, handler => GetMinWidth(handler)); Assert.Equal(expected, result, 0); From 78bc63178ec066e81270e9a171fd2f87d10110f4 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 00:33:52 -0600 Subject: [PATCH 09/19] Not sure how this test ever worked --- src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs b/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs index 8c5c1bc89154..b7dcc5994788 100644 --- a/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs +++ b/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs @@ -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); From 494e682e39de8379add84d2a27cca01267f17e16 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 01:00:42 -0600 Subject: [PATCH 10/19] shrug --- src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs b/src/Controls/tests/Core.UnitTests/Layouts/LayoutCompatTests.cs index b7dcc5994788..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); From d193d826b646e3fb67f78faa3edd5bdec4ed00b4 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 09:49:27 -0600 Subject: [PATCH 11/19] Cleanup --- src/Core/src/Platform/Android/ViewExtensions.cs | 2 -- src/Core/src/Primitives/{Measure.cs => Dimension.cs} | 0 2 files changed, 2 deletions(-) rename src/Core/src/Primitives/{Measure.cs => Dimension.cs} (100%) diff --git a/src/Core/src/Platform/Android/ViewExtensions.cs b/src/Core/src/Platform/Android/ViewExtensions.cs index 46134897f7ad..da2d570c84e3 100644 --- a/src/Core/src/Platform/Android/ViewExtensions.cs +++ b/src/Core/src/Platform/Android/ViewExtensions.cs @@ -139,8 +139,6 @@ public static void UpdateMinimumWidth(this AView nativeView, IView view) public static void UpdateMaximumHeight(this AView nativeView, IView view) { - var xplatHeight = view.MaximumHeight; - // GetDesiredSize will take the specified Height into account during the layout if (!nativeView.IsInLayout) { diff --git a/src/Core/src/Primitives/Measure.cs b/src/Core/src/Primitives/Dimension.cs similarity index 100% rename from src/Core/src/Primitives/Measure.cs rename to src/Core/src/Primitives/Dimension.cs From 8b9203bb063302535d3419825bd8439fa0ba0352 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 10:55:07 -0600 Subject: [PATCH 12/19] Use correct version of Width/Height for ScrollView --- src/Controls/src/Core/HandlerImpl/ScrollView.Impl.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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; From df6fd4686d846551cca76d8a70ab83996db5c3f6 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 12:56:11 -0600 Subject: [PATCH 13/19] Use measurespec instead of raw values when measuring PageViewGroup --- .../Core/src/Android/Renderers/PageContainer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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); } } From c50884c38a15614dac2dbe6e328e80fa8787ead7 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 13:03:24 -0600 Subject: [PATCH 14/19] Don't let VET force the page back to 0x0 --- src/Compatibility/Core/src/Android/VisualElementTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. } From bf45fa9b22a6412c287ba18ce166bd17adc7c446 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 14:35:07 -0600 Subject: [PATCH 15/19] Stomp out another -1 --- src/Core/src/Layouts/LayoutExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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; } From 35de25ed487aae83bed38f158b3628acd061926b Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 14:43:36 -0600 Subject: [PATCH 16/19] Use correct unset value --- .../UnitTests/Layouts/LayoutExtensionTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Core/tests/UnitTests/Layouts/LayoutExtensionTests.cs b/src/Core/tests/UnitTests/Layouts/LayoutExtensionTests.cs index 70acd726e6fd..b1c290f4e5bb 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); var frame = element.ComputeFrame(new Rectangle(offset.X, offset.Y, widthConstraint, heightConstraint)); @@ -151,8 +151,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); var frame = element.ComputeFrame(new Rectangle(offset.X, offset.Y, widthConstraint, heightConstraint)); @@ -209,8 +209,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)); From a264511965d444a6f8b6f5233bd2fdb5c1012705 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Fri, 27 Aug 2021 04:01:24 +0200 Subject: [PATCH 17/19] stomp stomp stomp --- src/Core/src/Platform/iOS/ViewExtensions.cs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs index 061a92decd85..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,23 +166,11 @@ 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 setting of the value; 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 setting of the value; the initial layout will take care of it - return; - } - UpdateFrame(nativeView, view); } @@ -207,8 +196,14 @@ public static void UpdateMaximumWidth(this UIView nativeView, IView 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); } From 198e6a2dcb7969e8af6cfcc8b71a5fa4546ee371 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 23:42:22 -0600 Subject: [PATCH 18/19] Add extra MauiContext null check for when the old layout system tries to force a layout too early --- src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs b/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs index 81ae92af43b2..0ffaf1149906 100644 --- a/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs +++ b/src/Core/src/Handlers/View/ViewHandlerOfT.Android.cs @@ -27,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; } From b72e475caa40285b912f2ca3b7952bf08273c0dc Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 26 Aug 2021 23:48:36 -0600 Subject: [PATCH 19/19] Enable AbsoluteLayout for iOS --- src/Compatibility/Core/src/AppHostBuilderExtensions.cs | 4 ---- 1 file changed, 4 deletions(-) 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();