Skip to content

Commit

Permalink
Merge implementations of ConstrainedBox to handle all the scenarios
Browse files Browse the repository at this point in the history
1px Rounding was caused by multiple unnecessary passes of Arrange, so only re-calculated if needed (when parent Panel doesn't respect Measure request i.e. Grid Stretch)
Also adds in handling for Infinity scenarios like StackPanel/ScrollViewer and can try and respect child measurements
Adds initial basic tests as a starting point for more tests
  • Loading branch information
michael-hawker committed Jul 29, 2021
1 parent 4041c5c commit 0fd2a37
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -88,39 +88,102 @@ private static void ScalePropertyChanged(DependencyObject d, DependencyPropertyC
}
}

private bool IsPositiveRealNumber(double value) => !double.IsNaN(value) && !double.IsInfinity(value) && value > 0;

private Size _lastMeasuredSize;

/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
return base.MeasureOverride(CalculateConstrainedSize(availableSize));
CalculateConstrainedSize(ref availableSize);

_lastMeasuredSize = availableSize;

// Call base.MeasureOverride so any child elements know what room there is to work with.
// Don't return this though. An image that hasn't loaded yet for example will request very little space.
base.MeasureOverride(_lastMeasuredSize);
return _lastMeasuredSize;
}

//// Our Arrange pass should just use the value we calculated in Measure, so we don't have extra work to do (at least the ContentPresenter we use presently does it for us.)

/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
return base.ArrangeOverride(CalculateConstrainedSize(finalSize));
// Even though we requested in measure to be a specific size, that doesn't mean our parent
// panel respected that request. Grid for instance can by default Stretch and if you don't
// set Horizontal/VerticalAlignment on the control it won't constrain as we expect.
// We could also be in a StackPanel/ScrollViewer where it wants to provide as much space as possible.
// However, if we always re-calculate even if we are provided the proper finalSize, this can trigger
// multiple arrange passes and cause a rounding error in layout. Therefore, we only want to
// re-calculate if we think we will have a significant impact.
//// TODO: Not sure what good tolerance is here
if (Math.Abs(finalSize.Width - _lastMeasuredSize.Width) > 1.5 ||
Math.Abs(finalSize.Height - _lastMeasuredSize.Height) > 1.5)
{
CalculateConstrainedSize(ref finalSize);

// Copy again so if Arrange is re-triggered we won't re-calculate.
_lastMeasuredSize = finalSize;
}

return base.ArrangeOverride(finalSize);
}

private Size CalculateConstrainedSize(Size initialSize)
private void CalculateConstrainedSize(ref Size availableSize)
{
var availableSize = new Size(initialSize.Width * ScaleX, initialSize.Height * ScaleY);
var hasWidth = IsPositiveRealNumber(availableSize.Width);
var hasHeight = IsPositiveRealNumber(availableSize.Height);

if (!hasWidth && !hasHeight)
{
// We have infinite space, like a ScrollViewer with both scrolling directions
// Ask child how big they want to be first.
availableSize = base.MeasureOverride(availableSize);

hasWidth = IsPositiveRealNumber(availableSize.Width);
hasHeight = IsPositiveRealNumber(availableSize.Height);

if (!hasWidth && !hasHeight)
{
// At this point we have no way to determine a constraint, the Panel won't do anything
// This should be rare? We don't really have a way to provide a warning here.
return;
}
}

// Scale size first before we constrain aspect ratio
availableSize.Width *= ScaleX;
availableSize.Height *= ScaleY;

// If we don't have an Aspect Ratio, just return the scaled value.
if (ReadLocalValue(AspectRatioProperty) == DependencyProperty.UnsetValue)
{
return availableSize;
return;
}

// Calculate the Aspect Ratio constraint based on the newly scaled size.
var currentAspect = availableSize.Width / availableSize.Height;
var desiredAspect = AspectRatio.Value;

if (currentAspect >= desiredAspect)
if (!hasWidth)
{
// If available width is infinite, set width based on height
availableSize.Width = availableSize.Height * AspectRatio;
}
else if (!hasHeight)
{
// If avalable height is infinite, set height based on width
availableSize.Height = availableSize.Width / AspectRatio;
}
else if (currentAspect > AspectRatio)
{
return new Size(availableSize.Height * desiredAspect, availableSize.Height);
// If the container aspect ratio is wider than our aspect ratio, set width based on height
availableSize.Width = availableSize.Height * AspectRatio;
}
else
{
return new Size(availableSize.Width, availableSize.Width / desiredAspect);
// If the container aspect ratio is taller than our aspect ratio, set height based on width
availableSize.Height = availableSize.Width / AspectRatio;
}
}
}
Expand Down
108 changes: 108 additions & 0 deletions UnitTests/UnitTests.UWP/UI/Controls/Test_ConstrainedBox.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Linq;
using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp;
using Microsoft.Toolkit.Uwp.UI;
using Microsoft.Toolkit.Uwp.UI.Controls;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;

namespace UnitTests.UWP.UI.Controls
{
[TestClass]
public class Test_ConstrainedBox : VisualUITestBase
{
[TestCategory("ConstrainedBox")]
[TestMethod]
public async Task Test_ConstrainedBox_Normal_Horizontal()
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var treeRoot = XamlReader.Load(@"<Page
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:controls=""using:Microsoft.Toolkit.Uwp.UI.Controls"">
<controls:ConstrainedBox x:Name=""ConstrainedBox"" AspectRatio=""2:1"" Width=""200"">
<Border HorizontalAlignment=""Stretch"" VerticalAlignment=""Stretch"" Background=""Red""/>
</controls:ConstrainedBox>
</Page>") as FrameworkElement;

Assert.IsNotNull(treeRoot, "Could not load XAML tree.");

// Initialize Visual Tree
await SetTestContentAsync(treeRoot);

var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox;

Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree.");

// Force Layout calculations
panel.UpdateLayout();

var child = panel.Content as Border;

Assert.IsNotNull(child, "Could not find inner Border");

// Check Size
Assert.AreEqual(child.ActualWidth, 200, "Width unexpected");
Assert.AreEqual(child.ActualHeight, 100, "Height unexpected");
});
}

[TestCategory("ConstrainedBox")]
[TestMethod]
public async Task Test_ConstrainedBox_Normal_ScaleX()
{
await App.DispatcherQueue.EnqueueAsync(async () =>
{
var treeRoot = XamlReader.Load(@"<Page
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:controls=""using:Microsoft.Toolkit.Uwp.UI.Controls"">
<Grid x:Name=""ParentGrid"" Width=""200"" Height=""200"">
<controls:ConstrainedBox x:Name=""ConstrainedBox"" ScaleX=""0.5""
HorizontalAlignment=""Stretch"" VerticalAlignment=""Stretch"">
<Border HorizontalAlignment=""Stretch"" VerticalAlignment=""Stretch"" Background=""Red""/>
</controls:ConstrainedBox>
</Grid>
</Page>") as FrameworkElement;

Assert.IsNotNull(treeRoot, "Could not load XAML tree.");

// Initialize Visual Tree
await SetTestContentAsync(treeRoot);

var grid = treeRoot.FindChild("ParentGrid") as Grid;

var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox;

Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree.");

// Force Layout calculations
panel.UpdateLayout();

var child = panel.Content as Border;

Assert.IsNotNull(child, "Could not find inner Border");

// Check Size
Assert.AreEqual(child.ActualWidth, 100);
Assert.AreEqual(child.ActualHeight, 200);

// Check inner Positioning, we do this from the Grid as the ConstainedBox also modifies its own size
// and is hugging the child.
var position = grid.CoordinatesTo(child);

Assert.AreEqual(position.X, 50);
Assert.AreEqual(position.Y, 0);
});
}
}
}
6 changes: 4 additions & 2 deletions UnitTests/UnitTests.UWP/UnitTestApp.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Application.Resources>

<!-- Workarounds for .NET Native issue in unit tests -->
<ui:EnumValuesExtension x:Key="DummyExtension"/>
<ui:EnumValuesExtension x:Key="DummyExtension" />

<unitTestExtensions:Animal x:Key="Animal">Cat</unitTestExtensions:Animal>

Expand Down Expand Up @@ -43,7 +43,9 @@
</interactivity:Interaction.Behaviors>
</TextBox>

<Style TargetType="controls:UniformGrid" />
<controls:ConstrainedBox x:Key="TestConstrainedBox" />
<controls:UniformGrid x:Key="TestUniformGrid" />
<controls:WrapPanel x:Key="TestWrapPanel" />
<ui:NullableBoolExtension x:Key="nullableBool" />

<helpers:ObjectWithNullableBoolProperty x:Key="objectWithNullableBoolProperty" />
Expand Down
3 changes: 2 additions & 1 deletion UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
Expand Down Expand Up @@ -231,6 +231,7 @@
<Compile Include="UI\Controls\Test_UniformGrid_FreeSpots.cs" />
<Compile Include="UI\Controls\Test_UniformGrid_Dimensions.cs" />
<Compile Include="UI\Controls\Test_RangeSelector.cs" />
<Compile Include="UI\Controls\Test_ConstrainedBox.cs" />
<Compile Include="UI\Controls\Test_WrapPanel_Visibility.cs" />
<Compile Include="UI\Controls\Test_WrapPanel_BasicLayout.cs" />
<Compile Include="UI\Extensions\Test_VisualExtensions.cs" />
Expand Down

0 comments on commit 0fd2a37

Please sign in to comment.