From 2dad5591382ab3ee9abeb8a10105cc11b1516a2c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 8 Sep 2022 21:06:29 +0200 Subject: [PATCH 1/3] Transform screen position for the point args from another visual root --- src/Avalonia.Base/Input/PointerEventArgs.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 1f3c726e7b..5cc572198a 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -65,7 +65,17 @@ namespace Avalonia.Input return default; if (relativeTo == null) return pt; - return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; + + var pointRootVisual = _rootVisual; + if (relativeTo.VisualRoot is { } root + && _rootVisual != root) + { + var screenPoint = _rootVisual.PointToScreen(pt); + pt = relativeTo.VisualRoot.PointToClient(screenPoint); + pointRootVisual = root; + } + + return pt * pointRootVisual.TransformToVisual(relativeTo) ?? default; } /// From 46bf9e26023d37ebd9808819a354410a8418ab96 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 8 Sep 2022 21:06:53 +0200 Subject: [PATCH 2/3] Fix issue with 0,0 position when pointerover was cleaned up --- src/Avalonia.Base/Input/PointerEventArgs.cs | 1 + .../Input/PointerOverPreProcessor.cs | 17 +++--- .../Input/PointerOverTests.cs | 58 ++++++++++++++++++- .../Input/PointerTests.cs | 2 +- 4 files changed, 67 insertions(+), 11 deletions(-) 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() From 5a12a036d96e758a640c905b0cd295f9ea75bc1b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 13 Sep 2022 18:50:23 +0200 Subject: [PATCH 3/3] Undo pointer transformation changes --- src/Avalonia.Base/Input/PointerEventArgs.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index d54d0be903..a0e8d4a074 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -66,17 +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) - { - var screenPoint = _rootVisual.PointToScreen(pt); - pt = relativeTo.VisualRoot.PointToClient(screenPoint); - pointRootVisual = root; - } - - return pt * pointRootVisual.TransformToVisual(relativeTo) ?? default; + return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; } ///