Skip to content

Commit

Permalink
Merge pull request #18812 from ramezgerges/dragenter_with_dragstarting
Browse files Browse the repository at this point in the history
fix(dragdrop): fire DragEnter and DragOver when firing DragStarting
  • Loading branch information
ramezgerges authored Nov 27, 2024
2 parents d12128a + 75adc10 commit 164d808
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 20 deletions.
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 @@ -1819,7 +1825,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 @@ -1829,10 +1835,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

0 comments on commit 164d808

Please sign in to comment.