Skip to content

Commit 3f0a7c6

Browse files
Ahamed-AliPureWeen
andauthored
[Windows]Fixed the PointerGestureRecognizer behaves incorrectly when multiple windows are open. (#30537)
* Fixed the PointerGestureRecognizer events from another window interfere with the active window * Optimize the fix * Logging the error * missed paramter * - add some code to debounce when there are multi window scenarios --------- Co-authored-by: Shane Neuville <shneuvil@microsoft.com>
1 parent ede52c5 commit 3f0a7c6

File tree

1 file changed

+94
-3
lines changed

1 file changed

+94
-3
lines changed

src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Windows.cs

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)