Skip to content

Commit

Permalink
Make layout honor MaxWidth and MaxHeight requests (#15022)
Browse files Browse the repository at this point in the history
* Ensure MaximumWidth is used when arranging

If Width is not explicitly set but MaximumWidth is, we should still use that as part of the arranging calculations

* Account for alignment

* Add tests

* Auto-format source code

---------

Co-authored-by: GitHub Actions Autoformatter <autoformat@example.com>
  • Loading branch information
jknaudt21 and GitHub Actions Autoformatter authored May 11, 2023
1 parent 5151c9b commit 773f8c2
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 14 deletions.
34 changes: 21 additions & 13 deletions src/Core/src/Layouts/LayoutExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ public static Rect ComputeFrame(this IView view, Rect 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;

// But if the element is set to fill horizontally and it doesn't have an explicitly set width,
// then we want the minimum between its MaximumWidth and the bounds' width
// MaximumWidth is always positive infinity if not defined by the user
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
consumedWidth = bounds.Width;
consumedWidth = Math.Min(bounds.Width, view.MaximumWidth);
}

// And the actual frame width needs to subtract the margins
Expand All @@ -52,10 +53,11 @@ public static Rect ComputeFrame(this IView view, Rect bounds)
var consumedHeight = view.DesiredSize.Height;

// 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
// then we want the minimum between its MaximumHeight and the bounds' height
// MaximumHeight is always positive infinity if not defined by the user
if (view.VerticalLayoutAlignment == LayoutAlignment.Fill && !IsExplicitSet(view.Height))
{
consumedHeight = bounds.Height;
consumedHeight = Math.Min(bounds.Height, view.MaximumHeight);
}

// And the actual frame height needs to subtract the margins
Expand All @@ -70,15 +72,17 @@ public static Rect ComputeFrame(this IView view, Rect bounds)
static double AlignHorizontal(IView view, Rect bounds, Thickness margin)
{
var alignment = view.HorizontalLayoutAlignment;
var desiredWidth = view.DesiredSize.Width;

if (alignment == LayoutAlignment.Fill && IsExplicitSet(view.Width))
if (alignment == LayoutAlignment.Fill && (IsExplicitSet(view.Width) || !double.IsInfinity(view.MaximumWidth)))
{
// If the view has an explicit width set and the layout alignment is Fill,
// If the view has an explicit width (or non-infinite MaxWidth) set and the layout alignment is Fill,
// we just treat the view as centered within the space it "fills"
alignment = LayoutAlignment.Center;
}

var desiredWidth = view.DesiredSize.Width;
// If the width is not set, we use the minimum between the MaxWidth or the bound's width
desiredWidth = IsExplicitSet(view.Width) ? desiredWidth : Math.Min(bounds.Width, view.MaximumWidth);
}

return AlignHorizontal(bounds.X, margin.Left, margin.Right, bounds.Width, desiredWidth, alignment);
}
Expand All @@ -105,24 +109,28 @@ static double AlignHorizontal(double startX, double startMargin, double endMargi
static double AlignVertical(IView view, Rect bounds, Thickness margin)
{
var alignment = view.VerticalLayoutAlignment;
var desiredHeight = view.DesiredSize.Height;

if (alignment == LayoutAlignment.Fill && IsExplicitSet(view.Height))
if (alignment == LayoutAlignment.Fill && (IsExplicitSet(view.Height) || !double.IsInfinity(view.MaximumHeight)))
{
// If the view has an explicit height set and the layout alignment is Fill,
// If the view has an explicit height (or non-infinite MaxHeight) set and the layout alignment is Fill,
// we just treat the view as centered within the space it "fills"
alignment = LayoutAlignment.Center;

// If the height is not set, we use the minimum between the MaxHeight or the bound's height
desiredHeight = IsExplicitSet(view.Height) ? desiredHeight : Math.Min(bounds.Height, view.MaximumHeight);
}

double frameY = bounds.Y + margin.Top;

switch (alignment)
{
case LayoutAlignment.Center:
frameY += (bounds.Height - view.DesiredSize.Height) / 2;
frameY += (bounds.Height - desiredHeight) / 2;
break;

case LayoutAlignment.End:
frameY += bounds.Height - view.DesiredSize.Height;
frameY += bounds.Height - desiredHeight;
break;
}

Expand Down
110 changes: 109 additions & 1 deletion src/Core/tests/UnitTests/Layouts/LayoutExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Layouts;
using Microsoft.Maui.Primitives;
Expand All @@ -18,6 +19,8 @@ public void FrameExcludesMargin()
element.Margin.Returns(margin);
element.Width.Returns(Dimension.Unset);
element.Height.Returns(Dimension.Unset);
element.MaximumWidth.Returns(Dimension.Maximum);
element.MaximumHeight.Returns(Dimension.Maximum);

var bounds = new Rect(0, 0, 100, 100);
var frame = element.ComputeFrame(bounds);
Expand Down Expand Up @@ -145,6 +148,8 @@ public void FrameAccountsForHorizontalLayoutAlignment(LayoutAlignment layoutAlig
element.HorizontalLayoutAlignment.Returns(layoutAlignment);
element.Width.Returns(Dimension.Unset);
element.Height.Returns(Dimension.Unset);
element.MaximumWidth.Returns(Dimension.Maximum);
element.MaximumHeight.Returns(Dimension.Maximum);
element.FlowDirection.Returns(FlowDirection.LeftToRight);

var frame = element.ComputeFrame(new Rect(offset.X, offset.Y, widthConstraint, heightConstraint));
Expand All @@ -170,6 +175,8 @@ public void FrameAccountsForVerticalLayoutAlignment(LayoutAlignment layoutAlignm
element.VerticalLayoutAlignment.Returns(layoutAlignment);
element.Width.Returns(Dimension.Unset);
element.Height.Returns(Dimension.Unset);
element.MaximumWidth.Returns(Dimension.Maximum);
element.MaximumHeight.Returns(Dimension.Maximum);
element.FlowDirection.Returns(FlowDirection.LeftToRight);

var frame = element.ComputeFrame(new Rect(offset.X, offset.Y, widthConstraint, heightConstraint));
Expand Down Expand Up @@ -269,5 +276,106 @@ public void HeightOverridesFillFromCenter()

Assert.Equal(expectedY, frame.Top);
}

[Theory]
[InlineData(0, 300)]
[InlineData(300, 0)]
[InlineData(Dimension.Maximum, 300)]
[InlineData(Dimension.Maximum, 0)]
public void HorizontalFillRespectsMaxWidth(double maxWidth, double widthConstraint)
{
var heightConstraint = 300;
var desiredSize = new Size(50, 50);

var element = Substitute.For<IView>();
element.DesiredSize.Returns(desiredSize);
element.HorizontalLayoutAlignment.Returns(LayoutAlignment.Fill);
element.Width.Returns(Dimension.Unset);
element.MaximumWidth.Returns(maxWidth);

var frame = element.ComputeFrame(new Rect(0, 0, widthConstraint, heightConstraint));

// The width should always be the minimum between the width constraint and the element's MaximumWidth
var expectedWidth = Math.Min(maxWidth, widthConstraint);

Assert.Equal(expectedWidth, frame.Width);
}

[Theory]
[InlineData(0, 300)]
[InlineData(300, 0)]
[InlineData(Dimension.Maximum, 300)]
[InlineData(Dimension.Maximum, 0)]
public void VerticalFillRespectsMaxHeight(double maxHeight, double heightConstraint)
{
var widthConstraint = 300;
var desiredSize = new Size(50, 50);

var element = Substitute.For<IView>();
element.DesiredSize.Returns(desiredSize);
element.VerticalLayoutAlignment.Returns(LayoutAlignment.Fill);
element.Height.Returns(Dimension.Unset);
element.MaximumHeight.Returns(maxHeight);

var frame = element.ComputeFrame(new Rect(0, 0, widthConstraint, heightConstraint));

// The width should always be the minimum between the width constraint and the element's MaximumWidth
var expectedHeight = Math.Min(maxHeight, heightConstraint);

Assert.Equal(expectedHeight, frame.Height);
}


[Fact]
public void MaxWidthOverridesFromCenter()
{
var widthConstraint = 300;
var heightConstraint = 300;
var desiredSize = new Size(50, 50);

var maxWidth = 100;

var element = Substitute.For<IView>();
element.DesiredSize.Returns(desiredSize);
element.HorizontalLayoutAlignment.Returns(LayoutAlignment.Fill);
element.Width.Returns(Dimension.Unset);
element.MaximumWidth.Returns(maxWidth);

var frame = element.ComputeFrame(new Rect(0, 0, widthConstraint, heightConstraint));

// Since we set MaxWidth (and its less than the width constraint), our expected width should win over fill
// We want to do the filling from the center of the space, so the top edge of the frame should be
// the center, minus half of the view
var expectedWidth = Math.Min(maxWidth, widthConstraint);
var expectedX = (widthConstraint / 2) - (expectedWidth / 2);

Assert.Equal(expectedX, frame.X);
}

[Fact]
public void MaxHeightOverridesFromCenter()
{
var widthConstraint = 300;
var heightConstraint = 300;
var desiredSize = new Size(50, 50);

var maxHeight = 100;

var element = Substitute.For<IView>();
element.DesiredSize.Returns(desiredSize);
element.HorizontalLayoutAlignment.Returns(LayoutAlignment.Fill);
element.Height.Returns(Dimension.Unset);
element.MaximumHeight.Returns(maxHeight);

var frame = element.ComputeFrame(new Rect(0, 0, widthConstraint, heightConstraint));

// Since we set MaxHeight (and its less than the height constraint), our expected height should win over fill
// We want to do the filling from the center of the space, so the top edge of the frame should be
// the center, minus half of the view
var expectedHeight = Math.Min(maxHeight, heightConstraint);
var expectedY = (heightConstraint / 2) - (expectedHeight / 2);

Assert.Equal(expectedY, frame.Y);
}
}
}

0 comments on commit 773f8c2

Please sign in to comment.