diff --git a/src/Avalonia.Base/Controls/AdornerLayerBase.cs b/src/Avalonia.Base/Controls/AdornerLayerBase.cs new file mode 100644 index 00000000000..b1e0fca75da --- /dev/null +++ b/src/Avalonia.Base/Controls/AdornerLayerBase.cs @@ -0,0 +1,23 @@ +using Avalonia.Metadata; + +namespace Avalonia.Controls; + +[PrivateApi] +public class AdornerLayerBase +{ + /// + /// Allows for getting and setting of the adorned element. + /// + public static readonly AttachedProperty AdornedElementProperty = + AvaloniaProperty.RegisterAttached("AdornedElement"); + + public static Visual? GetAdornedElement(Visual adorner) + { + return adorner.GetValue(AdornedElementProperty); + } + + public static void SetAdornedElement(Visual adorner, Visual? adorned) + { + adorner.SetValue(AdornedElementProperty, adorned); + } +} diff --git a/src/Avalonia.Base/Controls/IAdornerLayer.cs b/src/Avalonia.Base/Controls/IAdornerLayer.cs new file mode 100644 index 00000000000..d5bd462b26b --- /dev/null +++ b/src/Avalonia.Base/Controls/IAdornerLayer.cs @@ -0,0 +1,9 @@ +using Avalonia.Metadata; + +namespace Avalonia.Controls; + +[PrivateApi] +[NotClientImplementable] +public interface IAdornerLayer +{ +} diff --git a/src/Avalonia.Base/VisualExtensions.cs b/src/Avalonia.Base/VisualExtensions.cs index e8dc5465d69..f2a3d5e2276 100644 --- a/src/Avalonia.Base/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.VisualTree; namespace Avalonia @@ -52,8 +53,21 @@ public static PixelPoint PointToScreen(this Visual visual, Point point) if (common != null) { - var thisOffset = GetOffsetFrom(common, from); - var thatOffset = GetOffsetFrom(common, to); + Matrix thisOffset; + if (TryGetAdorner(from, common, out var adornedFrom, out var adornerLayerFrom)) + { + thisOffset = GetOffsetFrom(common, adornedFrom!) * GetOffsetFrom(adornerLayerFrom!, from); + } + else + thisOffset = GetOffsetFrom(common, from); + + Matrix thatOffset; + if (TryGetAdorner(to, common, out var adornedTo, out var adornerLayerTo)) + { + thatOffset = GetOffsetFrom(common, adornedTo!) * GetOffsetFrom(adornerLayerTo!, to); + } + else + thatOffset = GetOffsetFrom(common, to); if (!thatOffset.TryInvert(out var thatOffsetInverted)) { @@ -66,6 +80,29 @@ public static PixelPoint PointToScreen(this Visual visual, Point point) return null; } + private static bool TryGetAdorner(Visual target, Visual? stopAtVisual, out Visual? adorned, out Visual? adornerLayer) + { + var element = target; + while (element != null && element != stopAtVisual) + { + if (AdornerLayerBase.GetAdornedElement(element) is { } adornedElement) + { + adorned = adornedElement; + adornerLayer = element; + while (adornerLayer != null && adornerLayer is not IAdornerLayer) + { + adornerLayer = adornerLayer.VisualParent; + } + return adornerLayer != null; + } + element = element.VisualParent; + } + + adorned = null; + adornerLayer = null; + return false; + } + /// /// Translates a point relative to this visual to coordinates that are relative to the specified visual. /// diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 412dd236ffd..96edbde9495 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls.Primitives /// /// TODO: Need to track position of adorned elements and move the adorner if they move. /// - public class AdornerLayer : Canvas + public class AdornerLayer : Canvas, IAdornerLayer { /// /// Allows for getting and setting of the adorned element. /// public static readonly AttachedProperty AdornedElementProperty = - AvaloniaProperty.RegisterAttached("AdornedElement"); + AdornerLayerBase.AdornedElementProperty.AddOwner(); /// /// Allows for controlling clipping of the adorner. diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index a0b853f2dc8..05f78118dd2 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -571,15 +571,7 @@ private static Rect CalculateAnchorRect(TopLevel topLevel, PopupPositionRequest var target = positionRequest.Target; if (target == null) throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); - Matrix? matrix; - if (TryGetAdorner(target, out var adorned, out var adornerLayer)) - { - matrix = adorned!.TransformToVisual(topLevel) * target.TransformToVisual(adornerLayer!); - } - else - { - matrix = target.TransformToVisual(topLevel); - } + Matrix? matrix = target.TransformToVisual(topLevel); if (matrix == null) { @@ -592,25 +584,6 @@ private static Rect CalculateAnchorRect(TopLevel topLevel, PopupPositionRequest var anchorRect = positionRequest.AnchorRect ?? bounds; return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); } - - private static bool TryGetAdorner(Visual target, out Visual? adorned, out Visual? adornerLayer) - { - var element = target; - while (element != null) - { - if (AdornerLayer.GetAdornedElement(element) is { } adornedElement) - { - adorned = adornedElement; - adornerLayer = AdornerLayer.GetAdornerLayer(adorned); - return true; - } - element = element.VisualParent; - } - - adorned = null; - adornerLayer = null; - return false; - } } } diff --git a/tests/Avalonia.Controls.UnitTests/AdornerLayerTests.cs b/tests/Avalonia.Controls.UnitTests/AdornerLayerTests.cs new file mode 100644 index 00000000000..6ba891b87a9 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/AdornerLayerTests.cs @@ -0,0 +1,34 @@ +using Avalonia.Controls.Primitives; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests; + +public class AdornerLayerTests : ScopedTestBase +{ + [Fact] + public void Adorners_Include_Adorned_Elements_In_Transform_Visual() + { + var button = new Button() + { + Margin = new Thickness(100, 100) + }; + var root = new TestRoot() + { + Child = new VisualLayerManager() + { + Child = button + } + }; + var adorner = new Border(); + + var adornerLayer = AdornerLayer.GetAdornerLayer(button); + adornerLayer.Children.Add(adorner); + AdornerLayer.SetAdornedElement(adorner, button); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + var translatedPoint = root.TranslatePoint(new Point(100, 100), adorner); + Assert.Equal(new Point(0, 0), translatedPoint); + } +}