From 34140dbf39e459b29cc9cc675a44a09446475d19 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 26 Nov 2024 15:49:01 -0500 Subject: [PATCH 1/2] fix(pointers): Fix possible missing pointer exited event with touch using wasm on iOS/iPad --- .../Runtime/BrowserPointerInputSource.wasm.cs | 25 +++++++++-- .../Internal/InputManager.Pointers.Managed.cs | 44 ++++++++++++++----- .../ts/Runtime/BrowserPointerInputSource.ts | 4 +- src/Uno.UWP/UI/Core/PointerEventArgs.cs | 8 ++++ 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs b/src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs index 51c2741190c9..bbf6a6d943ce 100644 --- a/src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs +++ b/src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs @@ -15,6 +15,7 @@ using _NativeMethods = __Windows.UI.Core.CoreWindow.NativeMethods; using System.Runtime.InteropServices; using Windows.System; +using Microsoft.UI.Xaml; namespace Uno.UI.Runtime; @@ -29,6 +30,7 @@ internal partial class BrowserPointerInputSource : IUnoCorePointerInputSource private static readonly Logger? _logTrace = _log.IsTraceEnabled(LogLevel.Trace) ? _log : null; private ulong _bootTime; + private bool _isIOs; private bool _isOver; private PointerPoint? _lastPoint; private CoreCursor _pointerCursor = new(CoreCursorType.Arrow, 0); @@ -55,11 +57,19 @@ public BrowserPointerInputSource() private static partial void Initialize([JSMarshalAs] object inputSource); [JSExport] - private static void OnInitialized([JSMarshalAs] object inputSource, double bootTime) + private static void OnInitialized([JSMarshalAs] object inputSource, double bootTime, string userAgent) { - ((BrowserPointerInputSource)inputSource)._bootTime = (ulong)bootTime; + if (inputSource is BrowserPointerInputSource that) + { + that._bootTime = (ulong)bootTime; + that._isIOs = userAgent.Contains("iPhone") || userAgent.Contains("iPad"); // Note: OperatingSystem.IsIOS() is false - _logTrace?.Trace("Complete initialization of BrowserPointerInputSource, we are now ready to receive pointer events!"); + _logTrace?.Trace("Complete initialization of BrowserPointerInputSource, we are now ready to receive pointer events!"); + } + else if (_log.IsEnabled(LogLevel.Error)) + { + _log.Error("Requested init using an invalid source."); + } } [JSExport] @@ -121,6 +131,7 @@ private static int OnNativeEvent( case HtmlPointerEvent.pointerleave: that._isOver = false; that.PointerExited?.Invoke(that, args); + _PointerIdentifierPool.ReleaseManaged(pointerIdentifier); break; case HtmlPointerEvent.pointerdown: @@ -130,7 +141,13 @@ private static int OnNativeEvent( case HtmlPointerEvent.pointerup: //case HtmlPointerEvent.lostpointercapture: // if pointer is captured, we don't get a up, just a capture lost (with skia for wasm) that.PointerReleased?.Invoke(that, args); - _PointerIdentifierPool.ReleaseManaged(pointerIdentifier); + if (that._isIOs && args is { CurrentPoint.PointerDeviceType: PointerDeviceType.Touch, DispatchResult: UIElement.PointerEventDispatchResult { VisualTreeAltered: true } }) + { + // On iOS, when the element under the pointer is removed, the browser won't send any pointer leave event. + + args.DispatchResult = null; // To be clean only, the value is not used in the leave case. + goto case HtmlPointerEvent.pointerleave; + } break; case HtmlPointerEvent.pointermove: diff --git a/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs b/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs index c2dd932db439..74cd6daf2282 100644 --- a/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs +++ b/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs @@ -239,9 +239,10 @@ private void OnPointerWheelChanged(Windows.UI.Core.PointerEventArgs args, bool i #endif var routedArgs = new PointerRoutedEventArgs(args, originalSource) { IsInjected = isInjected }; + var result = default(PointerEventDispatchResult); // First raise the event, either on the OriginalSource or on the capture owners if any - RaiseUsingCaptures(Wheel, originalSource, routedArgs, setCursor: true); + result = RaiseUsingCaptures(Wheel, originalSource, routedArgs, setCursor: true); // Scrolling can change the element underneath the pointer, so we need to update (originalSource, var staleBranch) = HitTest(args, caller: "OnPointerWheelChanged_post_wheel", isStale: _isOver); @@ -250,7 +251,9 @@ private void OnPointerWheelChanged(Windows.UI.Core.PointerEventArgs args, bool i // Second raise the PointerExited events on the stale branch if (staleBranch.HasValue) { - if (Raise(Leave, staleBranch.Value, routedArgs) is { VisualTreeAltered: true }) + var leaveResult = Raise(Leave, staleBranch.Value, routedArgs); + result += leaveResult; + if (leaveResult is { VisualTreeAltered: true }) { // The visual tree has been modified in a way that requires performing a new hit test. originalSource = HitTest(args, caller: "OnPointerWheelChanged_post_leave").element ?? _inputManager.ContentRoot.VisualTree.RootElement; @@ -259,7 +262,7 @@ private void OnPointerWheelChanged(Windows.UI.Core.PointerEventArgs args, bool i // Third (try to) raise the PointerEnter on the OriginalSource // Note: This won't do anything if already over. - Raise(Enter, originalSource!, routedArgs); + result += Raise(Enter, originalSource!, routedArgs); if (!PointerCapture.TryGet(routedArgs.Pointer, out var capture) || capture.IsImplicitOnly) { @@ -267,6 +270,8 @@ private void OnPointerWheelChanged(Windows.UI.Core.PointerEventArgs args, bool i // If not, we make sure to update the cursor based on the new originalSource. SetSourceCursor(originalSource); } + + args.DispatchResult = result; } private void OnPointerEntered(Windows.UI.Core.PointerEventArgs args, bool isInjected = false) @@ -300,7 +305,9 @@ private void OnPointerEntered(Windows.UI.Core.PointerEventArgs args, bool isInje var routedArgs = new PointerRoutedEventArgs(args, originalSource) { IsInjected = isInjected }; - Raise(Enter, originalSource, routedArgs); + var result = Raise(Enter, originalSource, routedArgs); + + args.DispatchResult = result; } private void OnPointerExited(Windows.UI.Core.PointerEventArgs args, bool isInjected = false) @@ -340,13 +347,16 @@ private void OnPointerExited(Windows.UI.Core.PointerEventArgs args, bool isInjec var routedArgs = new PointerRoutedEventArgs(args, originalSource) { IsInjected = isInjected }; - Raise(Leave, overBranchLeaf, routedArgs); + var result = Raise(Leave, overBranchLeaf, routedArgs); + if (!args.CurrentPoint.IsInContact && (PointerDeviceType)args.CurrentPoint.Pointer.Type == PointerDeviceType.Touch) { // We release the captures on exit when pointer if not pressed // Note: for a "Tap" with a finger the sequence is Up / Exited / Lost, so the lost cannot be raised on Up ReleaseCaptures(routedArgs); } + + args.DispatchResult = result; } private void OnPointerPressed(Windows.UI.Core.PointerEventArgs args, bool isInjected = false) @@ -396,7 +406,9 @@ private void OnPointerPressed(Windows.UI.Core.PointerEventArgs args, bool isInje var routedArgs = new PointerRoutedEventArgs(args, originalSource) { IsInjected = isInjected }; _pressedElements[routedArgs.Pointer] = originalSource; - Raise(Pressed, originalSource, routedArgs); + var result = Raise(Pressed, originalSource, routedArgs); + + args.DispatchResult = result; } private void OnPointerReleased(Windows.UI.Core.PointerEventArgs args, bool isInjected = false) @@ -439,7 +451,8 @@ private void OnPointerReleased(Windows.UI.Core.PointerEventArgs args, bool isInj var routedArgs = new PointerRoutedEventArgs(args, originalSource) { IsInjected = isInjected }; - RaiseUsingCaptures(Released, originalSource, routedArgs, setCursor: false); + var result = RaiseUsingCaptures(Released, originalSource, routedArgs, setCursor: false); + if (isOutOfWindow || (PointerDeviceType)args.CurrentPoint.Pointer.Type != PointerDeviceType.Touch) { // We release the captures on up but only after the released event and processed the gesture @@ -451,6 +464,8 @@ private void OnPointerReleased(Windows.UI.Core.PointerEventArgs args, bool isInj SetSourceCursor(originalSource); } ClearPressedState(routedArgs); + + args.DispatchResult = result; } private void OnPointerMoved(Windows.UI.Core.PointerEventArgs args, bool isInjected = false) @@ -482,11 +497,14 @@ private void OnPointerMoved(Windows.UI.Core.PointerEventArgs args, bool isInject } var routedArgs = new PointerRoutedEventArgs(args, originalSource) { IsInjected = isInjected }; + var result = default(PointerEventDispatchResult); // First raise the PointerExited events on the stale branch if (staleBranch.HasValue) { - if (Raise(Leave, staleBranch.Value, routedArgs) is { VisualTreeAltered: true }) + var leaveResult = Raise(Leave, staleBranch.Value, routedArgs); + result += leaveResult; + if (leaveResult is { VisualTreeAltered: true }) { // The visual tree has been modified in a way that requires performing a new hit test. originalSource = HitTest(args, caller: "OnPointerMoved_post_leave").element ?? _inputManager.ContentRoot.VisualTree.RootElement; @@ -495,7 +513,9 @@ private void OnPointerMoved(Windows.UI.Core.PointerEventArgs args, bool isInject // Second (try to) raise the PointerEnter on the OriginalSource // Note: This won't do anything if already over. - if (Raise(Enter, originalSource, routedArgs) is { VisualTreeAltered: true }) + var enterResult = Raise(Enter, originalSource, routedArgs); + result += enterResult; + if (enterResult is { VisualTreeAltered: true }) { // The visual tree has been modified in a way that requires performing a new hit test. originalSource = HitTest(args, caller: "OnPointerMoved_post_enter").element ?? _inputManager.ContentRoot.VisualTree.RootElement; @@ -503,6 +523,8 @@ private void OnPointerMoved(Windows.UI.Core.PointerEventArgs args, bool isInject // Finally raise the event, either on the OriginalSource or on the capture owners if any RaiseUsingCaptures(Move, originalSource, routedArgs, setCursor: true); + + args.DispatchResult = result; } private void OnPointerCancelled(PointerEventArgs args, bool isInjected = false) @@ -535,10 +557,12 @@ private void OnPointerCancelled(PointerEventArgs args, bool isInjected = false) var routedArgs = new PointerRoutedEventArgs(args, originalSource) { IsInjected = isInjected }; - RaiseUsingCaptures(Cancelled, originalSource, routedArgs, setCursor: false); + var result = RaiseUsingCaptures(Cancelled, originalSource, routedArgs, setCursor: false); // Note: No ReleaseCaptures(routedArgs);, the cancel automatically raise it SetSourceCursor(originalSource); ClearPressedState(routedArgs); + + args.DispatchResult = result; } #region Captures diff --git a/src/Uno.UI/ts/Runtime/BrowserPointerInputSource.ts b/src/Uno.UI/ts/Runtime/BrowserPointerInputSource.ts index 8ed54f359677..b625850055e7 100644 --- a/src/Uno.UI/ts/Runtime/BrowserPointerInputSource.ts +++ b/src/Uno.UI/ts/Runtime/BrowserPointerInputSource.ts @@ -54,7 +54,9 @@ this._bootTime = Date.now() - performance.now(); this._source = manageSource; - BrowserPointerInputSource._exports.OnInitialized(manageSource, this._bootTime); + var userAgent = navigator.userAgent || navigator.vendor || window.opera; + + BrowserPointerInputSource._exports.OnInitialized(manageSource, this._bootTime, userAgent); this.subscribePointerEvents(); // Subscribe only after the managed initialization is done } diff --git a/src/Uno.UWP/UI/Core/PointerEventArgs.cs b/src/Uno.UWP/UI/Core/PointerEventArgs.cs index e3b7850a8aea..c05201d74dac 100644 --- a/src/Uno.UWP/UI/Core/PointerEventArgs.cs +++ b/src/Uno.UWP/UI/Core/PointerEventArgs.cs @@ -21,6 +21,14 @@ internal PointerEventArgs(PointerPoint currentPoint, VirtualKeyModifiers keyModi public IList GetIntermediatePoints() => new List { CurrentPoint }; +#nullable enable + /// + /// Gets the dispatch result of this event, if any. + /// This is defined by the InputManager if the event goes though it. + /// + internal object? DispatchResult { get; set; } +#nullable restore + /// public override string ToString() => $"{CurrentPoint} | modifiers: {KeyModifiers}"; From 750041fc2a8b0719679c0c166dd58b70c841478b Mon Sep 17 00:00:00 2001 From: David Date: Wed, 27 Nov 2024 15:51:32 -0500 Subject: [PATCH 2/2] chore: Code review --- src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs | 5 ++++- src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs b/src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs index bbf6a6d943ce..6df747b81ee7 100644 --- a/src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs +++ b/src/Uno.UI/Runtime/BrowserPointerInputSource.wasm.cs @@ -62,7 +62,10 @@ private static void OnInitialized([JSMarshalAs] object inputSource, if (inputSource is BrowserPointerInputSource that) { that._bootTime = (ulong)bootTime; - that._isIOs = userAgent.Contains("iPhone") || userAgent.Contains("iPad"); // Note: OperatingSystem.IsIOS() is false + + // Note: OperatingSystem.IsIOS() is false + that._isIOs = userAgent.Contains("iPhone", StringComparison.OrdinalIgnoreCase) + || userAgent.Contains("iPad", StringComparison.OrdinalIgnoreCase); _logTrace?.Trace("Complete initialization of BrowserPointerInputSource, we are now ready to receive pointer events!"); } diff --git a/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs b/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs index 74cd6daf2282..f71622035e33 100644 --- a/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs +++ b/src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs @@ -239,10 +239,9 @@ private void OnPointerWheelChanged(Windows.UI.Core.PointerEventArgs args, bool i #endif var routedArgs = new PointerRoutedEventArgs(args, originalSource) { IsInjected = isInjected }; - var result = default(PointerEventDispatchResult); // First raise the event, either on the OriginalSource or on the capture owners if any - result = RaiseUsingCaptures(Wheel, originalSource, routedArgs, setCursor: true); + var result = RaiseUsingCaptures(Wheel, originalSource, routedArgs, setCursor: true); // Scrolling can change the element underneath the pointer, so we need to update (originalSource, var staleBranch) = HitTest(args, caller: "OnPointerWheelChanged_post_wheel", isStale: _isOver);