diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 5cc572198a..d54d0be903 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -66,6 +66,7 @@ namespace Avalonia.Input if (relativeTo == null) return pt; + // If relativeTo visual is from another visual tree and possibly window, translate position first. var pointRootVisual = _rootVisual; if (relativeTo.VisualRoot is { } root && _rootVisual != root) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index 2ebf01bcf6..e367f9f74f 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -68,27 +68,29 @@ namespace Avalonia.Input if (dirtyRect.Contains(clientPoint)) { - SetPointerOver(pointer, _inputRoot, _inputRoot.InputHitTest(clientPoint), 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); + var element = pointer.Captured ?? _inputRoot.InputHitTest(clientPoint); + SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } else if (!_inputRoot.Bounds.Contains(clientPoint)) { - ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } } } private void ClearPointerOver() { - if (_lastPointer is (var pointer, var _)) + if (_lastPointer is (var pointer, var position)) { - ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + var clientPoint = _inputRoot.PointToClient(position); + ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } _lastPointer = null; _lastActivePointerDevice = null; } private void ClearPointerOver(IPointer pointer, IInputRoot root, - ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + ulong timestamp, Point? position, PointerPointProperties properties, KeyModifiers inputModifiers) { var element = root.PointerOverElement; if (element is null) @@ -96,11 +98,10 @@ namespace Avalonia.Input return; } - // Do not pass rootVisual, when we have unknown (negative) position, + // Do not pass rootVisual, when we have unknown position, // so GetPosition won't return invalid values. - var hasPosition = position.X >= 0 && position.Y >= 0; var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer, - hasPosition ? root : null, hasPosition ? position : default, + position.HasValue ? root : null, position.HasValue ? position.Value : default, timestamp, properties, inputModifiers); if (element != null && !element.IsAttachedToVisualTree) diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs index a83b176484..1ac50446c0 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs @@ -356,20 +356,28 @@ namespace Avalonia.Base.UnitTests.Input var impl = CreateTopLevelImplMock(renderer.Object); var invalidateRect = new Rect(0, 0, 15, 15); + var lastClientPosition = new Point(1, 5); + + var result = new List<(object?, string, Point)>(); + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null))); + } Canvas canvas; - var root = CreateInputRoot(impl.Object, new Panel + var root = (Window)CreateInputRoot(impl.Object, new Panel { Children = { (canvas = new Canvas()) } }); + AddEnteredExitedHandlers(HandleEvent, root, canvas); // Let input know about latest device. SetHit(renderer, canvas); - impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition)); Assert.True(canvas.IsPointerOver); SetHit(renderer, canvas); @@ -380,6 +388,52 @@ namespace Avalonia.Base.UnitTests.Input SetHit(renderer, null); renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect)); Assert.False(canvas.IsPointerOver); + + Assert.Equal( + new[] + { + ((object?)canvas, nameof(InputElement.PointerEntered), lastClientPosition), + (root, nameof(InputElement.PointerEntered), lastClientPosition), + (canvas, nameof(InputElement.PointerExited), lastClientPosition), + (root, nameof(InputElement.PointerExited), lastClientPosition), + }, + result); + } + + [Fact] + public void PointerOver_Invalidation_Should_Use_Previously_Captured_Element() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + + var invalidateRect = new Rect(0, 0, 15, 15); + + Canvas canvas1, canvas2; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas1 = new Canvas()), + (canvas2 = new Canvas()) + } + }); + + canvas1.PointerMoved += (s, a) => a.Pointer.Capture(canvas1); + + // Let input know about latest device. + SetHit(renderer, canvas1); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + Assert.True(canvas1.IsPointerOver); + Assert.False(canvas2.IsPointerOver); + + SetHit(renderer, canvas2); + renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect)); + Assert.False(canvas1.IsPointerOver); + Assert.True(canvas2.IsPointerOver); } // https://github.com/AvaloniaUI/Avalonia/issues/7748 diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs index e25ef0a9b8..0bd23d64a9 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs @@ -8,7 +8,7 @@ using Xunit; namespace Avalonia.Base.UnitTests.Input { - public class PointerTests + public class PointerTests : PointerTestsBase { [Fact] public void On_Capture_Transfer_PointerCaptureLost_Should_Propagate_Up_To_The_Common_Parent()