From b08c7e9ebdb6494a67a1dd4cfa55baf61d642fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ku=C4=8Dera?= <10546952+miloush@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:42:31 +0000 Subject: [PATCH] Capture source change notification (#20656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fire CaptureChanging event when source changes * Do not notfiy platform if only source changed * Notify ancestors of element to be captured if none yet * Pointer capture notify on source change tests --------- Co-authored-by: Jan Kučera --- src/Avalonia.Base/Input/Pointer.cs | 20 ++++--- .../Input/PointerTests.cs | 56 +++++++++++++++++++ 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index 94643fa91e..ebb98dd0bc 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -54,23 +54,28 @@ namespace Avalonia.Input internal void Capture(IInputElement? control, CaptureSource source) { var oldCapture = Captured; - if (oldCapture == control) + var oldSource = CaptureSource; + + // 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) return; var oldVisual = oldCapture as Visual; + var newVisual = control as Visual; IInputElement? commonParent = null; - if (oldVisual != null) + if (oldVisual != null || newVisual != null) { commonParent = FindCommonParent(control, oldCapture); - foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType()) + var visual = oldVisual ?? newVisual!; // We want the capture to be cancellable even if there is no currently captured element. + foreach (var notifyTarget in visual.GetSelfAndVisualAncestors().OfType()) { - if (notifyTarget == commonParent) - break; var args = new PointerCaptureChangingEventArgs(notifyTarget, this, control, source); notifyTarget.RaiseEvent(args); if (args.Handled) return; + if (notifyTarget == commonParent) + break; } } @@ -79,7 +84,8 @@ namespace Avalonia.Input Captured = control; CaptureSource = source; - if (source != CaptureSource.Platform) + // However, we still want to notify the platform only if the captured element actually changed. + if (oldCapture != control && source != CaptureSource.Platform) PlatformCapture(control); if (oldVisual != null) @@ -90,7 +96,7 @@ namespace Avalonia.Input notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this)); } - if (Captured is Visual newVisual) + if (newVisual != null) newVisual.DetachedFromVisualTree += OnCaptureDetached; if (Captured != null) diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs index 912ce5b6a6..4e6eb51b58 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerTests.cs @@ -53,5 +53,61 @@ namespace Avalonia.Base.UnitTests.Input Assert.Equal(2, pointer.PlatformCaptureCalled); } + + [Fact] + public void Capture_Explicit_ShouldNotify_After_Implicit() + { + var pointer = new TestPointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + + Border capture = new Border(); + + List sources = new(); + capture.PointerCaptureChanging += (sender, e) => + { + sources.Add(e.CaptureSource); + }; + + pointer.Capture(capture, CaptureSource.Implicit); + pointer.Capture(capture, CaptureSource.Explicit); + + Assert.True(sources.SequenceEqual([CaptureSource.Implicit, CaptureSource.Explicit])); + + Assert.Equal(1, pointer.PlatformCaptureCalled); + + pointer.Capture(null, CaptureSource.Implicit); // not ignored, so captured element will become null + pointer.Capture(null, CaptureSource.Explicit); // changing from null to null does not notify anything + + Assert.True(sources.SequenceEqual([CaptureSource.Implicit, CaptureSource.Explicit, CaptureSource.Implicit])); + + Assert.Equal(2, pointer.PlatformCaptureCalled); + } + + [Fact] + public void Capture_Explicit_ShouldNotify_After_HandledImplicit() + { + var pointer = new TestPointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + + Border capture = new Border(); + + List sources = new(); + capture.PointerCaptureChanging += (sender, e) => + { + sources.Add(e.CaptureSource); + e.Handled = e.CaptureSource == CaptureSource.Implicit; + }; + + pointer.Capture(capture, CaptureSource.Implicit); + pointer.Capture(capture, CaptureSource.Explicit); + + Assert.True(sources.SequenceEqual([CaptureSource.Implicit, CaptureSource.Explicit])); + + Assert.Equal(1, pointer.PlatformCaptureCalled); + + pointer.Capture(null, CaptureSource.Implicit); + pointer.Capture(null, CaptureSource.Explicit); + Assert.True(sources.SequenceEqual([CaptureSource.Implicit, CaptureSource.Explicit, CaptureSource.Implicit, CaptureSource.Explicit])); + + Assert.Equal(2, pointer.PlatformCaptureCalled); + } } }