Browse Source

Merge branch 'LayoutTransform' of https://github.com/donandren/Perspex into donandren-LayoutTransform

pull/482/head
Steven Kirk 11 years ago
parent
commit
ba4b3b0794
  1. 389
      src/Perspex.Controls/LayoutTransformControl.cs
  2. 1
      src/Perspex.Controls/Perspex.Controls.csproj
  3. 6
      src/Perspex.SceneGraph/Matrix.cs
  4. 69
      src/Perspex.SceneGraph/Media/ScaleTransform.cs
  5. 1
      src/Perspex.SceneGraph/Perspex.SceneGraph.csproj
  6. 1
      src/Perspex.Themes.Default/DefaultTheme.xaml
  7. 12
      src/Perspex.Themes.Default/LayoutTransformControl.xaml
  8. 5
      src/Perspex.Themes.Default/Perspex.Themes.Default.csproj
  9. 245
      tests/Perspex.Controls.UnitTests/LayoutTransformControlTests.cs
  10. 1
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

389
src/Perspex.Controls/LayoutTransformControl.cs

@ -0,0 +1,389 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
//
// Idea got from and adapted to work in perspex
// http://silverlight.codeplex.com/SourceControl/changeset/view/74775#Release/Silverlight4/Source/Controls.Layout.Toolkit/LayoutTransformer/LayoutTransformer.cs
//
using Perspex.Controls.Primitives;
using Perspex.Media;
using Perspex.VisualTree;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Linq;
namespace Perspex.Controls
{
public class LayoutTransformControl : ContentControl
{
public static readonly PerspexProperty<Transform> LayoutTransformProperty =
PerspexProperty.Register<LayoutTransformControl, Transform>(nameof(LayoutTransform));
static LayoutTransformControl()
{
LayoutTransformProperty.Changed
.AddClassHandler<LayoutTransformControl>(x => x.OnLayoutTransformChanged);
}
public Transform LayoutTransform
{
get { return GetValue(LayoutTransformProperty); }
set { SetValue(LayoutTransformProperty, value); }
}
public Control TransformRoot => _transformRoot ??
(_transformRoot = this.GetVisualChildren().OfType<Control>().FirstOrDefault());
/// <summary>
/// Provides the behavior for the "Arrange" pass of layout.
/// </summary>
/// <param name="finalSize">The final area within the parent that this element should use to arrange itself and its children.</param>
/// <returns>The actual size used.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
if (TransformRoot == null || LayoutTransform == null)
{
return base.ArrangeOverride(finalSize);
}
// Determine the largest available size after the transformation
Size finalSizeTransformed = ComputeLargestTransformedSize(finalSize);
if (IsSizeSmaller(finalSizeTransformed, TransformRoot.DesiredSize))
{
// Some elements do not like being given less space than they asked for (ex: TextBlock)
// Bump the working size up to do the right thing by them
finalSizeTransformed = TransformRoot.DesiredSize;
}
// Transform the working size to find its width/height
Rect transformedRect = new Rect(0, 0, finalSizeTransformed.Width, finalSizeTransformed.Height).TransformToAABB(_transformation);
// Create the Arrange rect to center the transformed content
Rect finalRect = new Rect(
-transformedRect.X + ((finalSize.Width - transformedRect.Width) / 2),
-transformedRect.Y + ((finalSize.Height - transformedRect.Height) / 2),
finalSizeTransformed.Width,
finalSizeTransformed.Height);
// Perform an Arrange on TransformRoot (containing Child)
Size arrangedsize;
TransformRoot.Arrange(finalRect);
arrangedsize = TransformRoot.Bounds.Size;
// This is the first opportunity under Silverlight to find out the Child's true DesiredSize
if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && (Size.Empty == _childActualSize))
{
//// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used
//// Make a note of the actual DesiredSize
//_childActualSize = arrangedsize;
//// Force a new measure/arrange pass
//InvalidateMeasure();
}
else
{
// Clear the "need to measure/arrange again" flag
_childActualSize = Size.Empty;
}
// Return result to perform the transformation
return finalSize;
}
/// <summary>
/// Provides the behavior for the "Measure" pass of layout.
/// </summary>
/// <param name="availableSize">The available size that this element can give to child elements.</param>
/// <returns>The size that this element determines it needs during layout, based on its calculations of child element sizes.</returns>
protected override Size MeasureOverride(Size availableSize)
{
if (TransformRoot == null || LayoutTransform == null)
{
return base.MeasureOverride(availableSize);
}
Size measureSize;
if (_childActualSize == Size.Empty)
{
// Determine the largest size after the transformation
measureSize = ComputeLargestTransformedSize(availableSize);
}
else
{
// Previous measure/arrange pass determined that Child.DesiredSize was larger than believed
measureSize = _childActualSize;
}
// Perform a measure on the TransformRoot (containing Child)
TransformRoot.Measure(measureSize);
var desiredSize = TransformRoot.DesiredSize;
// Transform DesiredSize to find its width/height
Rect transformedDesiredRect = new Rect(0, 0, desiredSize.Width, desiredSize.Height).TransformToAABB(_transformation);
Size transformedDesiredSize = new Size(transformedDesiredRect.Width, transformedDesiredRect.Height);
// Return result to allocate enough space for the transformation
return transformedDesiredSize;
}
/// <summary>
/// Builds the visual tree for the LayoutTransformerControl when a new
/// template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
_matrixTransform = new MatrixTransform();
if (null != TransformRoot)
{
TransformRoot.RenderTransform = _matrixTransform;
TransformRoot.TransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
}
ApplyLayoutTransform();
}
/// <summary>
/// Acceptable difference between two doubles.
/// </summary>
private const double AcceptableDelta = 0.0001;
/// <summary>
/// Number of decimals to round the Matrix to.
/// </summary>
private const int DecimalsAfterRound = 4;
/// <summary>
/// Actual DesiredSize of Child element (the value it returned from its MeasureOverride method).
/// </summary>
private Size _childActualSize = Size.Empty;
/// <summary>
/// RenderTransform/MatrixTransform applied to TransformRoot.
/// </summary>
private MatrixTransform _matrixTransform;
/// <summary>
/// Transformation matrix corresponding to _matrixTransform.
/// </summary>
private Matrix _transformation;
private IDisposable _transformChangedEvent = null;
private Control _transformRoot;
/// <summary>
/// Returns true if Size a is smaller than Size b in either dimension.
/// </summary>
/// <param name="a">Second Size.</param>
/// <param name="b">First Size.</param>
/// <returns>True if Size a is smaller than Size b in either dimension.</returns>
private static bool IsSizeSmaller(Size a, Size b)
{
return (a.Width + AcceptableDelta < b.Width) || (a.Height + AcceptableDelta < b.Height);
}
/// <summary>
/// Rounds the non-offset elements of a Matrix to avoid issues due to floating point imprecision.
/// </summary>
/// <param name="matrix">Matrix to round.</param>
/// <param name="decimals">Number of decimal places to round to.</param>
/// <returns>Rounded Matrix.</returns>
private static Matrix RoundMatrix(Matrix matrix, int decimals)
{
return new Matrix(
Math.Round(matrix.M11, decimals),
Math.Round(matrix.M12, decimals),
Math.Round(matrix.M21, decimals),
Math.Round(matrix.M22, decimals),
matrix.M31,
matrix.M32);
}
/// <summary>
/// Applies the layout transform on the LayoutTransformerControl content.
/// </summary>
/// <remarks>
/// Only used in advanced scenarios (like animating the LayoutTransform).
/// Should be used to notify the LayoutTransformer control that some aspect
/// of its Transform property has changed.
/// </remarks>
private void ApplyLayoutTransform()
{
if (LayoutTransform == null) return;
// Get the transform matrix and apply it
_transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound);
if (null != _matrixTransform)
{
_matrixTransform.Matrix = _transformation;
}
// New transform means re-layout is necessary
InvalidateMeasure();
}
/// <summary>
/// Compute the largest usable size (greatest area) after applying the transformation to the specified bounds.
/// </summary>
/// <param name="arrangeBounds">Arrange bounds.</param>
/// <returns>Largest Size possible.</returns>
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Closely corresponds to WPF's FrameworkElement.FindMaximalAreaLocalSpaceRect.")]
private Size ComputeLargestTransformedSize(Size arrangeBounds)
{
// Computed largest transformed size
Size computedSize = Size.Empty;
// Detect infinite bounds and constrain the scenario
bool infiniteWidth = double.IsInfinity(arrangeBounds.Width);
if (infiniteWidth)
{
// arrangeBounds.Width = arrangeBounds.Height;
arrangeBounds = arrangeBounds.WithWidth(arrangeBounds.Height);
}
bool infiniteHeight = double.IsInfinity(arrangeBounds.Height);
if (infiniteHeight)
{
//arrangeBounds.Height = arrangeBounds.Width;
arrangeBounds = arrangeBounds.WithHeight(arrangeBounds.Width);
}
// Capture the matrix parameters
double a = _transformation.M11;
double b = _transformation.M12;
double c = _transformation.M21;
double d = _transformation.M22;
// Compute maximum possible transformed width/height based on starting width/height
// These constraints define two lines in the positive x/y quadrant
double maxWidthFromWidth = Math.Abs(arrangeBounds.Width / a);
double maxHeightFromWidth = Math.Abs(arrangeBounds.Width / c);
double maxWidthFromHeight = Math.Abs(arrangeBounds.Height / b);
double maxHeightFromHeight = Math.Abs(arrangeBounds.Height / d);
// The transformed width/height that maximize the area under each segment is its midpoint
// At most one of the two midpoints will satisfy both constraints
double idealWidthFromWidth = maxWidthFromWidth / 2;
double idealHeightFromWidth = maxHeightFromWidth / 2;
double idealWidthFromHeight = maxWidthFromHeight / 2;
double idealHeightFromHeight = maxHeightFromHeight / 2;
// Compute slope of both constraint lines
double slopeFromWidth = -(maxHeightFromWidth / maxWidthFromWidth);
double slopeFromHeight = -(maxHeightFromHeight / maxWidthFromHeight);
if ((0 == arrangeBounds.Width) || (0 == arrangeBounds.Height))
{
// Check for empty bounds
computedSize = new Size(arrangeBounds.Width, arrangeBounds.Height);
}
else if (infiniteWidth && infiniteHeight)
{
// Check for completely unbound scenario
computedSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
}
else if (!_transformation.HasInverse)
{
// Check for singular matrix
computedSize = new Size(0, 0);
}
else if ((0 == b) || (0 == c))
{
// Check for 0/180 degree special cases
double maxHeight = (infiniteHeight ? double.PositiveInfinity : maxHeightFromHeight);
double maxWidth = (infiniteWidth ? double.PositiveInfinity : maxWidthFromWidth);
if ((0 == b) && (0 == c))
{
// No constraints
computedSize = new Size(maxWidth, maxHeight);
}
else if (0 == b)
{
// Constrained by width
double computedHeight = Math.Min(idealHeightFromWidth, maxHeight);
computedSize = new Size(
maxWidth - Math.Abs((c * computedHeight) / a),
computedHeight);
}
else if (0 == c)
{
// Constrained by height
double computedWidth = Math.Min(idealWidthFromHeight, maxWidth);
computedSize = new Size(
computedWidth,
maxHeight - Math.Abs((b * computedWidth) / d));
}
}
else if ((0 == a) || (0 == d))
{
// Check for 90/270 degree special cases
double maxWidth = (infiniteHeight ? double.PositiveInfinity : maxWidthFromHeight);
double maxHeight = (infiniteWidth ? double.PositiveInfinity : maxHeightFromWidth);
if ((0 == a) && (0 == d))
{
// No constraints
computedSize = new Size(maxWidth, maxHeight);
}
else if (0 == a)
{
// Constrained by width
double computedHeight = Math.Min(idealHeightFromHeight, maxHeight);
computedSize = new Size(
maxWidth - Math.Abs((d * computedHeight) / b),
computedHeight);
}
else if (0 == d)
{
// Constrained by height
double computedWidth = Math.Min(idealWidthFromWidth, maxWidth);
computedSize = new Size(
computedWidth,
maxHeight - Math.Abs((a * computedWidth) / c));
}
}
else if (idealHeightFromWidth <= ((slopeFromHeight * idealWidthFromWidth) + maxHeightFromHeight))
{
// Check the width midpoint for viability (by being below the height constraint line)
computedSize = new Size(idealWidthFromWidth, idealHeightFromWidth);
}
else if (idealHeightFromHeight <= ((slopeFromWidth * idealWidthFromHeight) + maxHeightFromWidth))
{
// Check the height midpoint for viability (by being below the width constraint line)
computedSize = new Size(idealWidthFromHeight, idealHeightFromHeight);
}
else
{
// Neither midpoint is viable; use the intersection of the two constraint lines instead
// Compute width by setting heights equal (m1*x+c1=m2*x+c2)
double computedWidth = (maxHeightFromHeight - maxHeightFromWidth) / (slopeFromWidth - slopeFromHeight);
// Compute height from width constraint line (y=m*x+c; using height would give same result)
computedSize = new Size(
computedWidth,
(slopeFromWidth * computedWidth) + maxHeightFromWidth);
}
// Return result
return computedSize;
}
private void OnLayoutTransformChanged(PerspexPropertyChangedEventArgs e)
{
var newTransform = e.NewValue as Transform;
if (_transformChangedEvent != null)
{
_transformChangedEvent.Dispose();
_transformChangedEvent = null;
}
if (newTransform != null)
{
_transformChangedEvent = Observable.FromEventPattern<EventHandler, EventArgs>(
v => newTransform.Changed += v, v => newTransform.Changed -= v)
.Subscribe(onNext: v => ApplyLayoutTransform());
}
ApplyLayoutTransform();
}
}
}

1
src/Perspex.Controls/Perspex.Controls.csproj

@ -54,6 +54,7 @@
<Compile Include="INameScope.cs" /> <Compile Include="INameScope.cs" />
<Compile Include="IPseudoClasses.cs" /> <Compile Include="IPseudoClasses.cs" />
<Compile Include="DropDownItem.cs" /> <Compile Include="DropDownItem.cs" />
<Compile Include="LayoutTransformControl.cs" />
<Compile Include="Mixins\ContentControlMixin.cs" /> <Compile Include="Mixins\ContentControlMixin.cs" />
<Compile Include="NameScope.cs" /> <Compile Include="NameScope.cs" />
<Compile Include="NameScopeEventArgs.cs" /> <Compile Include="NameScopeEventArgs.cs" />

6
src/Perspex.SceneGraph/Matrix.cs

@ -53,6 +53,11 @@ namespace Perspex
/// </summary> /// </summary>
public bool IsIdentity => Equals(Identity); public bool IsIdentity => Equals(Identity);
/// <summary>
/// HasInverse Property - returns true if this matrix is invertable, false otherwise.
/// </summary>
public bool HasInverse => GetDeterminant() != 0;
/// <summary> /// <summary>
/// The first element of the first row /// The first element of the first row
/// </summary> /// </summary>
@ -209,6 +214,7 @@ namespace Perspex
return (_m11 * _m22) - (_m12 * _m21); return (_m11 * _m22) - (_m12 * _m21);
} }
/// <summary> /// <summary>
/// Returns a boolean indicating whether the matrix is equal to the other given matrix. /// Returns a boolean indicating whether the matrix is equal to the other given matrix.
/// </summary> /// </summary>

69
src/Perspex.SceneGraph/Media/ScaleTransform.cs

@ -0,0 +1,69 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Perspex.Media
{
/// <summary>
/// Scale an <see cref="IVisual"/>.
/// </summary>
public class ScaleTransform : Transform
{
/// <summary>
/// Defines the <see cref="ScaleX"/> property.
/// </summary>
public static readonly StyledProperty<double> ScaleXProperty =
PerspexProperty.Register<ScaleTransform, double>(nameof(ScaleX), 1);
/// <summary>
/// Defines the <see cref="ScaleY"/> property.
/// </summary>
public static readonly StyledProperty<double> ScaleYProperty =
PerspexProperty.Register<ScaleTransform, double>(nameof(ScaleY), 1);
/// <summary>
/// Initializes a new instance of the <see cref="ScaleTransform"/> class.
/// </summary>
public ScaleTransform()
{
this.GetObservable(ScaleXProperty).Subscribe(_ => RaiseChanged());
this.GetObservable(ScaleYProperty).Subscribe(_ => RaiseChanged());
}
/// <summary>
/// Initializes a new instance of the <see cref="ScaleTransform"/> class.
/// </summary>
/// <param name="scaleX">ScaleX</param>
/// <param name="scaleY">ScaleY</param>
public ScaleTransform(double scaleX, double scaleY)
: this()
{
ScaleX = scaleX;
ScaleY = scaleY;
}
/// <summary>
/// Gets or sets the ScaleX property.
/// </summary>
public double ScaleX
{
get { return GetValue(ScaleXProperty); }
set { SetValue(ScaleXProperty, value); }
}
/// <summary>
/// Gets or sets the ScaleY property.
/// </summary>
public double ScaleY
{
get { return GetValue(ScaleYProperty); }
set { SetValue(ScaleYProperty, value); }
}
/// <summary>
/// Gets the tranform's <see cref="Matrix"/>.
/// </summary>
public override Matrix Value => Matrix.CreateScale(ScaleX, ScaleY);
}
}

1
src/Perspex.SceneGraph/Perspex.SceneGraph.csproj

@ -80,6 +80,7 @@
<Compile Include="Media\LinearGradientBrush.cs" /> <Compile Include="Media\LinearGradientBrush.cs" />
<Compile Include="Media\MediaExtensions.cs" /> <Compile Include="Media\MediaExtensions.cs" />
<Compile Include="Media\PenLineCap.cs" /> <Compile Include="Media\PenLineCap.cs" />
<Compile Include="Media\ScaleTransform.cs" />
<Compile Include="Media\TextAlignment.cs" /> <Compile Include="Media\TextAlignment.cs" />
<Compile Include="Media\FontWeight.cs" /> <Compile Include="Media\FontWeight.cs" />
<Compile Include="Media\FontStyle.cs" /> <Compile Include="Media\FontStyle.cs" />

1
src/Perspex.Themes.Default/DefaultTheme.xaml

@ -8,6 +8,7 @@
<StyleInclude Source="resm:Perspex.Themes.Default.DropDownItem.xaml?assembly=Perspex.Themes.Default"/> <StyleInclude Source="resm:Perspex.Themes.Default.DropDownItem.xaml?assembly=Perspex.Themes.Default"/>
<StyleInclude Source="resm:Perspex.Themes.Default.GridSplitter.xaml?assembly=Perspex.Themes.Default"/> <StyleInclude Source="resm:Perspex.Themes.Default.GridSplitter.xaml?assembly=Perspex.Themes.Default"/>
<StyleInclude Source="resm:Perspex.Themes.Default.ItemsControl.xaml?assembly=Perspex.Themes.Default"/> <StyleInclude Source="resm:Perspex.Themes.Default.ItemsControl.xaml?assembly=Perspex.Themes.Default"/>
<StyleInclude Source="resm:Perspex.Themes.Default.LayoutTransformControl.xaml?assembly=Perspex.Themes.Default"/>
<StyleInclude Source="resm:Perspex.Themes.Default.ListBox.xaml?assembly=Perspex.Themes.Default"/> <StyleInclude Source="resm:Perspex.Themes.Default.ListBox.xaml?assembly=Perspex.Themes.Default"/>
<StyleInclude Source="resm:Perspex.Themes.Default.ListBoxItem.xaml?assembly=Perspex.Themes.Default"/> <StyleInclude Source="resm:Perspex.Themes.Default.ListBoxItem.xaml?assembly=Perspex.Themes.Default"/>
<StyleInclude Source="resm:Perspex.Themes.Default.Menu.xaml?assembly=Perspex.Themes.Default"/> <StyleInclude Source="resm:Perspex.Themes.Default.Menu.xaml?assembly=Perspex.Themes.Default"/>

12
src/Perspex.Themes.Default/LayoutTransformControl.xaml

@ -0,0 +1,12 @@
<Style xmlns="https://github.com/perspex" Selector="LayoutTransformControl">
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</ControlTemplate>
</Setter>
</Style>

5
src/Perspex.Themes.Default/Perspex.Themes.Default.csproj

@ -200,6 +200,11 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="LayoutTransformControl.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

245
tests/Perspex.Controls.UnitTests/LayoutTransformControlTests.cs

@ -0,0 +1,245 @@
using Perspex.Controls.Presenters;
using Perspex.Controls.Shapes;
using Perspex.Controls.Templates;
using Perspex.Media;
using Xunit;
namespace Perspex.Controls.UnitTests
{
public class LayoutTransformControlTests
{
[Fact]
public void Measure_On_Scale_x2_Is_Correct()
{
double scale = 2;
TransformMeasureSizeTest(
new Size(100, 50),
new ScaleTransform() { ScaleX = scale, ScaleY = scale },
new Size(200, 100));
}
[Fact]
public void Measure_On_Scale_x0_5_Is_Correct()
{
double scale = 0.5;
TransformMeasureSizeTest(
new Size(100, 50),
new ScaleTransform() { ScaleX = scale, ScaleY = scale },
new Size(50, 25));
}
[Fact]
public void Measure_On_Rotate_90_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 25),
new RotateTransform() { Angle = 90 },
new Size(25, 100));
}
[Fact]
public void Measure_On_Rotate_minus_90_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 25),
new RotateTransform() { Angle = -90 },
new Size(25, 100));
}
[Fact]
public void Measure_On_Rotate_0_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 25),
new RotateTransform() { Angle = 0 },
new Size(100, 25));
}
[Fact]
public void Measure_On_Rotate_180_degrees_Is_Correct()
{
TransformMeasureSizeTest(
new Size(100, 25),
new RotateTransform() { Angle = 180 },
new Size(100, 25));
}
[Fact]
public void Bounds_On_Scale_x2_Are_correct()
{
double scale = 2;
TransformRootBoundsTest(
new Size(100, 50),
new ScaleTransform() { ScaleX = scale, ScaleY = scale },
new Rect(0, 0, 100, 50));
}
[Fact]
public void Bounds_On_Scale_x0_5_Are_correct()
{
double scale = 0.5;
TransformRootBoundsTest(
new Size(100, 50),
new ScaleTransform() { ScaleX = scale, ScaleY = scale },
new Rect(0, 0, 100, 50));
}
[Fact]
public void Bounds_On_Rotate_180_degrees_Are_correct()
{
TransformRootBoundsTest(
new Size(100, 25),
new RotateTransform() { Angle = 180 },
new Rect(100, 25, 100, 25));
}
[Fact]
public void Bounds_On_Rotate_0_degrees_Are_correct()
{
TransformRootBoundsTest(
new Size(100, 25),
new RotateTransform() { Angle = 0 },
new Rect(0, 0, 100, 25));
}
[Fact]
public void Bounds_On_Rotate_90_degrees_Are_correct()
{
TransformRootBoundsTest(
new Size(100, 25),
new RotateTransform() { Angle = 90 },
new Rect(25, 0, 100, 25));
}
[Fact]
public void Bounds_On_Rotate_minus_90_degrees_Are_correct()
{
TransformRootBoundsTest(
new Size(100, 25),
new RotateTransform() { Angle = -90 },
new Rect(0, 100, 100, 25));
}
[Fact]
public void Should_Generate_RenderTransform_90_degrees()
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
25,
new RotateTransform() { Angle = 90 });
Assert.NotNull(lt.TransformRoot.RenderTransform);
Matrix m = lt.TransformRoot.RenderTransform.Value;
Matrix res = Matrix.CreateRotation(Matrix.ToRadians(90));
Assert.Equal(m.M11, res.M11, 3);
Assert.Equal(m.M12, res.M12, 3);
Assert.Equal(m.M21, res.M21, 3);
Assert.Equal(m.M22, res.M22, 3);
Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 3);
}
[Fact]
public void Should_Generate_RenderTransform_minus_90_degrees()
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
25,
new RotateTransform() { Angle = -90 });
Assert.NotNull(lt.TransformRoot.RenderTransform);
var m = lt.TransformRoot.RenderTransform.Value;
var res = Matrix.CreateRotation(Matrix.ToRadians(-90));
Assert.Equal(m.M11, res.M11, 3);
Assert.Equal(m.M12, res.M12, 3);
Assert.Equal(m.M21, res.M21, 3);
Assert.Equal(m.M22, res.M22, 3);
Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 3);
}
[Fact]
public void Should_Generate_ScaleTransform_x2()
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
100,
50,
new ScaleTransform() { ScaleX = 2, ScaleY = 2 });
Assert.NotNull(lt.TransformRoot.RenderTransform);
Matrix m = lt.TransformRoot.RenderTransform.Value;
Matrix res = Matrix.CreateScale(2, 2);
Assert.Equal(m.M11, res.M11, 3);
Assert.Equal(m.M12, res.M12, 3);
Assert.Equal(m.M21, res.M21, 3);
Assert.Equal(m.M22, res.M22, 3);
Assert.Equal(m.M31, res.M31, 3);
Assert.Equal(m.M32, res.M32, 3);
}
private static void TransformMeasureSizeTest(Size size, Transform transform, Size expectedSize)
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(
size.Width,
size.Height,
transform);
Size outSize = lt.DesiredSize;
Assert.Equal(outSize.Width, expectedSize.Width);
Assert.Equal(outSize.Height, expectedSize.Height);
}
private static void TransformRootBoundsTest(Size size, Transform transform, Rect expectedBounds)
{
LayoutTransformControl lt = CreateWithChildAndMeasureAndTransform(size.Width, size.Height, transform);
Rect outBounds = lt.TransformRoot.Bounds;
Assert.Equal(outBounds.X, expectedBounds.X);
Assert.Equal(outBounds.Y, expectedBounds.Y);
Assert.Equal(outBounds.Width, expectedBounds.Width);
Assert.Equal(outBounds.Height, expectedBounds.Height);
}
private static LayoutTransformControl CreateWithChildAndMeasureAndTransform(
double width,
double height,
Transform transform)
{
var lt = new LayoutTransformControl()
{
LayoutTransform = transform,
Template = new FuncControlTemplate<LayoutTransformControl>(
p => new ContentPresenter() { Content = p.Content })
};
lt.Content = new Rectangle() { Width = width, Height = height };
lt.ApplyTemplate();
//we need to force create visual child
//so the measure after is correct
(lt.Presenter as ContentPresenter).UpdateChild();
Assert.NotNull(lt.Presenter?.Child);
lt.Measure(Size.Infinity);
lt.Arrange(new Rect(lt.DesiredSize));
return lt;
}
}
}

1
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -90,6 +90,7 @@
</Choose> </Choose>
<ItemGroup> <ItemGroup>
<Compile Include="ClassesTests.cs" /> <Compile Include="ClassesTests.cs" />
<Compile Include="LayoutTransformControlTests.cs" />
<Compile Include="UserControlTests.cs" /> <Compile Include="UserControlTests.cs" />
<Compile Include="DockPanelTests.cs" /> <Compile Include="DockPanelTests.cs" />
<Compile Include="EnumerableExtensions.cs" /> <Compile Include="EnumerableExtensions.cs" />

Loading…
Cancel
Save