diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 6bb5733a19..340a006ed1 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -220,9 +220,7 @@ namespace Avalonia.Input } finally { - _pointer.Capture(null, CaptureSource.Implicit); - _pointer.CaptureGestureRecognizer(null); - _pointer.IsGestureRecognitionSkipped = false; + _pointer.CaptureLost(CaptureSource.Implicit); _lastMouseDownButton = default; } return e.Handled; diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index f7211ea351..b7dba977d5 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -63,6 +63,9 @@ namespace Avalonia.Input case RawPointerEventType.LeaveWindow: shouldReleasePointer = true; break; + case RawPointerEventType.CancelCapture: + pointer.PlatformCaptureLost(); + break; case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: case RawPointerEventType.MiddleButtonDown: @@ -90,8 +93,8 @@ namespace Avalonia.Input { if (shouldReleasePointer) { - pointer.Dispose(); _pointers.Remove(e.RawPointerId); + pointer.Dispose(); } } } @@ -173,9 +176,7 @@ namespace Avalonia.Input } finally { - pointer.Capture(null, CaptureSource.Implicit); - pointer.CaptureGestureRecognizer(null); - pointer.IsGestureRecognitionSkipped = false; + pointer.CaptureLost(CaptureSource.Implicit); _lastMouseDownButton = default; } diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index 9b751baf12..47aab499ce 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Avalonia.Input.GestureRecognizers; using Avalonia.VisualTree; @@ -18,6 +19,8 @@ namespace Avalonia.Input private static int s_NextFreePointerId = 1000; public static int GetNextFreeId() => s_NextFreePointerId++; + private bool _disposed; + public Pointer(int id, PointerType type, bool isPrimary) { Id = id; @@ -40,10 +43,25 @@ namespace Avalonia.Input } - internal void PlatformCaptureLost() + internal void PlatformCaptureLost() => CaptureLost(CaptureSource.Platform); + + /// + /// Ends every capture the pointer holds, on the element and on a gesture recognizer. + /// + internal void CaptureLost(CaptureSource source) + { + if (_disposed) + return; + + CaptureLostCore(source); + } + + private void CaptureLostCore(CaptureSource source) { if (Captured != null) - Capture(null, CaptureSource.Platform); + CaptureCore(null, source); + CaptureGestureRecognizerCore(null); + IsGestureRecognitionSkipped = false; } public void Capture(IInputElement? control) @@ -52,6 +70,17 @@ namespace Avalonia.Input } internal void Capture(IInputElement? control, CaptureSource source) + { + if (_disposed) + { + Debug.Assert(control is null, "Capturing a pointer that no longer exists."); + return; + } + + CaptureCore(control, source); + } + + private void CaptureCore(IInputElement? control, CaptureSource source) { var oldCapture = Captured; var oldSource = CaptureSource; @@ -148,7 +177,14 @@ namespace Avalonia.Input public void Dispose() { - // callers are responsible for calling Capture(null, source) with an appropriate source + if (_disposed) + return; + + // Mark the pointer gone first, so a capture lost handler can't capture it again. + // It no longer exists, so the platform is the only source the release can come from. + _disposed = true; + + CaptureLostCore(CaptureSource.Platform); } /// @@ -156,6 +192,17 @@ namespace Avalonia.Input /// /// The gesture recognizer. internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer) + { + if (_disposed) + { + Debug.Assert(gestureRecognizer is null, "Capturing a pointer that no longer exists to a gesture recognizer."); + return; + } + + CaptureGestureRecognizerCore(gestureRecognizer); + } + + private void CaptureGestureRecognizerCore(GestureRecognizer? gestureRecognizer) { if (CapturedGestureRecognizer != gestureRecognizer) { diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index 6429d9c0fc..dac12dedd8 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/src/Avalonia.Base/Input/TouchDevice.cs @@ -109,20 +109,14 @@ namespace Avalonia.Input { target.RaiseEvent(e); } - pointer?.Capture(null, CaptureSource.Implicit); + pointer?.CaptureLost(CaptureSource.Implicit); } } if (args.Type == RawPointerEventType.TouchCancel) { _pointers.Remove(args.RawPointerId); - using (pointer) - { - pointer?.Capture(null, CaptureSource.Platform); - pointer?.CaptureGestureRecognizer(null); - if (pointer != null) - pointer.IsGestureRecognitionSkipped = false; - } + pointer?.Dispose(); } if (args.Type == RawPointerEventType.TouchUpdate) @@ -165,7 +159,7 @@ namespace Avalonia.Input internal void PlatformCaptureLost() { foreach (var pointer in _pointers.Values) - pointer.Capture(null); + pointer.PlatformCaptureLost(); } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 18aa162608..0d3ed14bdc 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -987,8 +987,8 @@ namespace Avalonia.Win32.Interop POINTER_FLAG_FOURTHBUTTON = 0x00000080, POINTER_FLAG_FIFTHBUTTON = 0x00000100, POINTER_FLAG_PRIMARY = 0x00002000, - POINTER_FLAG_CONFIDENCE = 0x00000400, - POINTER_FLAG_CANCELED = 0x00000800, + POINTER_FLAG_CONFIDENCE = 0x00004000, + POINTER_FLAG_CANCELED = 0x00008000, POINTER_FLAG_DOWN = 0x00010000, POINTER_FLAG_UPDATE = 0x00020000, POINTER_FLAG_UP = 0x00040000, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 711481d3d5..a8dbe219d2 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -583,7 +583,6 @@ namespace Avalonia.Win32 break; } case WindowsMessage.WM_POINTERLEAVE: - case WindowsMessage.WM_POINTERCAPTURECHANGED: { if (!_wmPointerEnabled) { @@ -594,6 +593,17 @@ namespace Avalonia.Win32 e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); break; } + case WindowsMessage.WM_POINTERCAPTURECHANGED: + { + if (!_wmPointerEnabled) + { + break; + } + GetDevicePointerInfo(wParam, out var device, out var info, out var point, out var modifiers, ref timestamp); + var eventType = device is TouchDevice ? RawPointerEventType.TouchCancel : RawPointerEventType.CancelCapture; + e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); + break; + } case WindowsMessage.WM_POINTERWHEEL: case WindowsMessage.WM_POINTERHWHEEL: { @@ -1300,7 +1310,7 @@ namespace Avalonia.Win32 var isTouch = info.pointerType == PointerInputType.PT_TOUCH; if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED)) { - return isTouch ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; + return isTouch ? RawPointerEventType.TouchCancel : RawPointerEventType.CancelCapture; } var eventType = ToEventType(info.ButtonChangeType, isTouch); diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index 082ae718f3..b8eeb2aa91 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -101,9 +101,7 @@ namespace Avalonia.UnitTests else target.RaiseEvent(e); - _pointer.Capture(null); - _pointer.CaptureGestureRecognizer(null); - _pointer.IsGestureRecognitionSkipped = false; + _pointer.CaptureLost(CaptureSource.Explicit); } else Move(target, source, position ?? default); diff --git a/tests/Avalonia.UnitTests/TouchTestHelper.cs b/tests/Avalonia.UnitTests/TouchTestHelper.cs index 7a13425997..73909cdd79 100644 --- a/tests/Avalonia.UnitTests/TouchTestHelper.cs +++ b/tests/Avalonia.UnitTests/TouchTestHelper.cs @@ -64,9 +64,7 @@ namespace Avalonia.UnitTests public void Cancel() { - _pointer.Capture(null); - _pointer.CaptureGestureRecognizer(null); - _pointer.IsGestureRecognitionSkipped = false; + _pointer.CaptureLost(CaptureSource.Platform); } } }