diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 7f82199b56..9c24e5c314 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -70,6 +70,17 @@ namespace Avalonia.Input if (relativeTo == null) return pt; + // If the visual the user passed in, is not connected to the same visual root + // (i.e. they called it for a control inside a popup. + if (!ReferenceEquals(_rootVisual, relativeTo.VisualRoot) && relativeTo.VisualRoot is { }) + { + // Convert to absolute screen coordinates. + var screenPt = _rootVisual.PointToScreen(pt); + + // Convert to client co-ordinates of the visual inside the other visual root. + return relativeTo.PointToClient(screenPt); + } + return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 51399d1202..888162cfee 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -19,6 +19,7 @@ using Avalonia.Rendering; using System.Threading.Tasks; using Avalonia.Threading; using Avalonia.Interactivity; +using Avalonia.Media; namespace Avalonia.Controls.UnitTests.Primitives { @@ -1076,30 +1077,98 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - private IDisposable CreateServices() + [Fact] + public void GetPosition_On_Control_In_Popup_Called_From_Parent_Should_Return_Valid_Coordinates() { - return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: - new MockWindowingPlatform(null, - x => + // This test only applies when using a PopupRoot host and not an overlay popup. + if (UsePopupHost) + return; + + using (CreateServices()) + { + var popupContent = new Border() { Height = 100, Width = 100, Background = Brushes.Red }; + var popup = new Popup { Child = popupContent, HorizontalOffset = 40, VerticalOffset = 40, Placement = PlacementMode.AnchorAndGravity, + PlacementAnchor = PopupAnchor.TopLeft, PlacementGravity = PopupGravity.BottomRight}; + var popupParent = new Border { Child = popup }; + var root = PreparedWindow(popupParent); + + popup.Open(); + + // Verify that the popup is positioned at 40,40 as descibed by the Horizontal/ + // VerticalOffset: 10,10 becomes 50,50 in screen coordinates. + Assert.Equal(new PixelPoint(50, 50), popupContent.PointToScreen(new Point(10, 10))); + + // The popup parent is positioned at 0,0 in screen coordinates so client and + // screen coordinates are the same. + Assert.Equal(new PixelPoint(10, 10), popupParent.PointToScreen(new Point(10, 10))); + + // The event will be raised on the popup content at 50,50 (90,90 in screen coordinates) + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + var ev = new PointerPressedEventArgs( + popupContent, + pointer, + popupContent.VisualRoot as PopupRoot, + new Point(50 , 50), + 0, + new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), + KeyModifiers.None); + + var contentRaised = 0; + var parentRaised = 0; + + // The event is raised on the popup content in popup coordinates. + popupContent.AddHandler(Button.PointerPressedEvent, (s, e) => + { + ++contentRaised; + Assert.Equal(new Point(50, 50), e.GetPosition(popupContent)); + }); + + // The event is raised on the parent in root coordinates (which in this case are + // the same as screen coordinates). + popupParent.AddHandler(Button.PointerPressedEvent, (s, e) => + { + ++parentRaised; + Assert.Equal(new Point(90, 90), e.GetPosition(popupParent)); + }); + + popupContent.RaiseEvent(ev); + + Assert.Equal(1, contentRaised); + Assert.Equal(1, parentRaised); + } + } + + private static PopupRoot CreateRoot(TopLevel popupParent, IPopupImpl impl = null) + { + impl ??= popupParent.PlatformImpl.CreatePopup(); + + var result = new PopupRoot(popupParent, impl) + { + Template = new FuncControlTemplate((parent, scope) => + new ContentPresenter { - if(UsePopupHost) - return null; - return MockWindowingPlatform.CreatePopupMock(x).Object; - }))); + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = parent[!PopupRoot.ContentProperty], + }.RegisterInNameScope(scope)), + }; + + result.ApplyTemplate(); + + return result; + } + + private IDisposable CreateServices() + { + return UnitTestApplication.Start(TestServices.StyledWindow.With( + windowingPlatform: CreateMockWindowingPlatform())); } private IDisposable CreateServicesWithFocus() { - return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: - new MockWindowingPlatform(null, - x => - { - if (UsePopupHost) - return null; - return MockWindowingPlatform.CreatePopupMock(x).Object; - }), - focusManager: new FocusManager(), - keyboardDevice: () => new KeyboardDevice())); + return UnitTestApplication.Start(TestServices.StyledWindow.With( + windowingPlatform: CreateMockWindowingPlatform(), + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice())); } @@ -1116,6 +1185,23 @@ namespace Avalonia.Controls.UnitTests.Primitives KeyModifiers.None); } + private MockWindowingPlatform CreateMockWindowingPlatform() + { + return new MockWindowingPlatform(() => + { + var mock = MockWindowingPlatform.CreateWindowMock(); + + mock.Setup(x => x.CreatePopup()).Returns(() => + { + if (UsePopupHost) + return null; + return MockWindowingPlatform.CreatePopupMock(mock.Object).Object; + }); + + return mock.Object; + }, null); + } + private static Window PreparedWindow(object content = null) { var w = new Window { Content = content }; diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index ca71a97a6e..5451773da4 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -24,7 +24,6 @@ namespace Avalonia.UnitTests public static Mock CreateWindowMock(double initialWidth = 800, double initialHeight = 600) { var windowImpl = new Mock(); - var position = new PixelPoint(); var clientSize = new Size(initialWidth, initialHeight); windowImpl.SetupAllProperties(); @@ -35,7 +34,6 @@ namespace Avalonia.UnitTests windowImpl.Setup(x => x.DesktopScaling).Returns(1); windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object); - windowImpl.Setup(x => x.Position).Returns(() => position); windowImpl.Setup(r => r.TryGetFeature(It.IsAny())).Returns(null); @@ -51,10 +49,16 @@ namespace Avalonia.UnitTests windowImpl.Setup(x => x.Move(It.IsAny())).Callback(x => { - position = x; + windowImpl.Setup(x => x.Position).Returns(x); windowImpl.Object.PositionChanged?.Invoke(x); }); + windowImpl.Setup(x => x.PointToScreen(It.IsAny())) + .Returns((point) => PixelPoint.FromPoint(point, 1) + windowImpl.Object.Position); + + windowImpl.Setup(x => x.PointToClient(It.IsAny())) + .Returns(point => (point - windowImpl.Object.Position).ToPoint(1)); + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) .Callback((x, y) => { @@ -73,9 +77,6 @@ namespace Avalonia.UnitTests windowImpl.Object.Activated?.Invoke(); }); - windowImpl.Setup(x => x.PointToScreen(It.IsAny())) - .Returns((Point p) => PixelPoint.FromPoint(p, 1D) + position); - return windowImpl; } @@ -83,10 +84,12 @@ namespace Avalonia.UnitTests { var popupImpl = new Mock(); var clientSize = new Size(); + var position = new PixelPoint(); var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) => { clientSize = size.Constrain(s_screenSize); + position = pos; popupImpl.Object.PositionChanged?.Invoke(pos); popupImpl.Object.Resized?.Invoke(clientSize, WindowResizeReason.Unspecified); }); @@ -100,6 +103,15 @@ namespace Avalonia.UnitTests popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize); popupImpl.Setup(x => x.RenderScaling).Returns(1); popupImpl.Setup(x => x.PopupPositioner).Returns(positioner); + popupImpl.Setup(x => x.Position).Returns(()=>position); + + popupImpl.Setup(x => x.PointToScreen(It.IsAny())) + .Returns((point) => PixelPoint.FromPoint(point, 1) + position); + + popupImpl.Setup(x => x.PointToClient(It.IsAny())) + .Returns(point => (point - position).ToPoint(1)); + + popupImpl.Setup(r => r.TryGetFeature(It.IsAny())).Returns(null); popupImpl.Setup(x => x.Dispose()).Callback(() =>