From ed7ee5cf39e70f5911239dccc426e8eaea010f43 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Mar 2023 11:23:52 +0100 Subject: [PATCH] Add Visual.GetTransformedBounds extension method. Fixes #9569. --- .../VisualTree/VisualExtensions.cs | 63 ++++++++ .../VisualExtensions_GetTransformedBounds.cs | 153 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetTransformedBounds.cs diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index 9e38c6e7f2..cc372f7c39 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/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; + } + /// /// Gets the first visual in the visual tree whose bounds contain a point. /// diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetTransformedBounds.cs b/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetTransformedBounds.cs new file mode 100644 index 0000000000..f4a800607a --- /dev/null +++ b/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)); + } + } +}