From 73ab31fcce5942eb43b93eb294996531b128989c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 28 Aug 2026 12:20:35 +0000 Subject: [PATCH] Use _effective_ capturer (which can be recognizer) for PlatformCapture (#22076) --- src/Avalonia.Base/Input/Pointer.cs | 46 +++++++--------- .../Input/PointerTests.cs | 53 +++++++++++++++++++ .../Input/PointerTestsBase.cs | 3 ++ 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index 47aab499ce..71a5372a62 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -58,9 +58,7 @@ namespace Avalonia.Input private void CaptureLostCore(CaptureSource source) { - if (Captured != null) - CaptureCore(null, source); - CaptureGestureRecognizerCore(null); + CaptureCore(null, null, source); IsGestureRecognitionSkipped = false; } @@ -69,6 +67,8 @@ namespace Avalonia.Input Capture(control, CaptureSource.Explicit); } + private IInputElement? EffectiveCapturer => this.CapturedGestureRecognizer?.Target ?? Captured; + internal void Capture(IInputElement? control, CaptureSource source) { if (_disposed) @@ -77,16 +77,21 @@ namespace Avalonia.Input return; } - CaptureCore(control, source); + CaptureCore(control, null, source); } - private void CaptureCore(IInputElement? control, CaptureSource source) + private void CaptureCore( + IInputElement? control, + GestureRecognizer? gestureRecognizer, + CaptureSource source) { var oldCapture = Captured; + var oldGestureRecognizer = CapturedGestureRecognizer; var oldSource = CaptureSource; + var oldEffectiveCapturer = EffectiveCapturer; // If a handler marks Implicit capture as handled, we still want them to have another chance if the element is captured explicitly. - if (oldCapture == control && oldSource == source) + if (oldCapture == control && oldGestureRecognizer == gestureRecognizer && oldSource == source) return; var oldVisual = oldCapture as Visual; @@ -110,12 +115,17 @@ namespace Avalonia.Input if (oldVisual != null) oldVisual.DetachedFromVisualTree -= OnCaptureDetached; + + if (oldGestureRecognizer != gestureRecognizer) + oldGestureRecognizer?.PointerCaptureLostInternal(this); + Captured = control; + CapturedGestureRecognizer = gestureRecognizer; CaptureSource = source; // However, we still want to notify the platform only if the captured element actually changed. - if (oldCapture != control && source != CaptureSource.Platform) - PlatformCapture(control); + if (oldEffectiveCapturer != EffectiveCapturer && source != CaptureSource.Platform) + PlatformCapture(EffectiveCapturer); if (oldVisual != null) foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType()) @@ -128,9 +138,6 @@ namespace Avalonia.Input if (newVisual != null) newVisual.DetachedFromVisualTree += OnCaptureDetached; - if (Captured != null) - CaptureGestureRecognizer(null); - if (Captured == null && CapturedGestureRecognizer == null) { IsGestureRecognitionSkipped = false; @@ -199,22 +206,7 @@ namespace Avalonia.Input return; } - CaptureGestureRecognizerCore(gestureRecognizer); - } - - private void CaptureGestureRecognizerCore(GestureRecognizer? gestureRecognizer) - { - if (CapturedGestureRecognizer != gestureRecognizer) - { - CapturedGestureRecognizer?.PointerCaptureLostInternal(this); - } - - CapturedGestureRecognizer = gestureRecognizer; - - if (gestureRecognizer != null) - { - Capture(null); - } + CaptureCore(null, gestureRecognizer, CaptureSource.Explicit); } } } diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs index dfc081ad31..e96552d542 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -54,6 +55,37 @@ namespace Avalonia.Base.UnitTests.Input Assert.Equal(2, pointer.PlatformCaptureCalled); } + [Fact] + public void Gesture_Recognizer_Capture_Should_Keep_Platform_Capture_On_Same_Target() + { + var pointer = new TestPointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + var target = new Border(); + var recognizer = new TestGestureRecognizer { Target = target }; + + pointer.Capture(target, CaptureSource.Implicit); + recognizer.CapturePointer(pointer); + + Assert.Null(pointer.Captured); + Assert.Same(recognizer, pointer.CapturedGestureRecognizer); + Assert.Equal([target], pointer.PlatformCaptures); + } + + [Fact] + public void Gesture_Recognizer_Capture_Should_Move_Platform_Capture_To_Target() + { + var pointer = new TestPointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + var initialCapture = new Border(); + var target = new Border { Child = initialCapture }; + var recognizer = new TestGestureRecognizer { Target = target }; + + pointer.Capture(initialCapture, CaptureSource.Implicit); + recognizer.CapturePointer(pointer); + + Assert.Null(pointer.Captured); + Assert.Same(recognizer, pointer.CapturedGestureRecognizer); + Assert.Equal([initialCapture, target], pointer.PlatformCaptures); + } + [Fact] public void Capture_Explicit_ShouldNotify_After_Implicit() { @@ -109,5 +141,26 @@ namespace Avalonia.Base.UnitTests.Input Assert.Equal(2, pointer.PlatformCaptureCalled); } + + private sealed class TestGestureRecognizer : GestureRecognizer + { + public void CapturePointer(IPointer pointer) => Capture(pointer); + + protected override void PointerPressed(PointerPressedEventArgs e) + { + } + + protected override void PointerReleased(PointerReleasedEventArgs e) + { + } + + protected override void PointerMoved(PointerEventArgs e) + { + } + + protected override void PointerCaptureLost(IPointer pointer) + { + } + } } } diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs index 1675dc9e62..401a75dc6a 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; @@ -17,12 +18,14 @@ public abstract class PointerTestsBase : ScopedTestBase protected class TestPointer : Pointer { internal int PlatformCaptureCalled = 0; + internal List PlatformCaptures { get; } = new(); internal TestPointer(int id, PointerType type, bool isPrimary) : base(id, type, isPrimary) { } protected override void PlatformCapture(IInputElement? element) { PlatformCaptureCalled++; + PlatformCaptures.Add(element); } }