diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 9285e65fa55..ee1b54b7924 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -3,14 +3,14 @@ using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.Metadata; -using Avalonia.VisualTree; +using Avalonia.Rendering; namespace Avalonia.Input { public class PointerEventArgs : RoutedEventArgs, IKeyModifiersEventArgs { - private readonly Visual? _rootVisual; - private readonly Point _rootVisualPosition; + private readonly IPresentationSource? _eventPresentationSource; // the original observer of the event + private readonly Point _presentationSourcePosition; private readonly PointerPointProperties _properties; private readonly Lazy?>? _previousPoints; @@ -24,8 +24,8 @@ public PointerEventArgs(RoutedEvent? routedEvent, : base(routedEvent) { Source = source; - _rootVisual = rootVisual; - _rootVisualPosition = rootVisualPosition; + _eventPresentationSource = rootVisual?.PresentationSource; + _presentationSourcePosition = rootVisualPosition; _properties = properties; Pointer = pointer; Timestamp = timestamp; @@ -76,23 +76,25 @@ private set private Point GetPosition(Point pt, Visual? relativeTo) { - if (_rootVisual == null) - return default; if (relativeTo == null) return pt; + if (_eventPresentationSource == null || relativeTo?.PresentationSource?.RootVisual == null) + return default; - // 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 { }) + if (relativeTo.PresentationSource != _eventPresentationSource) { - // 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); + if (_eventPresentationSource.PointToScreen(pt) is { } screenPt && + relativeTo.PresentationSource.PointToClient(screenPt) is { } targetClientPt) + { + pt = targetClientPt; + } + else + { + return default; + } } - return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; + return relativeTo.PresentationSource.RootVisual.TranslatePoint(pt, relativeTo) ?? default; } /// @@ -100,7 +102,7 @@ private Point GetPosition(Point pt, Visual? relativeTo) /// /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// The pointer position in the control's coordinates. - public Point GetPosition(Visual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); + public Point GetPosition(Visual? relativeTo) => GetPosition(_presentationSourcePosition, relativeTo); /// /// Returns the PointerPoint associated with the current event @@ -117,7 +119,7 @@ public PointerPoint GetCurrentPoint(Visual? relativeTo) /// public IReadOnlyList GetIntermediatePoints(Visual? relativeTo) { - var previousPoints = _previousPoints?.Value; + var previousPoints = _previousPoints?.Value; if (previousPoints == null || previousPoints.Count == 0) return new[] { GetCurrentPoint(relativeTo) }; var points = new PointerPoint[previousPoints.Count + 1]; @@ -145,7 +147,7 @@ public void PreventGestureRecognition() /// public PointerPointProperties Properties => _properties; } - + public enum MouseButton { None, diff --git a/src/Avalonia.Base/Rendering/IPresentationSource.cs b/src/Avalonia.Base/Rendering/IPresentationSource.cs index bf5beebe490..ff9da11afa2 100644 --- a/src/Avalonia.Base/Rendering/IPresentationSource.cs +++ b/src/Avalonia.Base/Rendering/IPresentationSource.cs @@ -41,6 +41,6 @@ public interface IPresentationSource /// internal Size ClientSize { get; } - internal PixelPoint PointToScreen(Point point); - internal Point PointToClient(PixelPoint point); + internal PixelPoint? PointToScreen(Point point); + internal Point? PointToClient(PixelPoint point); } diff --git a/src/Avalonia.Base/VisualExtensions.cs b/src/Avalonia.Base/VisualExtensions.cs index 3df2eca039f..fa4236bf467 100644 --- a/src/Avalonia.Base/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualExtensions.cs @@ -14,12 +14,13 @@ public static class VisualExtensions /// The visual. /// The point in screen coordinates. /// The point in client coordinates. + /// Thown when does not belong to a visual tree. public static Point PointToClient(this Visual visual, PixelPoint point) { var source = visual.PresentationSource; var root = source?.RootVisual ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); - var rootPoint = source.PointToClient(point); + throw new ArgumentException("Visual does not belong to a visual tree.", nameof(visual)); + var rootPoint = source.PointToClient(point) ?? default; return root.TranslatePoint(rootPoint, visual)!.Value; } @@ -29,13 +30,14 @@ public static Point PointToClient(this Visual visual, PixelPoint point) /// The visual. /// The point in client coordinates. /// The point in screen coordinates. + /// Thown when does not belong to a visual tree. public static PixelPoint PointToScreen(this Visual visual, Point point) { var source = visual.PresentationSource; var root = source?.RootVisual ?? - throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); + throw new ArgumentException("Visual does not belong to a visual tree.", nameof(visual)); var p = visual.TranslatePoint(point, root); - return source.PointToScreen(p!.Value); + return source.PointToScreen(p!.Value) ?? default; } /// diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs index 2a9e833a48a..e5fb12eba46 100644 --- a/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs +++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs @@ -23,9 +23,9 @@ public void SceneInvalidated(object? sender, SceneInvalidatedEventArgs sceneInva _pointerOverPreProcessor?.SceneInvalidated(sceneInvalidatedEventArgs.DirtyRect); } - public PixelPoint PointToScreen(Point point) => PlatformImpl?.PointToScreen(point) ?? default; + public PixelPoint? PointToScreen(Point point) => PlatformImpl?.PointToScreen(point); - public Point PointToClient(PixelPoint point) => PlatformImpl?.PointToClient(point) ?? default; + public Point? PointToClient(PixelPoint point) => PlatformImpl?.PointToClient(point); private void HandleScalingChanged(double scaling) => RenderScaling = LayoutHelper.ValidateScaling(scaling); diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs index 1ec6ec2be5c..f8bb8c3ed29 100644 --- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs @@ -1,4 +1,6 @@ -using Avalonia.Controls; +using System; +using Avalonia.Controls; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media; @@ -128,6 +130,74 @@ public void GetPosition_Should_Respect_Control_RenderTransform() Assert.Equal(new Point(1, 11), result); } + private IDisposable SetupCrossTreePositionRequest(PixelPoint topLevelPosition, out PointerEventArgs pointerEvent, out Control elementA, out Control elementB) + { + var app = UnitTestApplication.Start(new TestServices( + inputManager: new InputManager(), + renderInterface: new HeadlessPlatformRenderInterface())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl1 = CreateTopLevelImplMock(); + // Mocked position: topLevelPosition + impl1.Setup(w => w.PointToScreen(default)).Returns(p => (PixelPoint.FromPoint(p, 1) + topLevelPosition)); + + elementA = new Border(); + PointerEventArgs? moveEventArgs = null; + + elementA.PointerMoved += (s, e) => moveEventArgs = e; + var root1 = CreateInputRoot(impl1.Object, elementA, renderer.Object); + + SetMove(deviceMock, root1.InputRoot, elementA); + impl1.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root1)); + + Assert.NotNull(moveEventArgs); + pointerEvent = moveEventArgs; + + var impl2 = CreateTopLevelImplMock(); + // Mocked position: topLevelPosition * 2 + impl2.Setup(w => w.PointToClient(default)).Returns(p => (p - topLevelPosition - topLevelPosition).ToPoint(1)); + + elementB = new Border(); + var root2 = CreateInputRoot(impl2.Object, elementB, renderer.Object); + + return app; + } + + [Fact] + public void GetPosition_Should_Support_Cross_Tree_Requests() + { + var topLevelOffset = new PixelPoint(5, 0); + using (SetupCrossTreePositionRequest(topLevelOffset, out var pointerEvent, out _, out var elementB)) + { + Assert.Equal(topLevelOffset.ToPoint(1), pointerEvent.GetPosition(elementB)); + } + } + + [Fact] + public void GetPosition_Should_Return_Default_When_Cross_Tree_Source_Closed() + { + var topLevelOffset = new PixelPoint(5, 0); + using (SetupCrossTreePositionRequest(topLevelOffset, out var pointerEvent, out var elementA, out var elementB)) + { + ((PresentationSource)elementA.PresentationSource!).Dispose(); + + Assert.Equal(default, pointerEvent.GetPosition(elementB)); + } + } + + [Fact] + public void GetPosition_Should_Return_Default_When_Cross_Tree_Target_Closed() + { + var topLevelOffset = new PixelPoint(5, 0); + using (SetupCrossTreePositionRequest(topLevelOffset, out var pointerEvent, out _, out var elementB)) + { + ((PresentationSource)elementB.PresentationSource!).Dispose(); + + Assert.Equal(default, pointerEvent.GetPosition(elementB)); + } + } + [Fact] public void Mouse_Pointer_Should_Set_Focus_On_Pointer_Pressed() { diff --git a/tests/Avalonia.RenderTests/TestRenderRoot.cs b/tests/Avalonia.RenderTests/TestRenderRoot.cs index 602c5c6e8b3..4bbe560fb64 100644 --- a/tests/Avalonia.RenderTests/TestRenderRoot.cs +++ b/tests/Avalonia.RenderTests/TestRenderRoot.cs @@ -67,9 +67,9 @@ public void Invalidate(Rect rect) { } - public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); + public Point? PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); - public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); + public PixelPoint? PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); public IFocusManager? FocusManager { get; } public IPlatformSettings? PlatformSettings { get; } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 4400d77267c..5a34d301df2 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -113,9 +113,9 @@ public void Invalidate(Rect rect) { } - public Point PointToClient(PixelPoint p) => p.ToPoint(1); + public Point? PointToClient(PixelPoint p) => p.ToPoint(1); - public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); + public PixelPoint? PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); public void RegisterChildrenNames()