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);
+ }
+}