From ad34029c1becfe4eec479e592f8fbf6660a58f7d Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 30 Jul 2025 09:34:39 +0000 Subject: [PATCH] Use captured element if available as source for tap gestures (#19222) * update mouse test to better simulate clicks on captured controls * add tap failing test * use captured element if available as source for tap gestures --- src/Avalonia.Base/Input/Gestures.cs | 34 ++++++++++++------- .../Input/GesturesTests.cs | 34 +++++++++++++++++++ tests/Avalonia.UnitTests/MouseTestHelper.cs | 6 ++-- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 156178c061..3298af3a0f 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -218,9 +218,16 @@ namespace Avalonia.Input public static void RemoveScrollGestureInertiaStartingHandler(Interactive element, EventHandler handler) => element.RemoveHandler(ScrollGestureInertiaStartingEvent, handler); + private static object? GetCaptured(RoutedEventArgs? args) + { + if (args is not PointerEventArgs pointerEventArgs) + return null; + return pointerEventArgs.Pointer?.Captured ?? pointerEventArgs.Source; + } + private static void PointerPressed(RoutedEventArgs ev) { - if (ev.Source is null) + if (GetCaptured(ev) is not { } source) { return; } @@ -228,11 +235,11 @@ namespace Avalonia.Input if (ev.Route == RoutingStrategies.Bubble) { var e = (PointerPressedEventArgs)ev; - var visual = (Visual)ev.Source; + var visual = (Visual)source; if(s_gestureState != null) { - if(s_gestureState.Value.Type == GestureStateType.Holding && ev.Source is Interactive i) + if(s_gestureState.Value.Type == GestureStateType.Holding && source is Interactive i) { i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); } @@ -246,8 +253,8 @@ namespace Avalonia.Input if (e.ClickCount % 2 == 1) { s_gestureState = new GestureState(GestureStateType.Pending, e.Pointer); - s_lastPress.SetTarget(ev.Source); - s_lastPressPoint = e.GetPosition((Visual)ev.Source); + s_lastPress.SetTarget(source); + s_lastPressPoint = e.GetPosition((Visual)source); s_holdCancellationToken = new CancellationTokenSource(); var token = s_holdCancellationToken.Token; var settings = ((IInputRoot?)visual.GetVisualRoot())?.PlatformSettings; @@ -256,7 +263,7 @@ namespace Avalonia.Input { DispatcherTimer.RunOnce(() => { - if (s_gestureState != null && !token.IsCancellationRequested && e.Source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i))) + if (s_gestureState != null && !token.IsCancellationRequested && source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i))) { s_gestureState = new GestureState(GestureStateType.Holding, s_gestureState.Value.Pointer); i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e)); @@ -267,8 +274,8 @@ namespace Avalonia.Input else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { if (s_lastPress.TryGetTarget(out var target) && - target == e.Source && - e.Source is Interactive i) + target == source && + source is Interactive i) { s_gestureState = new GestureState(GestureStateType.DoubleTapped, e.Pointer); i.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); @@ -283,10 +290,12 @@ namespace Avalonia.Input { var e = (PointerReleasedEventArgs)ev; + var source = GetCaptured(ev); + if (s_lastPress.TryGetTarget(out var target) && - target == e.Source && - e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right && - e.Source is Interactive i) + target == source && + e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right && + source is Interactive i) { var point = e.GetCurrentPoint((Visual)target); var settings = ((IInputRoot?)i.GetVisualRoot())?.PlatformSettings; @@ -325,9 +334,10 @@ namespace Avalonia.Input if (ev.Route == RoutingStrategies.Bubble) { var e = (PointerEventArgs)ev; + var source = GetCaptured(e); if (s_lastPress.TryGetTarget(out var target)) { - if (e.Pointer == s_gestureState?.Pointer && ev.Source is Interactive i) + if (e.Pointer == s_gestureState?.Pointer && source is Interactive i) { var point = e.GetCurrentPoint((Visual)target); var settings = ((IInputRoot?)i.GetVisualRoot())?.PlatformSettings; diff --git a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs index a2afdd0af2..e0a35b9ab2 100644 --- a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs @@ -88,6 +88,40 @@ namespace Avalonia.Base.UnitTests.Input Assert.False(raised); } + [Fact] + public void Tapped_Should_Be_Raised_From_Captured_Control() + { + Border inner = new Border() + { + Focusable = true, + Name = "Inner" + }; + Border border = new Border() + { + Focusable = true, + Child = inner, + Name = "Parent" + }; + var root = new TestRoot + { + Child = border + }; + var raised = false; + + border.PointerPressed += (s, e) => + { + e.Pointer.Capture(inner); + }; + _mouse.Click(border, MouseButton.Left); + + root.AddHandler(Gestures.TappedEvent, (_, _) => raised = true); + + _mouse.Click(border, MouseButton.Left); + + + Assert.True(raised); + } + [Fact] public void RightTapped_Should_Be_Raised_For_Right_Button() { diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index e0d269083a..c963efedd0 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -104,7 +104,8 @@ namespace Avalonia.UnitTests Point position = default, KeyModifiers modifiers = default) { Down(target, source, button, position, modifiers); - Up(target, source, button, position, modifiers); + var captured = (_pointer.Captured as Interactive) ?? source; + Up(captured, captured, button, position, modifiers); } public void DoubleClick(Interactive target, MouseButton button = MouseButton.Left, Point position = default, @@ -115,7 +116,8 @@ namespace Avalonia.UnitTests Point position = default, KeyModifiers modifiers = default) { Down(target, source, button, position, modifiers, clickCount: 1); - Up(target, source, button, position, modifiers); + var captured = (_pointer.Captured as Interactive) ?? source; + Up(captured, captured, button, position, modifiers); Down(target, source, button, position, modifiers, clickCount: 2); }