Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dragdrop): fire DragEnter and DragOver when firing DragStarting #18812

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_UIElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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
}
Expand Down
39 changes: 21 additions & 18 deletions src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,26 @@ 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;

internal bool HasPressedButton => _properties.HasPressedButton;

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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -193,24 +196,24 @@ 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:
break;
}

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;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Uno.UI/UI/Xaml/UIElement.Pointers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,8 @@ private async Task<DataPackageOperation> 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;

Expand Down
Loading