diff --git a/src/Uno.UI/Extensions/UIEventExtensions.iOSmacOS.cs b/src/Uno.UI/Extensions/UIEventExtensions.iOSmacOS.cs index 0a6d9ae6669d..3e2320272882 100644 --- a/src/Uno.UI/Extensions/UIEventExtensions.iOSmacOS.cs +++ b/src/Uno.UI/Extensions/UIEventExtensions.iOSmacOS.cs @@ -58,6 +58,22 @@ internal static bool IsTouchInView(this _Touch touch, _View view) && screenLocation.X < bounds.Right && screenLocation.Y < bounds.Bottom; } + + internal static UIElement FindOriginalSource(this _Touch touch) + { + var view = touch.View; + while (view != null) + { + if (view is UIElement elt) + { + return elt; + } + + view = view.Superview; + } + + return null; + } #endif /// diff --git a/src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.iOS.cs b/src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.iOS.cs index 2d4f9f4b239c..c6702348c7ee 100644 --- a/src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.iOS.cs @@ -42,7 +42,7 @@ internal PointerRoutedEventArgs(PointerRoutedEventArgs previous, PointerRoutedEv _properties = previous._properties; } - internal PointerRoutedEventArgs(uint pointerId, UITouch nativeTouch, UIEvent nativeEvent, UIElement receiver) : this() + internal PointerRoutedEventArgs(uint pointerId, UITouch nativeTouch, UIEvent nativeEvent, UIElement originalSource) : this() { _nativeTouch = nativeTouch; _nativeEvent = nativeEvent; @@ -55,7 +55,7 @@ internal PointerRoutedEventArgs(uint pointerId, UITouch nativeTouch, UIEvent nat FrameId = ToFrameId(_nativeTouch.Timestamp); Pointer = new Pointer(pointerId, deviceType, isInContact, isInRange: true); KeyModifiers = VirtualKeyModifiers.None; - OriginalSource = FindOriginalSource(_nativeTouch) ?? receiver; + OriginalSource = originalSource; _properties = GetProperties(); // Make sure to capture the properties state so we can re-use them in "mixed" ctor } @@ -119,22 +119,6 @@ private static uint ToFrameId(double timestamp) // We use modulo to make sure to reset to 0 in that case (1.13 years of app run-time, but we prefer to be safe). return (uint)(frameId % uint.MaxValue); } - - private static UIElement FindOriginalSource(UITouch touch) - { - var view = touch.View; - while (view != null) - { - if (view is UIElement elt) - { - return elt; - } - - view = view.Superview; - } - - return null; - } #endregion } } diff --git a/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.cs b/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.cs index f0f34177ca87..08231aef015b 100644 --- a/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.cs +++ b/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.cs @@ -133,6 +133,11 @@ internal void ProcessPointerDown(PointerRoutedEventArgs args) // Raise the event to the target reRouted.To.OnPointerDown(args); +#if __IOS__ + // Also as the FlyoutPopupPanel is being removed from the UI tree, we won't get any ProcessPointerUp, so we are forcefully causing it here. + args.Reset(canBubbleNatively: false); + reRouted.To.OnPointerUp(args); +#endif args.Handled = true; // Make sure the event is flagged as handled so it won't be bubbled by native code to us again from the FlyoutPopupPanel. return; // The event already came back to us (due to reRouted.To.OnPointerDown(args)). diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.iOS.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.iOS.cs index 9f9770cdad5f..2c94b9f25c73 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.iOS.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.iOS.cs @@ -9,6 +9,7 @@ using Foundation; using UIKit; using Uno.Extensions; +using Uno.Foundation.Logging; using Uno.UI.Extensions; using Uno.UI.Xaml.Core; using WinUICoreServices = Uno.UI.Xaml.Core.CoreServices; @@ -75,25 +76,39 @@ public void Release(UIElement element) } } + [ThreadStatic] + private static UIElement _sequenceReRouteTarget; + public static void ReRoutePointerSequenceTo(UIElement target) + => _sequenceReRouteTarget = target; + private IEnumerable _parentsTouchesManager; private bool _isManipulating; partial void InitializePointersPartial() { MultipleTouchEnabled = true; - ArePointersEnabled = true; } #region Native touch handling (i.e. source of the pointer / gesture events) public override void TouchesBegan(NSSet touches, UIEvent evt) - => TouchesBegan(touches, evt, canBubbleNatively: true); + { + if (_sequenceReRouteTarget is { } target && target != this) + { + if (this.Log().IsEnabled(LogLevel.Debug)) + this.Log().Debug($"Re-routing pointer sequence (implicit capture) from {this.GetDebugName()} to {target.GetDebugName()}"); + + target.TouchesBegan(touches, evt, canBubbleNatively: false, forcedOriginalSource: target); + } + + TouchesBegan(touches, evt, canBubbleNatively: true); + } /// /// WARNING: canBubbleNatively=false on TouchesBegan has MAJOR impact regarding future events, use precautiously! /// (cf. remarks in the method) /// - internal void TouchesBegan(NSSet touches, UIEvent evt, bool canBubbleNatively) + internal void TouchesBegan(NSSet touches, UIEvent evt, bool canBubbleNatively, UIElement forcedOriginalSource = null) { #if TRACE_NATIVE_POINTER_EVENTS Console.WriteLine($"{this.GetDebugIdentifier()} [TOUCHES_BEGAN] enabled:{ArePointersEnabled}"); @@ -116,7 +131,8 @@ internal void TouchesBegan(NSSet touches, UIEvent evt, bool canBubbleNatively) foreach (UITouch touch in touches) { var pt = TransientNativePointer.Get(this, touch); - var args = new PointerRoutedEventArgs(pt.Id, touch, evt, this) { CanBubbleNatively = canBubbleNatively }; + var src = forcedOriginalSource ?? touch.FindOriginalSource() ?? this; + var args = new PointerRoutedEventArgs(pt.Id, touch, evt, src) { CanBubbleNatively = canBubbleNatively }; // We set the DownArgs only for the top most element (a.k.a. OriginalSource) pt.DownArgs ??= args; @@ -166,9 +182,19 @@ internal void TouchesBegan(NSSet touches, UIEvent evt, bool canBubbleNatively) } public override void TouchesMoved(NSSet touches, UIEvent evt) - => TouchesMoved(touches, evt, canBubbleNatively: true); + { + if (_sequenceReRouteTarget is { } target && target != this) + { + if (this.Log().IsEnabled(LogLevel.Debug)) + this.Log().Debug($"Re-routing pointer sequence (implicit capture) from {this.GetDebugName()} to {target.GetDebugName()}"); + + target.TouchesMoved(touches, evt, canBubbleNatively: false, forcedOriginalSource: target); + } + + TouchesMoved(touches, evt, canBubbleNatively: true); + } - internal void TouchesMoved(NSSet touches, UIEvent evt, bool canBubbleNatively) + internal void TouchesMoved(NSSet touches, UIEvent evt, bool canBubbleNatively, UIElement forcedOriginalSource = null) { #if TRACE_NATIVE_POINTER_EVENTS Console.WriteLine($"{this.GetDebugIdentifier()} [TOUCHES_MOVED]"); @@ -180,7 +206,8 @@ internal void TouchesMoved(NSSet touches, UIEvent evt, bool canBubbleNatively) foreach (UITouch touch in touches) { var pt = TransientNativePointer.Get(this, touch); - var args = new PointerRoutedEventArgs(pt.Id, touch, evt, this) { CanBubbleNatively = canBubbleNatively }; + var src = forcedOriginalSource ?? touch.FindOriginalSource() ?? this; + var args = new PointerRoutedEventArgs(pt.Id, touch, evt, src) { CanBubbleNatively = canBubbleNatively }; var isPointerOver = touch.IsTouchInView(this); // This is acceptable to keep that flag in a kind-of static way, since iOS do "implicit captures", @@ -205,9 +232,21 @@ internal void TouchesMoved(NSSet touches, UIEvent evt, bool canBubbleNatively) } public override void TouchesEnded(NSSet touches, UIEvent evt) - => TouchesEnded(touches, evt, canBubbleNatively: true); + { + if (_sequenceReRouteTarget is { } target && target != this) + { + _sequenceReRouteTarget = null; + + if (this.Log().IsEnabled(LogLevel.Debug)) + this.Log().Debug($"Re-routing pointer sequence (implicit capture) from {this.GetDebugName()} to {target.GetDebugName()}"); + + target.TouchesEnded(touches, evt, canBubbleNatively: false, forcedOriginalSource: target); + } + + TouchesEnded(touches, evt, canBubbleNatively: true); + } - internal void TouchesEnded(NSSet touches, UIEvent evt, bool canBubbleNatively) + internal void TouchesEnded(NSSet touches, UIEvent evt, bool canBubbleNatively, UIElement forcedOriginalSource = null) { #if TRACE_NATIVE_POINTER_EVENTS Console.WriteLine($"{this.GetDebugIdentifier()} [TOUCHES_ENDED]"); @@ -236,7 +275,8 @@ break lots of control (ScrollViewer) and ability to easily integrate an external foreach (UITouch touch in touches) { var pt = TransientNativePointer.Get(this, touch); - var args = new PointerRoutedEventArgs(pt.Id, touch, evt, this) { CanBubbleNatively = canBubbleNatively }; + var src = forcedOriginalSource ?? touch.FindOriginalSource() ?? this; + var args = new PointerRoutedEventArgs(pt.Id, touch, evt, src) { CanBubbleNatively = canBubbleNatively }; if (!pt.HadMove) { @@ -283,9 +323,21 @@ break lots of control (ScrollViewer) and ability to easily integrate an external } public override void TouchesCancelled(NSSet touches, UIEvent evt) - => TouchesCancelled(touches, evt, canBubbleNatively: true); + { + if (_sequenceReRouteTarget is { } target && target != this) + { + _sequenceReRouteTarget = null; + + if (this.Log().IsEnabled(LogLevel.Debug)) + this.Log().Debug($"Re-routing pointer sequence (implicit capture) from {this.GetDebugName()} to {target.GetDebugName()}"); + + target.TouchesCancelled(touches, evt, canBubbleNatively: false, forcedOriginalSource: target); + } + + TouchesCancelled(touches, evt, canBubbleNatively: true); + } - internal void TouchesCancelled(NSSet touches, UIEvent evt, bool canBubbleNatively) + internal void TouchesCancelled(NSSet touches, UIEvent evt, bool canBubbleNatively, UIElement forcedOriginalSource = null) { #if TRACE_NATIVE_POINTER_EVENTS Console.WriteLine($"{this.GetDebugIdentifier()} [TOUCHES_CANCELLED]"); @@ -297,7 +349,8 @@ internal void TouchesCancelled(NSSet touches, UIEvent evt, bool canBubbleNativel foreach (UITouch touch in touches) { var pt = TransientNativePointer.Get(this, touch); - var args = new PointerRoutedEventArgs(pt.Id, touch, evt, this) { CanBubbleNatively = canBubbleNatively }; + var src = forcedOriginalSource ?? touch.FindOriginalSource() ?? this; + var args = new PointerRoutedEventArgs(pt.Id, touch, evt, src) { CanBubbleNatively = canBubbleNatively }; // Note: We should have raise either PointerCaptureLost or PointerCancelled here depending of the reason which // drives the system to bubble a lost. However we don't have this kind of information on iOS, and it's