Skip to content

Commit a35d935

Browse files
Ahamed-AliPureWeen
authored andcommitted
[Windows]Fixed the PointerGestureRecognizer behaves incorrectly when multiple windows are open. (dotnet#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 fb3d6b1 commit a35d935

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;
@@ -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

Comments
 (0)