Windows: Fix PanGestureRecognizer not starting when drag begins near control edge#34362
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34362Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34362" |
|
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. |
|
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 👍 |
🤖 AI Summary📊 Expand Full Review —
|
| # | 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
- ❌ Tests FAIL without fix: NOT APPLICABLE — No tests exist for issue PanGestureRecognizer PanUPdated not firing when mouse cursor is on the first pixel of control #34119
- ❌ Tests PASS with fix: NOT APPLICABLE — No tests exist for issue PanGestureRecognizer PanUPdated not firing when mouse cursor is on the first pixel of control #34119
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:
- ✅ PanGestureRecognizer tests exist for other issues (Issue5191, Issue20772, Bugzilla39530)
- ❌ No tests exist specifically for issue PanGestureRecognizer PanUPdated not firing when mouse cursor is on the first pixel of control #34119 (drag begins near control edge scenario)
- ❌ No tests found in PR Windows: Fix PanGestureRecognizer not starting when drag begins near control edge #34362 (only code change, no test files added)
Test Search Results:
- Unit Tests:
PanGestureRecognizerUnitTests.cs(general Pan gesture tests, not edge-case specific) - UI Tests: Issue5191, Issue20772, Bugzilla39530 (existing Pan tests, but not for edge-case)
- Issue PanGestureRecognizer PanUPdated not firing when mouse cursor is on the first pixel of control #34119 Tests: None found in entire repository
Recommendation
The fix should be validated with UI tests that reproduce the edge-case scenario:
- Create a draggable view with PanGestureRecognizer in an AbsoluteLayout
- Test dragging from the edge (first pixels) of the control
- Verify
PanUpdatedfires (currently fails without the fix) - Verify
PanUpdatedfires 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
- Implement test cases for issue PanGestureRecognizer PanUPdated not firing when mouse cursor is on the first pixel of control #34119 edge-case scenario
- Re-run Phase 2 Gate with tests in place
- Proceed to Try-Fix phase if gate passes with full verification
🔧 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
The fix should be validated with UI tests that reproduce the edge-case scenario:
- Create a draggable view with PanGestureRecognizer in an AbsoluteLayout
- Test dragging from the edge (first pixels) of the control
- Verify
PanUpdatedfires (currently fails without the fix) - Verify
PanUpdatedfires after the fix
Suggested: Use write-tests-agent skill to create UI tests before merging.
📋 PR Finalization ReviewTitle: ✅ GoodCurrent: Description:
|
…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.
…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.
…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.
Fixes #34119
When dragging from the edge of a control on Windows,
PointerExitedcan fire before the manipulation begins.In this scenario the pointer ID is removed from the
_fingerscollection beforeHandlePanruns, resulting in_fingers.Count == 0. SincePanGestureRecognizertypically expectsTouchPoints == 1, the recognizer never matches andPanUpdatedis not raised.This change prevents removing the pointer prematurely when a gesture interaction has not fully completed, ensuring
_fingersremains consistent until the gesture lifecycle finishes.After this change, dragging from the edge of a control correctly triggers
PanUpdatedas expected.Tested
Reproduced using a simple
AbsoluteLayoutwith a draggable view andPanGestureRecognizer.Before fix:
PanUpdatedfrom firing.After fix:
PanUpdatedfires consistently when dragging from any part of the control, including edges.