@@ -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 ;
@@ -611,12 +618,55 @@ void OnPointerReleased(object sender, PointerRoutedEventArgs e)
611618 }
612619
613620 void OnPgrPointerEntered ( object sender , PointerRoutedEventArgs e )
614- => HandlePgrPointerEvent ( e , ( view , recognizer )
615- => recognizer . SendPointerEntered ( view , ( relativeTo )
616- => GetPosition ( relativeTo , e ) , _control is null ? null : new PlatformPointerEventArgs ( _control , e ) ) ) ;
621+ {
622+
623+ var pointerId = e . Pointer ? . PointerId ?? uint . MaxValue ;
624+ var now = DateTime . UtcNow ;
625+
626+ // Periodic cleanup when dictionary gets large - this should never happen since each
627+ // PointerEntered should have a matching PointerExited that cleans up the entry,
628+ // but we include this as a safety measure to prevent unbounded memory growth.
629+ // We clean up entries older than twice the debounce window.
630+ if ( _lastPointerEnteredTime . Count > 5 )
631+ {
632+ var cutoff = now . AddMilliseconds ( - POINTER_DEBOUNCE_MS * 2 ) ;
633+ var keysToRemove = _lastPointerEnteredTime . Where ( kvp => kvp . Value < cutoff ) . Select ( kvp => kvp . Key ) . ToList ( ) ;
634+ foreach ( var key in keysToRemove )
635+ _lastPointerEnteredTime . Remove ( key ) ;
636+ }
637+
638+ // Multi-window bug workaround: There's a specific bug where PointerEntered events
639+ // fire unexpectedly when multiple windows are open. We work around this by
640+ // debouncing - if the same pointer had an Enter event recently and we have multiple
641+ // windows open, we ignore the duplicate event. Only applies in multi-window scenarios
642+ // to avoid performance overhead in normal single-window usage.
643+ if ( _lastPointerEnteredTime . TryGetValue ( pointerId , out var lastTime ) &&
644+ ( now - lastTime ) . TotalMilliseconds < POINTER_DEBOUNCE_MS && HasMultipleWindows ( ) )
645+ {
646+ return ;
647+ }
648+
649+ // Track this pointer's entry time for future debounce checks
650+ _lastPointerEnteredTime [ pointerId ] = now ;
651+
652+ HandlePgrPointerEvent ( e , ( view , recognizer )
653+ => recognizer . SendPointerEntered ( view , ( relativeTo )
654+ => GetPosition ( relativeTo , e ) , _control is null ? null : new PlatformPointerEventArgs ( _control , e ) ) ) ;
655+ }
617656
618657 void OnPgrPointerExited ( object sender , PointerRoutedEventArgs e )
619658 {
659+
660+ // Clean up debounce tracking when pointer exits, but only for relevant events.
661+ // This is part of the multi-window bug workaround. We only clean up tracking
662+ // for events that are relevant to our current element's window to avoid clearing
663+ // tracking data when spurious events from other windows occur.
664+ if ( IsPointerEventRelevantToCurrentElement ( e ) )
665+ {
666+ var pointerId = e . Pointer ? . PointerId ?? uint . MaxValue ;
667+ _lastPointerEnteredTime . Remove ( pointerId ) ;
668+ }
669+
620670 HandlePgrPointerEvent ( e , ( view , recognizer )
621671 => recognizer . SendPointerExited ( view , ( relativeTo )
622672 => GetPosition ( relativeTo , e ) , _control is null ? null : new PlatformPointerEventArgs ( _control , e ) ) ) ;
@@ -664,13 +714,54 @@ private void HandlePgrPointerEvent(PointerRoutedEventArgs e, Action<View, Pointe
664714 return ;
665715 }
666716
717+ // Check if the pointer event is relevant to the current element's window
718+ if ( ! IsPointerEventRelevantToCurrentElement ( e ) )
719+ {
720+ return ;
721+ }
667722 var pointerGestures = ElementGestureRecognizers . GetGesturesFor < PointerGestureRecognizer > ( ) ;
668723 foreach ( var recognizer in pointerGestures )
669724 {
670725 SendPointerEvent . Invoke ( view , recognizer ) ;
671726 }
672727 }
673728
729+ /// <summary>
730+ /// Determines if multiple windows are currently open. This is used to decide
731+ /// whether to apply pointer event debouncing to work around a specific bug where
732+ /// PointerEntered events fire unexpectedly in multi-window scenarios.
733+ /// </summary>
734+ /// <returns>True if multiple windows are open, false otherwise</returns>
735+ bool HasMultipleWindows ( ) =>
736+ Application . Current ? . Windows ? . Count > 1 ;
737+
738+ bool IsPointerEventRelevantToCurrentElement ( PointerRoutedEventArgs e )
739+ {
740+ // For multi-window scenarios, we need to validate that the pointer event
741+ // is actually relevant to the current element's window
742+ try
743+ {
744+ // Check if the container has a valid XamlRoot (indicates it's in a live window)
745+ if ( _container ? . XamlRoot is null || e ? . OriginalSource is null )
746+ {
747+ return false ;
748+ }
749+ // Validate that the event source is from the same visual tree as our container
750+ if ( e . OriginalSource is FrameworkElement sourceElement && sourceElement . XamlRoot != _container . XamlRoot )
751+ {
752+ return false ; // Event is from a different window
753+ }
754+
755+ return true ;
756+ }
757+ catch ( Exception ex )
758+ {
759+ // Log the exception for diagnostics
760+ Application . Current ? . FindMauiContext ( ) ? . CreateLogger < GesturePlatformManager > ( ) ? . LogError ( ex , "An error occurred while validating pointer event relevance." ) ;
761+ return false ;
762+ }
763+ }
764+
674765 Point ? GetPosition ( IElement ? relativeTo , RoutedEventArgs e )
675766 {
676767 var result = e . GetPositionRelativeToElement ( relativeTo ) ;
0 commit comments