From c35038e7058fd088f560da87e573cf00bc18d845 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 13 Apr 2024 15:47:46 -0700 Subject: [PATCH 1/2] Ignore pointer capture for PointerEntered and PointerExited events following UWP behavior --- .../Input/PointerOverPreProcessor.cs | 6 +- .../Input/PointerOverTests.cs | 97 +++++++++++-------- .../Input/PointerTestsBase.cs | 23 +++++ 3 files changed, 81 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index d5a716ba602..9d050d218b9 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -67,7 +67,9 @@ public void OnNext(RawInputEventArgs value) else if (pointerDevice.TryGetPointer(args) is { } pointer && pointer.Type != PointerType.Touch) { - var element = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor; + // See PointerOver_Or_Exited_Should_Ignore_Capture test. + // Following UWP behavior, PointerEntered and PointerExited events should always ignore captured element. + var element = args.InputHitTestResult.firstEnabledAncestor; SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position, new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), @@ -84,7 +86,7 @@ public void SceneInvalidated(Rect dirtyRect) if (dirtyRect.Contains(clientPoint)) { - var element = pointer.Captured ?? _inputRoot.InputHitTest(clientPoint); + var element = _inputRoot.InputHitTest(clientPoint); SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } else if (!((Visual)_inputRoot).Bounds.Contains(clientPoint)) diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs index 62c99a553bc..7d2bb5442cc 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs @@ -116,42 +116,6 @@ public void TouchMove_Should_Not_Set_IsPointerOver() Assert.False(root.IsPointerOver); } - [Fact] - public void HitTest_Should_Be_Ignored_If_Element_Captured() - { - using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); - - var renderer = new Mock(); - var pointer = new Mock(); - var device = CreatePointerDeviceMock(pointer.Object).Object; - var impl = CreateTopLevelImplMock(); - - Canvas canvas; - Border border; - Decorator decorator; - - var root = CreateInputRoot(impl.Object, new Panel - { - Children = - { - (canvas = new Canvas()), - (border = new Border - { - Child = decorator = new Decorator(), - }) - } - }, renderer.Object); - - SetHit(renderer, canvas); - pointer.SetupGet(p => p.Captured).Returns(decorator); - impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); - - Assert.True(decorator.IsPointerOver); - Assert.True(border.IsPointerOver); - Assert.False(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - } - [Fact] public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True() { @@ -492,16 +456,63 @@ void HandleEvent(object? sender, PointerEventArgs e) result); } - private static void AddEnteredExitedHandlers( - EventHandler handler, - params IInputElement[] controls) + // https://github.com/AvaloniaUI/Avalonia/issues/15293 + [Fact] + public void PointerOver_Or_Exited_Should_Ignore_Capture() { - foreach (var c in controls) + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = new MouseDevice(); + var impl = CreateTopLevelImplMock(); + + var result = new List<(object, string, object?, bool)>(); + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }, renderer.Object); + + void HandleEvent(object? sender, PointerEventArgs e) { - c.PointerEntered += handler; - c.PointerExited += handler; - c.PointerMoved += handler; + result.Add((sender!, e.RoutedEvent!.Name, e.Pointer.Captured, canvas.IsPointerOver)); } + + AddEnteredExitedHandlers(HandleEvent, canvas); + AddPressedReleasedHandlers(HandleEvent, canvas); + + // Init pointer over. + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.Move)); + + // Init capture. + impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown)); + + // Leave the control, still captured. + impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.Move)); + SetHit(renderer, null); + impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.Move)); + + // Release capture. + impl.Object.Input!(CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp)); + + Assert.Equal( + new[] + { + ((object)canvas, nameof(InputElement.PointerEntered), (object?)null, true), + (canvas, nameof(InputElement.PointerMoved), null, true), + (canvas, nameof(InputElement.PointerPressed), canvas, true), + (canvas, nameof(InputElement.PointerMoved), canvas, true), + (canvas, nameof(InputElement.PointerExited), canvas, false), + (canvas, nameof(InputElement.PointerMoved), canvas, false), + (canvas, nameof(InputElement.PointerReleased), canvas, false) + }, + result); } } } diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs index b4d20f2496a..3ecaa58a9e5 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs @@ -100,4 +100,27 @@ private protected static TopLevel CreateInputRoot(IWindowImpl impl, Control chil return pointerDevice; } + + protected static void AddEnteredExitedHandlers( + EventHandler handler, + params IInputElement[] controls) + { + foreach (var c in controls) + { + c.PointerEntered += handler; + c.PointerExited += handler; + c.PointerMoved += handler; + } + } + + protected static void AddPressedReleasedHandlers( + EventHandler handler, + params IInputElement[] controls) + { + foreach (var c in controls) + { + c.PointerPressed += (s, a) => handler(s, a); + c.PointerReleased += (s, a) => handler(s, a); + } + } } From 30d12869f0a77a6d66c34e9aec34d636ab0f1a7f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 13 Apr 2024 15:59:12 -0700 Subject: [PATCH 2/2] Fix dev tools event logging --- .../Diagnostics/ViewModels/FiredEvent.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs index 1eab9afe8a2..16f068701b9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs @@ -8,24 +8,28 @@ namespace Avalonia.Diagnostics.ViewModels internal class FiredEvent : ViewModelBase { private readonly RoutedEventArgs _eventArgs; + private readonly RoutedEvent? _originalEvent; private EventChainLink? _handledBy; public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator, DateTime triggerTime) { _eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs)); Originator = originator ?? throw new ArgumentNullException(nameof(originator)); + _originalEvent = _eventArgs.RoutedEvent; AddToChain(originator); TriggerTime = triggerTime; } public bool IsPartOfSameEventChain(RoutedEventArgs e) { - return e == _eventArgs; + // Note, Avalonia might reuse RoutedEventArgs for different events to avoid extra allocations. + // Like, PointerEntered and PointerExited will use the same instance of RoutedEventArgs. + return e == _eventArgs && e.RoutedEvent == _originalEvent; } public DateTime TriggerTime { get; } - public RoutedEvent Event => _eventArgs.RoutedEvent!; + public RoutedEvent Event => _originalEvent!; public bool IsHandled => HandledBy?.Handled == true;