Browse Source

Add Visual.GetTransformedBounds extension method.

Fixes #9569.
Steven Kirk 2 years ago
parent
commit
ed7ee5cf39

+ 63 - 0
src/Avalonia.Base/VisualTree/VisualExtensions.cs

@@ -204,6 +204,69 @@ namespace Avalonia.VisualTree
             }
         }
 
+        public static TransformedBounds? GetTransformedBounds(this Visual visual)
+        {
+            Rect clip = default;
+            var transform = Matrix.Identity;
+
+            bool Visit(Visual visual)
+            {
+                if (!visual.IsVisible)
+                    return false;
+
+                // The visual's bounds in local coordinates.
+                var bounds = new Rect(visual.Bounds.Size);
+
+                // If the visual has no parent, we've reached the root. We start the clip
+                // rectangle with these bounds.
+                if (visual.GetVisualParent() is not { } parent)
+                {
+                    clip = bounds;
+                    return true;
+                }
+
+                // Otherwise recurse until the root visual is found, exiting early if one of the
+                // ancestors is invisible.
+                if (!Visit(parent))
+                    return false;
+
+                // Calculate the transform for this control from its offset and render transform.
+                var renderTransform = Matrix.Identity;
+
+                if (visual.HasMirrorTransform)
+                {
+                    var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+                    renderTransform *= mirrorMatrix;
+                }
+
+                if (visual.RenderTransform != null)
+                {
+                    var origin = visual.RenderTransformOrigin.ToPixels(bounds.Size);
+                    var offset = Matrix.CreateTranslation(origin);
+                    var finalTransform = (-offset) * visual.RenderTransform.Value * offset;
+                    renderTransform *= finalTransform;
+                }
+
+                transform = renderTransform *
+                    Matrix.CreateTranslation(visual.Bounds.Position) *
+                    transform;
+
+                // If the visual is clipped, update the clip bounds.
+                if (visual.ClipToBounds)
+                {
+                    var globalBounds = bounds.TransformToAABB(transform);
+                    var clipBounds = visual.ClipToBounds ?
+                        globalBounds.Intersect(clip) :
+                        clip;
+                    clip = clip.Intersect(clipBounds);
+                }
+
+                return true;
+            }
+
+            return Visit(visual) ? new(new(visual.Bounds.Size), clip, transform) : null;
+        }
+
         /// <summary>
         /// Gets the first visual in the visual tree whose bounds contain a point.
         /// </summary>

+ 153 - 0
tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetTransformedBounds.cs

@@ -0,0 +1,153 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.VisualTree;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.VisualTree
+{
+    public class VisualExtensions_GetTransformedBounds
+    {
+        [Fact]
+        public void Root()
+        {
+            var root = new Border
+            {
+                Width = 100,
+                Height = 123,
+            };
+
+            Layout(root);
+
+            Assert.Equal(
+                new TransformedBounds(
+                    new Rect(0, 0, 100, 123),
+                    new Rect(0, 0, 100, 123),
+                    Matrix.Identity),
+                root.GetTransformedBounds());
+        }
+
+        [Fact]
+        public void Depth_1_No_Transform_Or_Clip()
+        {
+            Border target;
+            var root = new Border
+            {
+                Width = 1000,
+                Height = 1000,
+                Child = target = new Border
+                {
+                    Width = 500,
+                    Height = 500,
+                }
+            };
+
+            Layout(root);
+
+            Assert.Equal(
+                new TransformedBounds(
+                    new Rect(250, 250, 500, 500),
+                    new Rect(0, 0, 1000, 1000),
+                    Matrix.CreateTranslation(250, 250)),
+                target.GetTransformedBounds());
+        }
+
+        [Fact]
+        public void Depth_2_No_Transform_Or_Clip()
+        {
+            Border target;
+            var root = new Border
+            {
+                Width = 1000,
+                Height = 1000,
+                Child = new Border
+                {
+                    Width = 800,
+                    Height = 800,
+                    Child = target = new Border
+                    {
+                        Width = 500,
+                        Height = 500,
+                    }
+                }
+            };
+
+            Layout(root);
+
+            Assert.Equal(
+                new TransformedBounds(
+                    new Rect(150, 150, 500, 500),
+                    new Rect(0, 0, 1000, 1000),
+                    Matrix.CreateTranslation(250, 250)),
+                target.GetTransformedBounds());
+        }
+
+        [Fact]
+        public void Depth_2_No_Transform_With_Clip()
+        {
+            Border target;
+            var root = new Border
+            {
+                Width = 1000,
+                Height = 1000,
+                Child = new Border
+                {
+                    Width = 800,
+                    Height = 800,
+                    ClipToBounds = true,
+                    Child = target = new Border
+                    {
+                        Width = 500,
+                        Height = 500,
+                    }
+                }
+            };
+
+            Layout(root);
+
+            Assert.Equal(
+                new TransformedBounds(
+                    new Rect(150, 150, 500, 500),
+                    new Rect(100, 100, 800, 800),
+                    Matrix.CreateTranslation(250, 250)),
+                target.GetTransformedBounds());
+        }
+
+        [Fact]
+        public void Depth_2_Transformed_Clip()
+        {
+            Border target;
+            var root = new Border
+            {
+                Width = 1000,
+                Height = 1000,
+                Child = new Border
+                {
+                    Width = 800,
+                    Height = 800,
+                    ClipToBounds = true,
+                    RenderTransform = new MatrixTransform(Matrix.CreateTranslation(10, 20)),
+                    Child = target = new Border
+                    {
+                        Width = 500,
+                        Height = 500,
+                    }
+                }
+            };
+
+            Layout(root);
+
+            Assert.Equal(
+                new TransformedBounds(
+                    new Rect(150, 150, 500, 500),
+                    new Rect(110, 120, 800, 800),
+                    Matrix.CreateTranslation(260, 270)),
+                target.GetTransformedBounds());
+        }
+
+        private void Layout(Control c)
+        {
+            c.Measure(Size.Infinity);
+            c.Arrange(new Rect(c.DesiredSize));
+        }
+    }
+}