Browse Source

Fix pointer capture cancellation on Win32 (#22054)

* Fix pointer capture cancellation on Win32

A cancelled pen or mouse interaction was reported as an ordinary leave,
and nothing released the capture that came with it, so the captured
element never saw PointerCaptureLost.

Win32 constants: POINTER_FLAG_CONFIDENCE and POINTER_FLAG_CANCELED were
declared an order of magnitude too small - the SDK defines them as
0x4000 and 0x8000 - so the cancellation check in GetEventType could
never match. Ctrl+Alt+Del or Win+L mid stroke therefore arrived as a
normal button up, and the app committed an interaction the user had
aborted.

Win32 handling: WM_POINTERCAPTURECHANGED and POINTER_FLAG_CANCELED now
map to CancelCapture for non touch pointers, matching what the legacy
WM_CAPTURECHANGED path has always done.

PenDevice: handle CancelCapture by releasing the capture while keeping
the pointer alive, since the pen is still in range and a leave arrives
separately.

Pointer hardening: CaptureLost ends every capture the pointer holds, on
the element and on a gesture recognizer, and the devices release through
it instead of repeating the steps. Capturing to a recognizer clears
Captured, so the null check in the platform path used to skip the whole
release while a scroll, pull or swipe gesture owned the pointer, and a
touch up never cleared the recognizer at all. Dispose releases as well
rather than leaving that to every caller, a disposed pointer ignores
further capture calls and asserts when a capturer is passed, and devices
drop a pointer from their map before disposing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep the explicit capture source in MouseTestHelper

The helper captures through Pointer.Capture(target), which is an
explicit capture, and used to release with Capture(null) - explicit as
well. Releasing as implicit changed the source that capture changing
handlers observe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
pull/22069/head
Nikita Tsukanov 1 month ago
committed by GitHub
parent
commit
a8738963cd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/Avalonia.Base/Input/MouseDevice.cs
  2. 9
      src/Avalonia.Base/Input/PenDevice.cs
  3. 53
      src/Avalonia.Base/Input/Pointer.cs
  4. 12
      src/Avalonia.Base/Input/TouchDevice.cs
  5. 4
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  6. 14
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  7. 4
      tests/Avalonia.UnitTests/MouseTestHelper.cs
  8. 4
      tests/Avalonia.UnitTests/TouchTestHelper.cs

4
src/Avalonia.Base/Input/MouseDevice.cs

@ -220,9 +220,7 @@ namespace Avalonia.Input
} }
finally finally
{ {
_pointer.Capture(null, CaptureSource.Implicit); _pointer.CaptureLost(CaptureSource.Implicit);
_pointer.CaptureGestureRecognizer(null);
_pointer.IsGestureRecognitionSkipped = false;
_lastMouseDownButton = default; _lastMouseDownButton = default;
} }
return e.Handled; return e.Handled;

9
src/Avalonia.Base/Input/PenDevice.cs

@ -63,6 +63,9 @@ namespace Avalonia.Input
case RawPointerEventType.LeaveWindow: case RawPointerEventType.LeaveWindow:
shouldReleasePointer = true; shouldReleasePointer = true;
break; break;
case RawPointerEventType.CancelCapture:
pointer.PlatformCaptureLost();
break;
case RawPointerEventType.LeftButtonDown: case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown: case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown: case RawPointerEventType.MiddleButtonDown:
@ -90,8 +93,8 @@ namespace Avalonia.Input
{ {
if (shouldReleasePointer) if (shouldReleasePointer)
{ {
pointer.Dispose();
_pointers.Remove(e.RawPointerId); _pointers.Remove(e.RawPointerId);
pointer.Dispose();
} }
} }
} }
@ -173,9 +176,7 @@ namespace Avalonia.Input
} }
finally finally
{ {
pointer.Capture(null, CaptureSource.Implicit); pointer.CaptureLost(CaptureSource.Implicit);
pointer.CaptureGestureRecognizer(null);
pointer.IsGestureRecognitionSkipped = false;
_lastMouseDownButton = default; _lastMouseDownButton = default;
} }

53
src/Avalonia.Base/Input/Pointer.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Avalonia.Input.GestureRecognizers; using Avalonia.Input.GestureRecognizers;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -18,6 +19,8 @@ namespace Avalonia.Input
private static int s_NextFreePointerId = 1000; private static int s_NextFreePointerId = 1000;
public static int GetNextFreeId() => s_NextFreePointerId++; public static int GetNextFreeId() => s_NextFreePointerId++;
private bool _disposed;
public Pointer(int id, PointerType type, bool isPrimary) public Pointer(int id, PointerType type, bool isPrimary)
{ {
Id = id; Id = id;
@ -40,10 +43,25 @@ namespace Avalonia.Input
} }
internal void PlatformCaptureLost() internal void PlatformCaptureLost() => CaptureLost(CaptureSource.Platform);
/// <summary>
/// Ends every capture the pointer holds, on the element and on a gesture recognizer.
/// </summary>
internal void CaptureLost(CaptureSource source)
{
if (_disposed)
return;
CaptureLostCore(source);
}
private void CaptureLostCore(CaptureSource source)
{ {
if (Captured != null) if (Captured != null)
Capture(null, CaptureSource.Platform); CaptureCore(null, source);
CaptureGestureRecognizerCore(null);
IsGestureRecognitionSkipped = false;
} }
public void Capture(IInputElement? control) public void Capture(IInputElement? control)
@ -52,6 +70,17 @@ namespace Avalonia.Input
} }
internal void Capture(IInputElement? control, CaptureSource source) 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 oldCapture = Captured;
var oldSource = CaptureSource; var oldSource = CaptureSource;
@ -148,7 +177,14 @@ namespace Avalonia.Input
public void Dispose() 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);
} }
/// <summary> /// <summary>
@ -156,6 +192,17 @@ namespace Avalonia.Input
/// </summary> /// </summary>
/// <param name="gestureRecognizer">The gesture recognizer.</param> /// <param name="gestureRecognizer">The gesture recognizer.</param>
internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer) 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) if (CapturedGestureRecognizer != gestureRecognizer)
{ {

12
src/Avalonia.Base/Input/TouchDevice.cs

@ -109,20 +109,14 @@ namespace Avalonia.Input
{ {
target.RaiseEvent(e); target.RaiseEvent(e);
} }
pointer?.Capture(null, CaptureSource.Implicit); pointer?.CaptureLost(CaptureSource.Implicit);
} }
} }
if (args.Type == RawPointerEventType.TouchCancel) if (args.Type == RawPointerEventType.TouchCancel)
{ {
_pointers.Remove(args.RawPointerId); _pointers.Remove(args.RawPointerId);
using (pointer) pointer?.Dispose();
{
pointer?.Capture(null, CaptureSource.Platform);
pointer?.CaptureGestureRecognizer(null);
if (pointer != null)
pointer.IsGestureRecognitionSkipped = false;
}
} }
if (args.Type == RawPointerEventType.TouchUpdate) if (args.Type == RawPointerEventType.TouchUpdate)
@ -165,7 +159,7 @@ namespace Avalonia.Input
internal void PlatformCaptureLost() internal void PlatformCaptureLost()
{ {
foreach (var pointer in _pointers.Values) foreach (var pointer in _pointers.Values)
pointer.Capture(null); pointer.PlatformCaptureLost();
} }
} }
} }

4
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -987,8 +987,8 @@ namespace Avalonia.Win32.Interop
POINTER_FLAG_FOURTHBUTTON = 0x00000080, POINTER_FLAG_FOURTHBUTTON = 0x00000080,
POINTER_FLAG_FIFTHBUTTON = 0x00000100, POINTER_FLAG_FIFTHBUTTON = 0x00000100,
POINTER_FLAG_PRIMARY = 0x00002000, POINTER_FLAG_PRIMARY = 0x00002000,
POINTER_FLAG_CONFIDENCE = 0x00000400, POINTER_FLAG_CONFIDENCE = 0x00004000,
POINTER_FLAG_CANCELED = 0x00000800, POINTER_FLAG_CANCELED = 0x00008000,
POINTER_FLAG_DOWN = 0x00010000, POINTER_FLAG_DOWN = 0x00010000,
POINTER_FLAG_UPDATE = 0x00020000, POINTER_FLAG_UPDATE = 0x00020000,
POINTER_FLAG_UP = 0x00040000, POINTER_FLAG_UP = 0x00040000,

14
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -583,7 +583,6 @@ namespace Avalonia.Win32
break; break;
} }
case WindowsMessage.WM_POINTERLEAVE: case WindowsMessage.WM_POINTERLEAVE:
case WindowsMessage.WM_POINTERCAPTURECHANGED:
{ {
if (!_wmPointerEnabled) if (!_wmPointerEnabled)
{ {
@ -594,6 +593,17 @@ namespace Avalonia.Win32
e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId); e = CreatePointerArgs(device, timestamp, eventType, point, modifiers, info.pointerId);
break; 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_POINTERWHEEL:
case WindowsMessage.WM_POINTERHWHEEL: case WindowsMessage.WM_POINTERHWHEEL:
{ {
@ -1300,7 +1310,7 @@ namespace Avalonia.Win32
var isTouch = info.pointerType == PointerInputType.PT_TOUCH; var isTouch = info.pointerType == PointerInputType.PT_TOUCH;
if (info.pointerFlags.HasFlag(PointerFlags.POINTER_FLAG_CANCELED)) 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); var eventType = ToEventType(info.ButtonChangeType, isTouch);

4
tests/Avalonia.UnitTests/MouseTestHelper.cs

@ -101,9 +101,7 @@ namespace Avalonia.UnitTests
else else
target.RaiseEvent(e); target.RaiseEvent(e);
_pointer.Capture(null); _pointer.CaptureLost(CaptureSource.Explicit);
_pointer.CaptureGestureRecognizer(null);
_pointer.IsGestureRecognitionSkipped = false;
} }
else else
Move(target, source, position ?? default); Move(target, source, position ?? default);

4
tests/Avalonia.UnitTests/TouchTestHelper.cs

@ -64,9 +64,7 @@ namespace Avalonia.UnitTests
public void Cancel() public void Cancel()
{ {
_pointer.Capture(null); _pointer.CaptureLost(CaptureSource.Platform);
_pointer.CaptureGestureRecognizer(null);
_pointer.IsGestureRecognitionSkipped = false;
} }
} }
} }

Loading…
Cancel
Save