diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_UIElement.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_UIElement.cs index ddb0c380ab13..179de1e5b0e2 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_UIElement.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_UIElement.cs @@ -32,8 +32,14 @@ using Uno.UI; using Windows.UI; using Windows.ApplicationModel.Appointments; +using Microsoft.UI.Input; using Microsoft.UI.Xaml.Hosting; using Uno.UI.Toolkit.Extensions; +using KeyEventArgs = Windows.UI.Core.KeyEventArgs; + +#if !HAS_UNO_WINUI +using Windows.UI.Input; +#endif #if __IOS__ using UIKit; @@ -1758,7 +1764,7 @@ await UITestHelper.Load(new StackPanel await TestServices.WindowHelper.WaitForIdle(); Assert.AreEqual(1, dragEnterCount); - Assert.AreEqual(1, dragOverCount); + Assert.AreEqual(2, dragOverCount); Assert.AreEqual(0, dropCount); mouse.Release(); @@ -1768,10 +1774,74 @@ await UITestHelper.Load(new StackPanel } Assert.AreEqual(1, dragEnterCount); - Assert.AreEqual(2, dragOverCount); + Assert.AreEqual(3, dragOverCount); Assert.AreEqual(waitAfterRelease ? 1 : 0, dropCount); } + [TestMethod] + [RunsOnUIThread] +#if !HAS_INPUT_INJECTOR + [Ignore("InputInjector is not supported on this platform.")] +#endif + public async Task When_DragEnter_Fires_Along_DragStarting() + { + if (TestServices.WindowHelper.IsXamlIsland) + { + Assert.Inconclusive("Drag and drop doesn't work in Uno islands."); + } + + var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector"); + using var mouse = injector.GetMouse(); + + var rect = new Rectangle + { + Fill = new SolidColorBrush(Microsoft.UI.Colors.Red), + Width = 100, + Height = 100, + CanDrag = true + }; + + var border = new Border + { + Background = new SolidColorBrush(Microsoft.UI.Colors.Blue), + Padding = new Thickness(10), + Child = rect, + AllowDrop = true + }; + + var dragStartingCount = 0; + var dragEnterCount = 0; + var dragOverCount = 0; + + border.DragEnter += (_, _) => dragEnterCount++; + border.DragOver += (_, _) => dragOverCount++; + rect.DragStarting += (_, _) => dragStartingCount++; + + await UITestHelper.Load(border); + + mouse.MoveTo(rect.GetAbsoluteBoundsRect().GetCenter()); + await UITestHelper.WaitForIdle(); + mouse.Press(); + await UITestHelper.WaitForIdle(); + mouse.MoveBy(GestureRecognizer.TapMaxXDelta / 3, 0); + await UITestHelper.WaitForIdle(); + mouse.MoveBy(GestureRecognizer.TapMaxXDelta / 3, 0); + await UITestHelper.WaitForIdle(); + mouse.MoveBy(GestureRecognizer.TapMaxXDelta / 3, 0); + await UITestHelper.WaitForIdle(); + + Assert.AreEqual(0, dragStartingCount); + Assert.AreEqual(0, dragEnterCount); + Assert.AreEqual(0, dragOverCount); + + mouse.MoveBy(GestureRecognizer.TapMaxXDelta / 3, 0); + await UITestHelper.WaitForIdle(); + + Assert.AreEqual(1, dragStartingCount); + Assert.AreEqual(1, dragEnterCount); + Assert.AreEqual(1, dragOverCount); + } + #endregion #endif } diff --git a/src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.Android.cs b/src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.Android.cs index 08d86ad9a1ae..6044821033f0 100644 --- a/src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.Android.cs +++ b/src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.Android.cs @@ -45,8 +45,10 @@ partial class PointerRoutedEventArgs // then _lastNativeEvent.lastArgs and LastPointerEvent will diverge. private static (MotionEvent nativeEvent, PointerRoutedEventArgs lastArgs)? _lastNativeEvent; - private readonly MotionEvent _nativeEvent; private readonly int _pointerIndex; + private readonly ulong _timestamp; + private readonly float _x; + private readonly float _y; private readonly UIElement _receiver; private readonly PointerPointProperties _properties; @@ -54,10 +56,15 @@ partial class PointerRoutedEventArgs internal PointerRoutedEventArgs(MotionEvent nativeEvent, int pointerIndex, UIElement originalSource, UIElement receiver) : this() { - _nativeEvent = nativeEvent; _pointerIndex = pointerIndex; _receiver = receiver; + // NOTE: do not keep a reference to nativeEvent, which will be reused by android's native event bubbling and will be mutated as it + // goes up through the visual tree. Instead, get whatever values you need here and keep them in fields. + _timestamp = ToTimeStamp(nativeEvent.EventTime); + _x = nativeEvent.GetX(pointerIndex); + _y = nativeEvent.GetY(pointerIndex); + // Here we assume that usually pointerId is 'PointerIndexShift' bits long (8 bits / 255 ids), // and that usually the deviceId is [0, something_not_too_big_hopefully_less_than_0x00ffffff]. // If deviceId is greater than 0x00ffffff, we might have a conflict but only in case of multi touch @@ -73,7 +80,7 @@ internal PointerRoutedEventArgs(MotionEvent nativeEvent, int pointerIndex, UIEle var isInContact = IsInContact(nativeEvent, basePointerType, nativePointerAction, nativePointerButtons); var keys = nativeEvent.MetaState.ToVirtualKeyModifiers(); - FrameId = (uint)_nativeEvent.EventTime; + FrameId = (uint)nativeEvent.EventTime; Pointer = new Pointer(pointerId, basePointerType, isInContact, isInRange: true); KeyModifiers = keys; OriginalSource = originalSource; @@ -101,38 +108,34 @@ internal PointerRoutedEventArgs(MotionEvent nativeEvent, int pointerIndex, UIEle }; } - _properties = GetProperties(nativePointerType, nativePointerAction, nativePointerButtons); // Last: we need the Pointer property to be set! + _properties = GetProperties(nativeEvent, nativePointerType, nativePointerAction, nativePointerButtons); // Last: we need the Pointer property to be set! } public PointerPoint GetCurrentPoint(UIElement relativeTo) { - var timestamp = ToTimeStamp(_nativeEvent.EventTime); var device = global::Windows.Devices.Input.PointerDevice.For((global::Windows.Devices.Input.PointerDeviceType)Pointer.PointerDeviceType); var (rawPosition, position) = GetPositions(relativeTo); - return new PointerPoint(FrameId, timestamp, device, Pointer.PointerId, rawPosition, position, Pointer.IsInContact, _properties); + return new PointerPoint(FrameId, _timestamp, device, Pointer.PointerId, rawPosition, position, Pointer.IsInContact, _properties); } private (Point raw, Point relative) GetPositions(UIElement relativeTo) { - var phyX = _nativeEvent.GetX(_pointerIndex); - var phyY = _nativeEvent.GetY(_pointerIndex); - Point raw, relative; if (relativeTo == null) // Relative to the window { var windowToReceiver = new int[2]; _receiver.GetLocationInWindow(windowToReceiver); - relative = new Point(phyX + windowToReceiver[0], phyY + windowToReceiver[1]).PhysicalToLogicalPixels(); + relative = new Point(_x + windowToReceiver[0], _y + windowToReceiver[1]).PhysicalToLogicalPixels(); } else if (relativeTo == _receiver) // Fast path { - relative = new Point(phyX, phyY).PhysicalToLogicalPixels(); + relative = new Point(_x, _y).PhysicalToLogicalPixels(); } else { - var posRelToReceiver = new Point(phyX, phyY).PhysicalToLogicalPixels(); + var posRelToReceiver = new Point(_x, _y).PhysicalToLogicalPixels(); var posRelToTarget = UIElement.GetTransform(from: _receiver, to: relativeTo).Transform(posRelToReceiver); relative = posRelToTarget; @@ -148,13 +151,13 @@ public PointerPoint GetCurrentPoint(UIElement relativeTo) var screenToReceiver = new int[2]; _receiver.GetLocationOnScreen(screenToReceiver); - raw = new Point(phyX + screenToReceiver[0], phyY + screenToReceiver[1]).PhysicalToLogicalPixels(); + raw = new Point(_x + screenToReceiver[0], _y + screenToReceiver[1]).PhysicalToLogicalPixels(); } return (raw, relative); } - private PointerPointProperties GetProperties(MotionEventToolType type, MotionEventActions action, MotionEventButtonState buttons) + private PointerPointProperties GetProperties(MotionEvent nativeEvent, MotionEventToolType type, MotionEventActions action, MotionEventButtonState buttons) { var props = new PointerPointProperties { @@ -193,16 +196,16 @@ private PointerPointProperties GetProperties(MotionEventToolType type, MotionEve // In that case we will still receive moves and up with the "StylusWithBarrel***" actions. props.IsBarrelButtonPressed = buttons.HasFlag(MotionEventButtonState.StylusPrimary); props.IsRightButtonPressed = Pointer.IsInContact; - props.Pressure = Math.Min(1f, _nativeEvent.GetPressure(_pointerIndex)); // Might exceed 1.0 on Android + props.Pressure = Math.Min(1f, nativeEvent.GetPressure(_pointerIndex)); // Might exceed 1.0 on Android break; case MotionEventToolType.Stylus: props.IsBarrelButtonPressed = buttons.HasFlag(MotionEventButtonState.StylusPrimary); props.IsLeftButtonPressed = Pointer.IsInContact; - props.Pressure = Math.Min(1f, _nativeEvent.GetPressure(_pointerIndex)); // Might exceed 1.0 on Android + props.Pressure = Math.Min(1f, nativeEvent.GetPressure(_pointerIndex)); // Might exceed 1.0 on Android break; case MotionEventToolType.Eraser: props.IsEraser = true; - props.Pressure = Math.Min(1f, _nativeEvent.GetPressure(_pointerIndex)); // Might exceed 1.0 on Android + props.Pressure = Math.Min(1f, nativeEvent.GetPressure(_pointerIndex)); // Might exceed 1.0 on Android break; default: @@ -210,7 +213,7 @@ private PointerPointProperties GetProperties(MotionEventToolType type, MotionEve } if (Android.OS.Build.VERSION.SdkInt >= BuildVersionCodes.M // ActionButton was introduced with API 23 (https://developer.android.com/reference/android/view/MotionEvent.html#getActionButton()) - && updates.TryGetValue(_nativeEvent.ActionButton, out var update)) + && updates.TryGetValue(nativeEvent.ActionButton, out var update)) { props.PointerUpdateKind = update; } diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs index 70802f509b74..6ce07e903384 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs @@ -788,6 +788,8 @@ private async Task StartDragAsyncCore(PointerPoint pointer }); XamlRoot.GetCoreDragDropManager(XamlRoot).DragStarted(dragInfo); + // Synchronously fire DragEnter+DragOver without waiting for another "mouse tick". This matches WinUI. + XamlRoot.VisualTree.ContentRoot.InputManager.DragDrop.ProcessMoved(ptArgs); var result = await asyncResult.Task;