diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 9935dbe27c..82e626f9c6 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -162,8 +162,8 @@ namespace Avalonia.Input KeyDownEvent.AddClassHandler(x => x.OnKeyDown); KeyUpEvent.AddClassHandler(x => x.OnKeyUp); TextInputEvent.AddClassHandler(x => x.OnTextInput); - PointerEnterEvent.AddClassHandler(x => x.OnPointerEnter); - PointerLeaveEvent.AddClassHandler(x => x.OnPointerLeave); + PointerEnterEvent.AddClassHandler(x => x.OnPointerEnterCore); + PointerLeaveEvent.AddClassHandler(x => x.OnPointerLeaveCore); PointerMovedEvent.AddClassHandler(x => x.OnPointerMoved); PointerPressedEvent.AddClassHandler(x => x.OnPointerPressed); PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased); @@ -445,7 +445,6 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerEnter(PointerEventArgs e) { - IsPointerOver = true; } /// @@ -454,7 +453,6 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerLeave(PointerEventArgs e) { - IsPointerOver = false; } /// @@ -494,6 +492,26 @@ namespace Avalonia.Input ((InputElement)e.Sender).UpdateIsEnabledCore(); } + /// + /// Called before the event occurs. + /// + /// The event args. + private void OnPointerEnterCore(PointerEventArgs e) + { + IsPointerOver = true; + OnPointerEnter(e); + } + + /// + /// Called before the event occurs. + /// + /// The event args. + private void OnPointerLeaveCore(PointerEventArgs e) + { + IsPointerOver = false; + OnPointerLeave(e); + } + /// /// Updates the property value. /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 50cdb5945c..7689f55d80 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -346,12 +346,7 @@ namespace Avalonia.Input IInputElement branch = null; - var e = new PointerEventArgs - { - RoutedEvent = InputElement.PointerEnterEvent, - Device = device, - }; - + var e = new PointerEventArgs { Device = device, }; var el = element; while (el != null) @@ -361,9 +356,6 @@ namespace Avalonia.Input branch = el; break; } - - e.Source = el; - el.RaiseEvent(e); el = (IInputElement)el.VisualParent; } @@ -373,11 +365,21 @@ namespace Avalonia.Input while (el != null && el != branch) { e.Source = el; + e.Handled = false; el.RaiseEvent(e); el = (IInputElement)el.VisualParent; } - root.PointerOverElement = element; + el = root.PointerOverElement = element; + e.RoutedEvent = InputElement.PointerEnterEvent; + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement)el.VisualParent; + } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 7764c47dbf..5853d1e82b 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -1,10 +1,12 @@ using Avalonia.Controls; using Avalonia.Input.Raw; +using Avalonia.Interactivity; using Avalonia.Rendering; using Avalonia.UnitTests; using Avalonia.VisualTree; using Moq; using System; +using System.Collections.Generic; using Xunit; namespace Avalonia.Input.UnitTests @@ -30,7 +32,7 @@ namespace Avalonia.Input.UnitTests } [Fact] - public void MouseMove_Should_Update_PointerOver() + public void MouseMove_Should_Update_IsPointerOver() { var renderer = new Mock(); @@ -59,40 +61,157 @@ namespace Avalonia.Input.UnitTests } }; - renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) - .Returns(new[] { decorator }); - - inputManager.ProcessInput(new RawMouseEventArgs( - root.MouseDevice, - 0, - root, - RawMouseEventType.Move, - new Point(), - InputModifiers.None)); + SetHit(renderer, decorator); + SendMouseMove(inputManager, root); Assert.True(decorator.IsPointerOver); Assert.True(border.IsPointerOver); Assert.False(canvas.IsPointerOver); Assert.True(root.IsPointerOver); - renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) - .Returns(new[] { canvas }); + SetHit(renderer, canvas); + SendMouseMove(inputManager, root); + + Assert.False(decorator.IsPointerOver); + Assert.False(border.IsPointerOver); + Assert.True(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + } + } + + [Fact] + public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True() + { + var renderer = new Mock(); + + using (TestApplication(renderer.Object)) + { + var inputManager = InputManager.Instance; - inputManager.ProcessInput(new RawMouseEventArgs( - root.MouseDevice, - 0, - root, - RawMouseEventType.Move, - new Point(), - InputModifiers.None)); + Canvas canvas; + Border border; + Decorator decorator; + + var root = new TestRoot + { + MouseDevice = new MouseDevice(), + Renderer = renderer.Object, + Child = new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + } + }; + + SetHit(renderer, canvas); + SendMouseMove(inputManager, root); Assert.False(decorator.IsPointerOver); Assert.False(border.IsPointerOver); Assert.True(canvas.IsPointerOver); Assert.True(root.IsPointerOver); + + // Ensure that e.Handled is reset between controls. + decorator.PointerEnter += (s, e) => e.Handled = true; + + SetHit(renderer, decorator); + SendMouseMove(inputManager, root); + + Assert.True(decorator.IsPointerOver); + Assert.True(border.IsPointerOver); + Assert.False(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); } } + [Fact] + public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order() + { + var renderer = new Mock(); + var result = new List<(object, string)>(); + + void HandleEvent(object sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent.Name)); + } + + using (TestApplication(renderer.Object)) + { + var inputManager = InputManager.Instance; + + Canvas canvas; + Border border; + Decorator decorator; + + var root = new TestRoot + { + MouseDevice = new MouseDevice(), + Renderer = renderer.Object, + Child = new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + } + }; + + SetHit(renderer, canvas); + SendMouseMove(inputManager, root); + + AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator); + SetHit(renderer, decorator); + SendMouseMove(inputManager, root); + + Assert.Equal( + new[] + { + ((object)canvas, "PointerLeave"), + ((object)decorator, "PointerEnter"), + ((object)border, "PointerEnter"), + }, + result); + } + } + + private void AddEnterLeaveHandlers( + EventHandler handler, + params IControl[] controls) + { + foreach (var c in controls) + { + c.PointerEnter += handler; + c.PointerLeave += handler; + } + } + + private void SendMouseMove(IInputManager inputManager, TestRoot root) + { + inputManager.ProcessInput(new RawMouseEventArgs( + root.MouseDevice, + 0, + root, + RawMouseEventType.Move, + new Point(), + InputModifiers.None)); + } + + private void SetHit(Mock renderer, IControl hit) + { + renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(new[] { hit }); + } + private IDisposable TestApplication(IRenderer renderer) { return UnitTestApplication.Start(