@@ -19,6 +19,13 @@ class GesturePlatformManager : IDisposable
1919 readonly IPlatformViewHandler _handler ;
2020 readonly NotifyCollectionChangedEventHandler _collectionChangedHandler ;
2121 readonly List < uint > _fingers = new List < uint > ( ) ;
22+ // Dictionary to track when each pointer last entered, used to work around a bug where
23+ // PointerEntered events fire unexpectedly in multi-window scenarios
24+ readonly Dictionary < uint , DateTime > _lastPointerEnteredTime = new ( ) ;
25+ // Debounce window in milliseconds - if two PointerEntered events for the same pointer
26+ // occur within this timeframe in a multi-window scenario, the second one is likely
27+ // the bug manifesting and should be ignored
28+ const int POINTER_DEBOUNCE_MS = 1000 ;
2229 FrameworkElement ? _container ;
2330 FrameworkElement ? _control ;
2431 VisualElement ? _element ;
@@ -594,12 +601,55 @@ void OnPointerReleased(object sender, PointerRoutedEventArgs e)
594601 }
595602
596603 void OnPgrPointerEntered ( object sender , PointerRoutedEventArgs e )
597- => HandlePgrPointerEvent ( e , ( view , recognizer )
598- => recognizer . SendPointerEntered ( view , ( relativeTo )
599- => GetPosition ( relativeTo , e ) , _control is null ? null : new PlatformPointerEventArgs ( _control , e ) ) ) ;
604+ {
605+
606+ var pointerId = e . Pointer ? . PointerId ?? uint . MaxValue ;
607+ var now = DateTime . UtcNow ;
608+
609+ // Periodic cleanup when dictionary gets large - this should never happen since each
610+ // PointerEntered should have a matching PointerExited that cleans up the entry,
611+ // but we include this as a safety measure to prevent unbounded memory growth.
612+ // We clean up entries older than twice the debounce window.
613+ if ( _lastPointerEnteredTime . Count > 5 )
614+ {
615+ var cutoff = now . AddMilliseconds ( - POINTER_DEBOUNCE_MS * 2 ) ;
616+ var keysToRemove = _lastPointerEnteredTime . Where ( kvp => kvp . Value < cutoff ) . Select ( kvp => kvp . Key ) . ToList ( ) ;
617+ foreach ( var key in keysToRemove )
618+ _lastPointerEnteredTime . Remove ( key ) ;
619+ }
620+
621+ // Multi-window bug workaround: There's a specific bug where PointerEntered events
622+ // fire unexpectedly when multiple windows are open. We work around this by
623+ // debouncing - if the same pointer had an Enter event recently and we have multiple
624+ // windows open, we ignore the duplicate event. Only applies in multi-window scenarios
625+ // to avoid performance overhead in normal single-window usage.
626+ if ( _lastPointerEnteredTime . TryGetValue ( pointerId , out var lastTime ) &&
627+ ( now - lastTime ) . TotalMilliseconds < POINTER_DEBOUNCE_MS && HasMultipleWindows ( ) )
628+ {
629+ return ;
630+ }
631+
632+ // Track this pointer's entry time for future debounce checks
633+ _lastPointerEnteredTime [ pointerId ] = now ;
634+
635+ HandlePgrPointerEvent ( e , ( view , recognizer )
636+ => recognizer . SendPointerEntered ( view , ( relativeTo )
637+ => GetPosition ( relativeTo , e ) , _control is null ? null : new PlatformPointerEventArgs ( _control , e ) ) ) ;
638+ }
600639
601640 void OnPgrPointerExited ( object sender , PointerRoutedEventArgs e )
602641 {
642+
643+ // Clean up debounce tracking when pointer exits, but only for relevant events.
644+ // This is part of the multi-window bug workaround. We only clean up tracking
645+ // for events that are relevant to our current element's window to avoid clearing
646+ // tracking data when spurious events from other windows occur.
647+ if ( IsPointerEventRelevantToCurrentElement ( e ) )
648+ {
649+ var pointerId = e . Pointer ? . PointerId ?? uint . MaxValue ;
650+ _lastPointerEnteredTime . Remove ( pointerId ) ;
651+ }
652+
603653 HandlePgrPointerEvent ( e , ( view , recognizer )
604654 => recognizer . SendPointerExited ( view , ( relativeTo )
605655 => GetPosition ( relativeTo , e ) , _control is null ? null : new PlatformPointerEventArgs ( _control , e ) ) ) ;
@@ -647,13 +697,54 @@ private void HandlePgrPointerEvent(PointerRoutedEventArgs e, Action<View, Pointe
647697 return ;
648698 }
649699
700+ // Check if the pointer event is relevant to the current element's window
701+ if ( ! IsPointerEventRelevantToCurrentElement ( e ) )
702+ {
703+ return ;
704+ }
650705 var pointerGestures = ElementGestureRecognizers . GetGesturesFor < PointerGestureRecognizer > ( ) ;
651706 foreach ( var recognizer in pointerGestures )
652707 {
653708 SendPointerEvent . Invoke ( view , recognizer ) ;
654709 }
655710 }
656711
712+ /// <summary>
713+ /// Determines if multiple windows are currently open. This is used to decide
714+ /// whether to apply pointer event debouncing to work around a specific bug where
715+ /// PointerEntered events fire unexpectedly in multi-window scenarios.
716+ /// </summary>
717+ /// <returns>True if multiple windows are open, false otherwise</returns>
718+ bool HasMultipleWindows ( ) =>
719+ Application . Current ? . Windows ? . Count > 1 ;
720+
721+ bool IsPointerEventRelevantToCurrentElement ( PointerRoutedEventArgs e )
722+ {
723+ // For multi-window scenarios, we need to validate that the pointer event
724+ // is actually relevant to the current element's window
725+ try
726+ {
727+ // Check if the container has a valid XamlRoot (indicates it's in a live window)
728+ if ( _container ? . XamlRoot is null || e ? . OriginalSource is null )
729+ {
730+ return false ;
731+ }
732+ // Validate that the event source is from the same visual tree as our container
733+ if ( e . OriginalSource is FrameworkElement sourceElement && sourceElement . XamlRoot != _container . XamlRoot )
734+ {
735+ return false ; // Event is from a different window
736+ }
737+
738+ return true ;
739+ }
740+ catch ( Exception ex )
741+ {
742+ // Log the exception for diagnostics
743+ Application . Current ? . FindMauiContext ( ) ? . CreateLogger < GesturePlatformManager > ( ) ? . LogError ( ex , "An error occurred while validating pointer event relevance." ) ;
744+ return false ;
745+ }
746+ }
747+
657748 Point ? GetPosition ( IElement ? relativeTo , RoutedEventArgs e )
658749 {
659750 var result = e . GetPositionRelativeToElement ( relativeTo ) ;
0 commit comments