This repository was archived by the owner on Feb 25, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathLayoutTransformControl.cs
More file actions
457 lines (393 loc) · 18.2 KB
/
Copy pathLayoutTransformControl.cs
File metadata and controls
457 lines (393 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
// 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;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
namespace CommunityToolkit.WinUI.UI.Controls
{
/// <summary>
/// Control that implements support for transformations as if applied by LayoutTransform.
/// </summary>
[ContentProperty(Name = "Child")]
public partial class LayoutTransformControl : Control
{
/// <summary>
/// Value used to work around double arithmetic rounding issues.
/// </summary>
private const double AcceptableDelta = 0.0001;
/// <summary>
/// Value used to work around double arithmetic rounding issues.
/// </summary>
private const int DecimalsAfterRound = 4;
/// <summary>
/// List of property change event sources for events when properties of the Transform tree change
/// </summary>
private readonly Dictionary<Transform, List<PropertyChangeEventSource<double>>>
_transformPropertyChangeEventSources = new Dictionary
<Transform, List<PropertyChangeEventSource<double>>>();
/// <summary>
/// Host panel for Child element.
/// </summary>
private Panel _layoutRoot;
/// <summary>
/// RenderTransform/MatrixTransform applied to layout root.
/// </summary>
private MatrixTransform _matrixTransform;
/// <summary>
/// Transformation matrix corresponding to matrix transform.
/// </summary>
private Matrix _transformation;
/// <summary>
/// Actual DesiredSize of Child element.
/// </summary>
private Size _childActualSize = Size.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="LayoutTransformControl"/> class.
/// </summary>
public LayoutTransformControl()
{
DefaultStyleKey = typeof(LayoutTransformControl);
// Can't tab to LayoutTransformControl
IsTabStop = false;
// Disable layout rounding because its rounding of values confuses things.
UseLayoutRounding = false;
}
/// <summary>
/// Called whenever the control's template changes.
/// </summary>
protected override void OnApplyTemplate()
{
// Save existing content and remove it from the visual tree
FrameworkElement savedContent = Child;
Child = null;
// Apply new template
base.OnApplyTemplate();
// Find template parts
_layoutRoot = GetTemplateChild("LayoutRoot") as Panel;
_matrixTransform = GetTemplateChild("MatrixTransform") as MatrixTransform;
// RestoreAsync saved content
Child = savedContent;
// Apply the current transform
TransformUpdated();
}
/// <summary>
/// Notifies the LayoutTransformControl that some aspect of its Transform property has changed.
/// </summary>
/// <remarks>
/// Call this to update the LayoutTransform in cases where
/// LayoutTransformControl wouldn't otherwise know to do so.
/// </remarks>
public void TransformUpdated()
{
ProcessTransform();
}
/// <summary>
/// Return true if Size a is smaller than Size b in either dimension.
/// </summary>
/// <param name="a">The left size.</param>
/// <param name="b">The right size.</param>
/// <returns>A value indicating whether the left size is smaller than
/// the right.</returns>
private static bool IsSizeSmaller(Size a, Size b)
{
// WPF equivalent of following code:
// return ((a.Width < b.Width) || (a.Height < b.Height));
return (a.Width + AcceptableDelta < b.Width) || (a.Height + AcceptableDelta < b.Height);
}
/// <summary>
/// Processes the current transform to determine the corresponding
/// matrix.
/// </summary>
private void ProcessTransform()
{
// Get the transform matrix and apply it
_transformation = GetTransformMatrix(Transform).Round(DecimalsAfterRound);
if (_matrixTransform != null)
{
_matrixTransform.Matrix = _transformation;
}
// New transform means re-layout is necessary
InvalidateMeasure();
}
/// <summary>
/// Walks the Transform and returns the corresponding matrix.
/// </summary>
/// <param name="transform">The transform to create a matrix for.
/// </param>
/// <returns>The matrix calculated from the transform.</returns>
private Matrix GetTransformMatrix(Transform transform)
{
if (transform != null)
{
// WPF equivalent of this entire method (why oh why only WPF...):
// return transform.Value;
// Process the TransformGroup
var transformGroup = transform as TransformGroup;
if (transformGroup != null)
{
var groupMatrix = Matrix.Identity;
foreach (var child in transformGroup.Children)
{
groupMatrix = groupMatrix.Multiply(GetTransformMatrix(child));
}
return groupMatrix;
}
// Process the RotateTransform
var rotateTransform = transform as RotateTransform;
if (rotateTransform != null)
{
return rotateTransform.GetMatrix();
}
// Process the ScaleTransform
var scaleTransform = transform as ScaleTransform;
if (scaleTransform != null)
{
return scaleTransform.GetMatrix();
}
// Process the SkewTransform
var skewTransform = transform as SkewTransform;
if (skewTransform != null)
{
return skewTransform.GetMatrix();
}
// Process the MatrixTransform
var matrixTransform = transform as MatrixTransform;
if (matrixTransform != null)
{
return matrixTransform.Matrix;
}
if (transform is CompositeTransform)
{
throw new NotSupportedException("CompositeTransforms are not supported (yet) by the LayoutTransformControl.");
}
// TranslateTransform has no effect in LayoutTransform
}
// Fall back to no-op transformation
return Matrix.Identity;
}
/// <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. Infinity can be specified as a value to
/// indicate that the element will size to whatever content is available.</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)
{
FrameworkElement child = Child;
if (_layoutRoot == null || child == null)
{
// No content, no size
return Size.Empty;
}
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 _layoutRoot (containing Child)
_layoutRoot.Measure(measureSize);
// Transform DesiredSize to find its width/height
Rect startingRect = new Rect(0, 0, _layoutRoot.DesiredSize.Width, _layoutRoot.DesiredSize.Height);
Rect transformedDesiredRect = startingRect.Transform(_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)
{
FrameworkElement child = Child;
if (_layoutRoot == null || child == null)
{
// No child, use whatever was given
return finalSize;
}
// Determine the largest available size after the transformation
Size finalSizeTransformed = ComputeLargestTransformedSize(finalSize);
if (IsSizeSmaller(finalSizeTransformed, _layoutRoot.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 = _layoutRoot.DesiredSize;
}
// Transform the working size to find its width/height
Rect startingRect = new Rect(0, 0, finalSizeTransformed.Width, finalSizeTransformed.Height);
Rect transformedRect = startingRect.Transform(_transformation);
// Create the Arrange rect to center the transformed content
Rect finalRect = new Rect(
-transformedRect.Left + ((finalSize.Width - transformedRect.Width) / 2),
-transformedRect.Top + ((finalSize.Height - transformedRect.Height) / 2),
finalSizeTransformed.Width,
finalSizeTransformed.Height);
// Perform an Arrange on _layoutRoot (containing Child)
_layoutRoot.Arrange(finalRect);
// This is the first opportunity to find out the Child's true DesiredSize
if (IsSizeSmaller(finalSizeTransformed, child.RenderSize) && (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 = new Size(child.ActualWidth, child.ActualHeight);
// 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>
/// Computes the largest usable size after applying the transformation to the specified bounds.
/// </summary>
/// <param name="arrangeBounds">The size to arrange within.</param>
/// <returns>The size required.</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;
}
bool infiniteHeight = double.IsInfinity(arrangeBounds.Height);
if (infiniteHeight)
{
arrangeBounds.Height = 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 (arrangeBounds.Width == 0 || arrangeBounds.Height == 0)
{
// Check for empty bounds
computedSize = new Size(0, 0);
}
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 (b == 0 || c == 0)
{
// Check for 0/180 degree special cases
double maxHeight = infiniteHeight ? double.PositiveInfinity : maxHeightFromHeight;
double maxWidth = infiniteWidth ? double.PositiveInfinity : maxWidthFromWidth;
if (b == 0 && c == 0)
{
// No constraints
computedSize = new Size(maxWidth, maxHeight);
}
else if (b == 0)
{
// Constrained by width
double computedHeight = Math.Min(idealHeightFromWidth, maxHeight);
computedSize = new Size(
maxWidth - Math.Abs((c * computedHeight) / a),
computedHeight);
}
else if (c == 0)
{
// Constrained by height
double computedWidth = Math.Min(idealWidthFromHeight, maxWidth);
computedSize = new Size(
computedWidth,
maxHeight - Math.Abs((b * computedWidth) / d));
}
}
else if (a == 0 || d == 0)
{
// Check for 90/270 degree special cases
double maxWidth = infiniteHeight ? double.PositiveInfinity : maxWidthFromHeight;
double maxHeight = infiniteWidth ? double.PositiveInfinity : maxHeightFromWidth;
if (a == 0 && d == 0)
{
// No constraints
computedSize = new Size(maxWidth, maxHeight);
}
else if (a == 0)
{
// Constrained by width
double computedHeight = Math.Min(idealHeightFromHeight, maxHeight);
computedSize = new Size(
maxWidth - Math.Abs((d * computedHeight) / b),
computedHeight);
}
else if (d == 0)
{
// 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 computedSize;
}
}
}