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
{
_pointer.Capture(null, CaptureSource.Implicit);
_pointer.CaptureGestureRecognizer(null);
_pointer.IsGestureRecognitionSkipped = false;
_pointer.CaptureLost(CaptureSource.Implicit);
_lastMouseDownButton = default;
}
return e.Handled;

9
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;
}

53
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);
/// <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)
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);
}
/// <summary>
@ -156,6 +192,17 @@ namespace Avalonia.Input
/// </summary>
/// <param name="gestureRecognizer">The gesture recognizer.</param>
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)
{

12
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();
}
}
}

4
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,

14
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);

4
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);

4
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);
}
}
}

Loading…
Cancel
Save