Skip to content

Windows: Fix PanGestureRecognizer not starting when drag begins near control edge#34362

Merged
kubaflo merged 1 commit intodotnet:inflight/currentfrom
jpd21122012:fix/34119-PanGestureRecognizer-Windows
Mar 22, 2026
Merged

Windows: Fix PanGestureRecognizer not starting when drag begins near control edge#34362
kubaflo merged 1 commit intodotnet:inflight/currentfrom
jpd21122012:fix/34119-PanGestureRecognizer-Windows

Conversation

@jpd21122012
Copy link
Copy Markdown
Contributor

Fixes #34119

When dragging from the edge of a control on Windows, PointerExited can fire before the manipulation begins.

In this scenario the pointer ID is removed from the _fingers collection before HandlePan runs, resulting in _fingers.Count == 0. Since PanGestureRecognizer typically expects TouchPoints == 1, the recognizer never matches and PanUpdated is not raised.

This change prevents removing the pointer prematurely when a gesture interaction has not fully completed, ensuring _fingers remains consistent until the gesture lifecycle finishes.

After this change, dragging from the edge of a control correctly triggers PanUpdated as expected.

Tested

Reproduced using a simple AbsoluteLayout with a draggable view and PanGestureRecognizer.

Before fix:

  • Starting the drag from the edge of the control sometimes prevented PanUpdated from firing.

After fix:

  • PanUpdated fires consistently when dragging from any part of the control, including edges.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 6, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34362

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34362"

@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Mar 6, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Hey there @@jpd21122012! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@jpd21122012
Copy link
Copy Markdown
Contributor Author

Hi, just checking if this PR could be reviewed for the upcoming 10.6 servicing release.

This fix addresses an issue on Windows where PanGestureRecognizer does not start when the drag begins near the control edge, leading to inconsistent gesture behavior.

The change is minimal and scoped to gesture handling, without affecting public APIs, so it should be safe for servicing.

Let me know if any adjustments or additional validation are needed 👍

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 22, 2026

🤖 AI Summary

📊 Expand Full Review4752c21 · Fix PanGestureRecognizer not starting when drag begins near control edge on Windows
🔍 Pre-Flight — Context & Validation

Issue: #34119 - PanGestureRecognizer PanUPdated not firing when mouse cursor is on the first pixel of control
PR: #34362 - Windows: Fix PanGestureRecognizer not starting when drag begins near control edge
Platforms Affected: Windows
Files Changed: 1 implementation, 0 test

Key Findings

  • The linked issue is Windows-only and describes PointerExited firing before pan handling when a drag starts on the edge of a control, which clears _fingers too early.
  • The PR makes a one-line change in src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs, expanding the guard in OnPointerExited so the pointer ID is not removed while pan, pinch, or swipe state is still active.
  • No test files were changed in the PR, so Gate will likely be blocked or skipped unless existing coverage for this scenario can be identified and verified.
  • Issue validation comment confirms reproduction on Windows across multiple MAUI versions; no PR review threads or prior agent review output were found.

Edge Cases Mentioned

  • Dragging from the first few pixels near the edge/top-left border of the draggable view can suppress PanUpdated.
  • Dragging from the interior/center of the same view works normally.
  • Reporter workaround attaches multiple PanGestureRecognizer instances with different TouchPoints, indicating the bug is tied to pointer counting rather than layout.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34362 Prevent OnPointerExited from removing the active pointer while pan/pinch/swipe state is still in progress. PENDING (Gate) src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs Original PR; implementation-only change

🚦 Gate — Test Verification

Gate Result: ⚠️ SKIPPED

Platform: Windows
Mode: No Tests Available
PR: #34362 — Windows: Fix PanGestureRecognizer not starting when drag begins near control edge
Related Issue: #34119


Verification Status


Blocker: Test Coverage Missing

Reason for Skip:

PR #34362 adds a code fix to GesturePlatformManager.Windows.cs (preventing premature pointer removal from _fingers collection during gesture interactions) but no tests were added to verify this fix.

A comprehensive search of the test suite revealed:

Test Search Results:


Recommendation

⚠️ Before merging, add tests for issue #34119:

The fix should be validated with UI tests that reproduce the edge-case scenario:

  1. Create a draggable view with PanGestureRecognizer in an AbsoluteLayout
  2. Test dragging from the edge (first pixels) of the control
  3. Verify PanUpdated fires (currently fails without the fix)
  4. Verify PanUpdated fires after the fix

Suggested: Use write-tests-agent skill to create UI tests before merging.


PR Analysis

Changed Files: 1

  • src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs (+1/-1)

Change Summary:

// Before:
if (!_isPanning)
    _fingers.Remove(e.Pointer.PointerId);

// After:
if (!_isPanning && !_isPinching && !_isSwiping)
    _fingers.Remove(e.Pointer.PointerId);

Logic: Prevents removing the pointer ID from _fingers collection prematurely when a gesture interaction is still active (panning, pinching, or swiping). This ensures _fingers.Count matches expected TouchPoints when HandlePan is called.


Next Steps


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Normalize pan finger matching with Math.Max(1, _fingers.Count) in HandlePan/PanComplete so a premature PointerExited does not prevent TouchPoints == 1 recognition. Blocked 1 file Compiled, but BuildAndRunHostApp.ps1 -TestFilter "Issue34119" found 0 matching tests. Compensates at read sites rather than preserving state.
2 try-fix Snapshot touch-point count in OnManipulationStarted and use the snapshot for pan matching/completion instead of live _fingers.Count. Blocked 1 file Compiled, but 0 tests matched Issue34119. More targeted than attempt 1, but introduces extra state.
3 try-fix Gate pointer removal in OnPointerExited on !e.Pointer.IsInContact rather than gesture-state flags. Blocked 1 file Validation command ran, but no Issue34119 test exists. Simpler than attempt 2, but depends on contact-state semantics instead of gesture lifecycle.
4 try-fix Add a fallback dummy pointer in OnManipulationStarted when _fingers.Count == 0. Blocked 1 file No matching automated test. Also least attractive because it mutates pointer state artificially.
5 try-fix Never remove the pointer in OnPointerExited; defer cleanup to release/cancel/completion. Blocked 1 file Compiled and is simpler than the PR, but may retain stale pointers for true hover/exit paths until later lifecycle callbacks.
6 try-fix Capture the pointer in OnPointerPressed and clean up on PointerCaptureLost / release. Blocked 1 file Input-layer fix that likely suppresses the bad exit, but adds pointer-capture lifecycle complexity.
7 try-fix Track _pendingGesturePointerIds from press until first manipulation callback and skip OnPointerExited removal for pending IDs. Blocked 1 file Highly targeted, but adds bookkeeping for a transient race condition.
8 try-fix Delay OnPointerExited removal through DispatcherQueue.TryEnqueue so manipulation flags can initialize first. Blocked 1 file Works around event ordering, but timing-dependent dispatch is brittle compared with direct state management.
9 try-fix Clamp _fingers.Count to at least 1 only while manipulation is active. Blocked 1 file Narrower than attempt 1, but still compensates after state becomes inconsistent instead of preventing the inconsistency.
10 try-fix Remove pointers on PointerExited only for mouse devices, keeping touch/pen contacts until explicit end events. Blocked 0 files Could not be attempted because the designated model hit a 429 rate limit twice; skipped per workflow blocker policy.
PR PR #34362 Prevent pointer removal in OnPointerExited while pan, pinch, or swipe state is still active. Unverified 1 file Original PR; no automated Issue34119 coverage exists in repo. Preserves pointer state only while a gesture is active.

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 1 Yes Read-site compensation using effective finger count for pan lifecycle matching.
claude-sonnet-4.6 1 Yes Capture touch-point snapshot at manipulation start and reuse it through completion.
gpt-5.3-codex 1 Yes Use pointer contact state to decide whether PointerExited may remove the pointer ID.
gemini-3-pro-preview 1 Yes Restore a lost pointer at manipulation start if _fingers is already empty.
claude-opus-4.6 2 Yes Defer pointer cleanup entirely out of OnPointerExited.
claude-sonnet-4.6 2 Yes Use pointer capture to prevent the premature PointerExited.
gpt-5.3-codex 2 Yes Track pending pressed pointers until the first manipulation callback.
gemini-3-pro-preview 2 No NO NEW IDEAS
claude-opus-4.6 3 Yes Clamp only while manipulation is active.
claude-sonnet-4.6 3 Yes Keep touch/pen pointers through exit; remove mouse pointers only.
gpt-5.3-codex 3 Yes Skip exit removal when geometry shows the pointer is still effectively inside bounds.
gemini-3-pro-preview 3 No NO NEW IDEAS

Exhausted: No
Selected Fix: Pending final geometry-check attempt; max cross-pollination rounds reached


📋 Report — Final Recommendation

Recommendation

⚠️ Before merging, add tests for issue #34119:

The fix should be validated with UI tests that reproduce the edge-case scenario:

  1. Create a draggable view with PanGestureRecognizer in an AbsoluteLayout
  2. Test dragging from the edge (first pixels) of the control
  3. Verify PanUpdated fires (currently fails without the fix)
  4. Verify PanUpdated fires after the fix

Suggested: Use write-tests-agent skill to create UI tests before merging.


@MauiBot MauiBot added s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 22, 2026
@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Mar 22, 2026

📋 PR Finalization Review

Title: ✅ Good

Current: Windows: Fix PanGestureRecognizer not starting when drag begins near control edge

Description: ⚠️ Needs Update

The description explains the root cause and fix well, but is missing the required NOTE block for testing PR artifacts. The technical explanation is accurate and matches the implementation.
Missing Elements:

  • Missing NOTE block at top (required for all PRs so users can test artifacts)

✨ Suggested PR Description

[!NOTE]
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description of Change

When dragging from the edge of a control on Windows, PointerExited can fire before the manipulation begins. The pointer ID is removed from the _fingers collection before HandlePan runs, resulting in _fingers.Count == 0. Since PanGestureRecognizer expects TouchPoints == 1, the recognizer never matches and PanUpdated is not raised.

This change extends the guard in OnPointerExited to also check _isPinching and _isSwiping, preventing premature pointer removal when any gesture interaction is active.

Root Cause

OnManipulationStarted sets _isPinching = true for ALL manipulation types, but the old guard in OnPointerExited only checked _isPanning. If PointerExited fires between OnManipulationStarted and HandlePan/HandleSwipe, the pointer was removed despite an active gesture.

Issues Fixed

Fixes #34119

Code Review: ✅ Passed

Looks Good

  • Minimal, surgical change: exactly one line modified in GesturePlatformManager.Windows.cs
  • Correct fix: The !_isPinching guard catches the critical window between OnManipulationStarted (sets _isPinching = true) and HandlePan/HandleSwipe (sets _isPanning/_isSwiping = true), preventing premature pointer removal
  • Consistent with lifecycle: OnPointerCanceled and OnManipulationCompleted already handle all three gesture types; this aligns OnPointerExited with the same pattern
  • No leak risk: Pointer is always cleaned up by OnManipulationCompleted, OnPointerReleased, or OnPointerCanceled

Minor Observation (Not a Blocker)

  • The !_isSwiping guard is technically redundant in the current code path since SwipeComplete(true) is called immediately before the condition and always resets _isSwiping = false. However, this is good defensive programming - it makes the condition symmetric and self-documenting.

@kubaflo kubaflo changed the base branch from main to inflight/current March 22, 2026 16:23
@kubaflo kubaflo merged commit db0792d into dotnet:inflight/current Mar 22, 2026
28 of 31 checks passed
PureWeen pushed a commit that referenced this pull request Mar 24, 2026
…control edge (#34362)

Fixes #34119

When dragging from the edge of a control on Windows, `PointerExited` can
fire before the manipulation begins.

In this scenario the pointer ID is removed from the `_fingers`
collection before `HandlePan` runs, resulting in `_fingers.Count == 0`.
Since `PanGestureRecognizer` typically expects `TouchPoints == 1`, the
recognizer never matches and `PanUpdated` is not raised.

This change prevents removing the pointer prematurely when a gesture
interaction has not fully completed, ensuring `_fingers` remains
consistent until the gesture lifecycle finishes.

After this change, dragging from the edge of a control correctly
triggers `PanUpdated` as expected.

### Tested

Reproduced using a simple `AbsoluteLayout` with a draggable view and
`PanGestureRecognizer`.

Before fix:
- Starting the drag from the edge of the control sometimes prevented
`PanUpdated` from firing.

After fix:
- `PanUpdated` fires consistently when dragging from any part of the
control, including edges.
KarthikRajaKalaimani pushed a commit to KarthikRajaKalaimani/maui that referenced this pull request Mar 30, 2026
…control edge (dotnet#34362)

Fixes dotnet#34119

When dragging from the edge of a control on Windows, `PointerExited` can
fire before the manipulation begins.

In this scenario the pointer ID is removed from the `_fingers`
collection before `HandlePan` runs, resulting in `_fingers.Count == 0`.
Since `PanGestureRecognizer` typically expects `TouchPoints == 1`, the
recognizer never matches and `PanUpdated` is not raised.

This change prevents removing the pointer prematurely when a gesture
interaction has not fully completed, ensuring `_fingers` remains
consistent until the gesture lifecycle finishes.

After this change, dragging from the edge of a control correctly
triggers `PanUpdated` as expected.

### Tested

Reproduced using a simple `AbsoluteLayout` with a draggable view and
`PanGestureRecognizer`.

Before fix:
- Starting the drag from the edge of the control sometimes prevented
`PanUpdated` from firing.

After fix:
- `PanUpdated` fires consistently when dragging from any part of the
control, including edges.
sheiksyedm pushed a commit that referenced this pull request Apr 4, 2026
…control edge (#34362)

Fixes #34119

When dragging from the edge of a control on Windows, `PointerExited` can
fire before the manipulation begins.

In this scenario the pointer ID is removed from the `_fingers`
collection before `HandlePan` runs, resulting in `_fingers.Count == 0`.
Since `PanGestureRecognizer` typically expects `TouchPoints == 1`, the
recognizer never matches and `PanUpdated` is not raised.

This change prevents removing the pointer prematurely when a gesture
interaction has not fully completed, ensuring `_fingers` remains
consistent until the gesture lifecycle finishes.

After this change, dragging from the edge of a control correctly
triggers `PanUpdated` as expected.

### Tested

Reproduced using a simple `AbsoluteLayout` with a draggable view and
`PanGestureRecognizer`.

Before fix:
- Starting the drag from the edge of the control sometimes prevented
`PanUpdated` from firing.

After fix:
- `PanUpdated` fires consistently when dragging from any part of the
control, including edges.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-gestures Gesture types community ✨ Community Contribution platform/windows s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PanGestureRecognizer PanUPdated not firing when mouse cursor is on the first pixel of control

4 participants