2 changed files with 408 additions and 0 deletions
@ -0,0 +1,407 @@ |
|||
// 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.Presenters; |
|||
using Perspex.Controls.Primitives; |
|||
using Perspex.Controls.Templates; |
|||
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); |
|||
TemplateProperty.OverrideDefaultValue<LayoutTransformControl>(_defaultTemplate); |
|||
} |
|||
|
|||
public Transform LayoutTransform |
|||
{ |
|||
get { return GetValue(LayoutTransformProperty); } |
|||
set { SetValue(LayoutTransformProperty, value); } |
|||
} |
|||
|
|||
/// <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; |
|||
|
|||
private static readonly FuncControlTemplate<LayoutTransformControl> _defaultTemplate = |
|||
new FuncControlTemplate<LayoutTransformControl>(control => |
|||
{ |
|||
return new Decorator() |
|||
{ |
|||
Child = new ContentPresenter() |
|||
{ |
|||
[~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty] |
|||
} |
|||
}; |
|||
}); |
|||
|
|||
/// <summary>
|
|||
/// RenderTransform/MatrixTransform applied to TransformRoot.
|
|||
/// </summary>
|
|||
private MatrixTransform _matrixTransform; |
|||
|
|||
/// <summary>
|
|||
/// Transformation matrix corresponding to _matrixTransform.
|
|||
/// </summary>
|
|||
private Matrix _transformation; |
|||
|
|||
/// <summary>
|
|||
/// Actual DesiredSize of Child element (the value it returned from its MeasureOverride method).
|
|||
/// </summary>
|
|||
private Size _childActualSize = Size.Empty; |
|||
|
|||
private Control _transformRoot; |
|||
|
|||
public Control TransformRoot => _transformRoot ?? |
|||
(_transformRoot = this.GetVisualChildren().OfType<Control>().FirstOrDefault()); |
|||
|
|||
private IDisposable _transformChangedEvent = null; |
|||
|
|||
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(); |
|||
} |
|||
|
|||
/// <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>
|
|||
/// 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>
|
|||
public 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>
|
|||
/// 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>
|
|||
/// 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>
|
|||
/// 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; |
|||
} |
|||
|
|||
/// <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); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue