Repository: AvaloniaUI/Avalonia

Issue:
Popup with a placement target pops up in a wrong position with compositor in adorner layer
**Describe the bug**
Popups are displayed in a wrong position, with compositor enabled, with Placement Target specified (pointer position works) when the parent control is an adorner layer.

**To Reproduce**
Open a popup (i.e. combobox), which belongs to a control that is placed on an adorner layer, with a placement target, with the compositor enabled.

or

1. Clone https://github.com/BAndysc/Avalonia_Compositor_Adorner_Popup_Bug
2. Run (by default the compositor is disabled)
3. See the correct result: (both popups displayed below the controls)
![image](https://user-images.githubusercontent.com/5689666/210281832-cf808a05-8cf7-48ea-87ca-b95f7a002fa6.png)
4. Change `UseCompositor` to `true` and run
5. See the popup is displayed in a wrong place

![image](https://user-images.githubusercontent.com/5689666/210281880-88871c65-eb94-444e-8975-f7bc11193b14.png)

**Desktop:**

- OS: macOS
- Version 11.0.999-cibuild0028025-beta

**Additional context:**
The example might be a bit cursed, but I think it is a common practice to place a combobox in an adorner layer, and the list will be opened in a wrong place as well (as this uses the same popups underneath)


Hints:
I've checked this again in latest stable Avalonia 11.0.0, but sadly it is still bugged. Sorry for the ping, but let me ping @kekekeks as I believe if anybody might have an idea where the problem is, that's Nikita.
I've debugged the code and I was able to find the offending line:

in Avalonia 0.10 the bounds here were taken directly from TransformedBounds property:

https://github.com/AvaloniaUI/Avalonia/blob/7739bd9ca3a20a8cb2b10c4ee2ca31337b227f4e/src/Avalonia.Controls/Primitives/AdornerLayer.cs#L183-L187

since this property has been removed, new Adorner code constructs TransformedBounds manually, but as you can see the `transform` Matrix is always set to Identity, which is the problem.

https://github.com/AvaloniaUI/Avalonia/blob/2ecfbb978337ef7937821717c01421cdf60cd504/src/Avalonia.Controls/Primitives/AdornerLayer.cs#L321-L325

Later in the code, this invalid TransformedBounds is assigned to RenderTransform, which is used to determine the popup location:

https://github.com/AvaloniaUI/Avalonia/blob/2ecfbb978337ef7937821717c01421cdf60cd504/src/Avalonia.Controls/Primitives/AdornerLayer.cs#L228


### Partial fix:



changing
```
info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity);
```

to

```
info.Bounds = adorned.GetTransformedBounds();
```

fixes the problem, but as you can probably guess at this point, this is not the only problem here. With the compositor change, observing for `TransformedBounds` changes was changed to observing for `Bounds` changes. This means if the control moves "indirectly" (i.e. its parents move), then this event will not be fired and the Bounds will not be correctly updated.

Since I am not sure how to solve this issue correctly, let me ping @kekekeks since you are the main compositor engine author.

Patch:
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
index 4029782772b..c0ea14037cd 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
@@ -468,7 +468,16 @@ public static void ConfigurePosition(ref this PopupPositionerParameters position
             {
                 if (target == null)
                     throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
-                var matrix = target.TransformToVisual(topLevel);
+                Matrix? matrix;
+                if (TryGetAdorner(target, out var adorned, out var adornerLayer))
+                {
+                    matrix = adorned!.TransformToVisual(topLevel) * target.TransformToVisual(adornerLayer!);
+                }
+                else
+                {
+                    matrix = target.TransformToVisual(topLevel);
+                }
+
                 if (matrix == null)
                 {
                     if (target.GetVisualRoot() == null)
@@ -529,6 +538,25 @@ public static void ConfigurePosition(ref this PopupPositionerParameters position
                 }
             }
         }
+
+        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;
+        }
     }
 
 }


Test patch:
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
index 5a0c60dff68..8398da59a5c 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
@@ -1143,6 +1143,53 @@ public void GetPosition_On_Control_In_Popup_Called_From_Parent_Should_Return_Val
             }
         }
 
+        [Fact]
+        public void Popup_Attached_To_Adorner_Respects_Adorner_Position()
+        {
+            using (CreateServices())
+            {
+                var popupTarget = new Border() { Height = 30, Background = Brushes.Red, [DockPanel.DockProperty] = Dock.Top };
+                var popupContent = new Border() { Height = 30, Width = 50, Background = Brushes.Yellow };
+                var popup = new Popup
+                {
+                    Child = popupContent,
+                    Placement = PlacementMode.AnchorAndGravity,
+                    PlacementTarget = popupTarget,
+                    PlacementAnchor = PopupAnchor.BottomRight,
+                    PlacementGravity = PopupGravity.BottomRight
+                };
+                var adorner = new DockPanel() { Children = { popupTarget, popup },
+                    HorizontalAlignment = HorizontalAlignment.Left,
+                    Width = 40,
+                    Margin = new Thickness(50, 5, 0, 0) };
+
+                var adorned = new Border() {
+                    Width = 100,
+                    Height = 100,
+                    Background = Brushes.Blue,
+                    [Canvas.LeftProperty] = 20,
+                    [Canvas.TopProperty] = 40
+                };
+                var windowContent = new Canvas();
+                windowContent.Children.Add(adorned);
+
+                var root = PreparedWindow(windowContent);
+
+                var adornerLayer = AdornerLayer.GetAdornerLayer(adorned);
+                adornerLayer.Children.Add(adorner);
+                AdornerLayer.SetAdornedElement(adorner, adorned);
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+                popup.Open();
+                Dispatcher.UIThread.RunJobs(DispatcherPriority.AfterRender);
+
+                // X: Adorned Canvas.Left + Adorner Margin Left + Adorner Width
+                // Y: Adorned Canvas.Top + Adorner Margin Top + Adorner Height
+                Assert.Equal(new PixelPoint(110, 75), popupContent.PointToScreen(new Point(0, 0)));
+            }
+        }
+
+
         private static PopupRoot CreateRoot(TopLevel popupParent, IPopupImpl impl = null)
         {
             impl ??= popupParent.PlatformImpl.CreatePopup();