Skip to content

Commit 0fd2a37

Browse files
Merge implementations of ConstrainedBox to handle all the scenarios
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
1 parent 4041c5c commit 0fd2a37

File tree

4 files changed

+186
-12
lines changed

4 files changed

+186
-12
lines changed

Microsoft.Toolkit.Uwp.UI.Controls.Primitives/ConstrainedBox/ConstrainedBox.cs

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,39 +88,102 @@ private static void ScalePropertyChanged(DependencyObject d, DependencyPropertyC
8888
}
8989
}
9090

91+
private bool IsPositiveRealNumber(double value) => !double.IsNaN(value) && !double.IsInfinity(value) && value > 0;
92+
93+
private Size _lastMeasuredSize;
94+
9195
/// <inheritdoc/>
9296
protected override Size MeasureOverride(Size availableSize)
9397
{
94-
return base.MeasureOverride(CalculateConstrainedSize(availableSize));
98+
CalculateConstrainedSize(ref availableSize);
99+
100+
_lastMeasuredSize = availableSize;
101+
102+
// Call base.MeasureOverride so any child elements know what room there is to work with.
103+
// Don't return this though. An image that hasn't loaded yet for example will request very little space.
104+
base.MeasureOverride(_lastMeasuredSize);
105+
return _lastMeasuredSize;
95106
}
96107

108+
//// 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.)
109+
97110
/// <inheritdoc/>
98111
protected override Size ArrangeOverride(Size finalSize)
99112
{
100-
return base.ArrangeOverride(CalculateConstrainedSize(finalSize));
113+
// Even though we requested in measure to be a specific size, that doesn't mean our parent
114+
// panel respected that request. Grid for instance can by default Stretch and if you don't
115+
// set Horizontal/VerticalAlignment on the control it won't constrain as we expect.
116+
// We could also be in a StackPanel/ScrollViewer where it wants to provide as much space as possible.
117+
// However, if we always re-calculate even if we are provided the proper finalSize, this can trigger
118+
// multiple arrange passes and cause a rounding error in layout. Therefore, we only want to
119+
// re-calculate if we think we will have a significant impact.
120+
//// TODO: Not sure what good tolerance is here
121+
if (Math.Abs(finalSize.Width - _lastMeasuredSize.Width) > 1.5 ||
122+
Math.Abs(finalSize.Height - _lastMeasuredSize.Height) > 1.5)
123+
{
124+
CalculateConstrainedSize(ref finalSize);
125+
126+
// Copy again so if Arrange is re-triggered we won't re-calculate.
127+
_lastMeasuredSize = finalSize;
128+
}
129+
130+
return base.ArrangeOverride(finalSize);
101131
}
102132

103-
private Size CalculateConstrainedSize(Size initialSize)
133+
private void CalculateConstrainedSize(ref Size availableSize)
104134
{
105-
var availableSize = new Size(initialSize.Width * ScaleX, initialSize.Height * ScaleY);
135+
var hasWidth = IsPositiveRealNumber(availableSize.Width);
136+
var hasHeight = IsPositiveRealNumber(availableSize.Height);
137+
138+
if (!hasWidth && !hasHeight)
139+
{
140+
// We have infinite space, like a ScrollViewer with both scrolling directions
141+
// Ask child how big they want to be first.
142+
availableSize = base.MeasureOverride(availableSize);
143+
144+
hasWidth = IsPositiveRealNumber(availableSize.Width);
145+
hasHeight = IsPositiveRealNumber(availableSize.Height);
146+
147+
if (!hasWidth && !hasHeight)
148+
{
149+
// At this point we have no way to determine a constraint, the Panel won't do anything
150+
// This should be rare? We don't really have a way to provide a warning here.
151+
return;
152+
}
153+
}
154+
155+
// Scale size first before we constrain aspect ratio
156+
availableSize.Width *= ScaleX;
157+
availableSize.Height *= ScaleY;
106158

107159
// If we don't have an Aspect Ratio, just return the scaled value.
108160
if (ReadLocalValue(AspectRatioProperty) == DependencyProperty.UnsetValue)
109161
{
110-
return availableSize;
162+
return;
111163
}
112164

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

117-
if (currentAspect >= desiredAspect)
168+
if (!hasWidth)
169+
{
170+
// If available width is infinite, set width based on height
171+
availableSize.Width = availableSize.Height * AspectRatio;
172+
}
173+
else if (!hasHeight)
174+
{
175+
// If avalable height is infinite, set height based on width
176+
availableSize.Height = availableSize.Width / AspectRatio;
177+
}
178+
else if (currentAspect > AspectRatio)
118179
{
119-
return new Size(availableSize.Height * desiredAspect, availableSize.Height);
180+
// If the container aspect ratio is wider than our aspect ratio, set width based on height
181+
availableSize.Width = availableSize.Height * AspectRatio;
120182
}
121183
else
122184
{
123-
return new Size(availableSize.Width, availableSize.Width / desiredAspect);
185+
// If the container aspect ratio is taller than our aspect ratio, set height based on width
186+
availableSize.Height = availableSize.Width / AspectRatio;
124187
}
125188
}
126189
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Microsoft.Toolkit.Uwp;
8+
using Microsoft.Toolkit.Uwp.UI;
9+
using Microsoft.Toolkit.Uwp.UI.Controls;
10+
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
12+
using Windows.Foundation;
13+
using Windows.UI.Xaml;
14+
using Windows.UI.Xaml.Controls;
15+
using Windows.UI.Xaml.Markup;
16+
17+
namespace UnitTests.UWP.UI.Controls
18+
{
19+
[TestClass]
20+
public class Test_ConstrainedBox : VisualUITestBase
21+
{
22+
[TestCategory("ConstrainedBox")]
23+
[TestMethod]
24+
public async Task Test_ConstrainedBox_Normal_Horizontal()
25+
{
26+
await App.DispatcherQueue.EnqueueAsync(async () =>
27+
{
28+
var treeRoot = XamlReader.Load(@"<Page
29+
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
30+
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
31+
xmlns:controls=""using:Microsoft.Toolkit.Uwp.UI.Controls"">
32+
<controls:ConstrainedBox x:Name=""ConstrainedBox"" AspectRatio=""2:1"" Width=""200"">
33+
<Border HorizontalAlignment=""Stretch"" VerticalAlignment=""Stretch"" Background=""Red""/>
34+
</controls:ConstrainedBox>
35+
</Page>") as FrameworkElement;
36+
37+
Assert.IsNotNull(treeRoot, "Could not load XAML tree.");
38+
39+
// Initialize Visual Tree
40+
await SetTestContentAsync(treeRoot);
41+
42+
var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox;
43+
44+
Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree.");
45+
46+
// Force Layout calculations
47+
panel.UpdateLayout();
48+
49+
var child = panel.Content as Border;
50+
51+
Assert.IsNotNull(child, "Could not find inner Border");
52+
53+
// Check Size
54+
Assert.AreEqual(child.ActualWidth, 200, "Width unexpected");
55+
Assert.AreEqual(child.ActualHeight, 100, "Height unexpected");
56+
});
57+
}
58+
59+
[TestCategory("ConstrainedBox")]
60+
[TestMethod]
61+
public async Task Test_ConstrainedBox_Normal_ScaleX()
62+
{
63+
await App.DispatcherQueue.EnqueueAsync(async () =>
64+
{
65+
var treeRoot = XamlReader.Load(@"<Page
66+
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
67+
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
68+
xmlns:controls=""using:Microsoft.Toolkit.Uwp.UI.Controls"">
69+
<Grid x:Name=""ParentGrid"" Width=""200"" Height=""200"">
70+
<controls:ConstrainedBox x:Name=""ConstrainedBox"" ScaleX=""0.5""
71+
HorizontalAlignment=""Stretch"" VerticalAlignment=""Stretch"">
72+
<Border HorizontalAlignment=""Stretch"" VerticalAlignment=""Stretch"" Background=""Red""/>
73+
</controls:ConstrainedBox>
74+
</Grid>
75+
</Page>") as FrameworkElement;
76+
77+
Assert.IsNotNull(treeRoot, "Could not load XAML tree.");
78+
79+
// Initialize Visual Tree
80+
await SetTestContentAsync(treeRoot);
81+
82+
var grid = treeRoot.FindChild("ParentGrid") as Grid;
83+
84+
var panel = treeRoot.FindChild("ConstrainedBox") as ConstrainedBox;
85+
86+
Assert.IsNotNull(panel, "Could not find ConstrainedBox in tree.");
87+
88+
// Force Layout calculations
89+
panel.UpdateLayout();
90+
91+
var child = panel.Content as Border;
92+
93+
Assert.IsNotNull(child, "Could not find inner Border");
94+
95+
// Check Size
96+
Assert.AreEqual(child.ActualWidth, 100);
97+
Assert.AreEqual(child.ActualHeight, 200);
98+
99+
// Check inner Positioning, we do this from the Grid as the ConstainedBox also modifies its own size
100+
// and is hugging the child.
101+
var position = grid.CoordinatesTo(child);
102+
103+
Assert.AreEqual(position.X, 50);
104+
Assert.AreEqual(position.Y, 0);
105+
});
106+
}
107+
}
108+
}

UnitTests/UnitTests.UWP/UnitTestApp.xaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<Application.Resources>
1212

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

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

@@ -43,7 +43,9 @@
4343
</interactivity:Interaction.Behaviors>
4444
</TextBox>
4545

46-
<Style TargetType="controls:UniformGrid" />
46+
<controls:ConstrainedBox x:Key="TestConstrainedBox" />
47+
<controls:UniformGrid x:Key="TestUniformGrid" />
48+
<controls:WrapPanel x:Key="TestWrapPanel" />
4749
<ui:NullableBoolExtension x:Key="nullableBool" />
4850

4951
<helpers:ObjectWithNullableBoolProperty x:Key="objectWithNullableBoolProperty" />

UnitTests/UnitTests.UWP/UnitTests.UWP.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
1+
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
22
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
33
<PropertyGroup>
44
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -231,6 +231,7 @@
231231
<Compile Include="UI\Controls\Test_UniformGrid_FreeSpots.cs" />
232232
<Compile Include="UI\Controls\Test_UniformGrid_Dimensions.cs" />
233233
<Compile Include="UI\Controls\Test_RangeSelector.cs" />
234+
<Compile Include="UI\Controls\Test_ConstrainedBox.cs" />
234235
<Compile Include="UI\Controls\Test_WrapPanel_Visibility.cs" />
235236
<Compile Include="UI\Controls\Test_WrapPanel_BasicLayout.cs" />
236237
<Compile Include="UI\Extensions\Test_VisualExtensions.cs" />

0 commit comments

Comments
 (0)