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(drag&drop): when a child and a parent subscribe to dragging, only fire on the child #18795

56 changes: 56 additions & 0 deletions src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_UIElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,62 @@ public async Task When_DragEnter_Fires_Along_DragStarting()
Assert.AreEqual(1, dragOverCount);
}

[TestMethod]
[RunsOnUIThread]
[DataRow(true)]
[DataRow(false)]
#if !HAS_INPUT_INJECTOR
[Ignore("InputInjector is not supported on this platform.")]
#endif
public async Task When_CanDrag_Child_And_Parent(bool childCanDrag)
{
var child = new Rectangle
{
Fill = new SolidColorBrush(Microsoft.UI.Colors.LightBlue),
Width = 100,
Height = 80,
CanDrag = childCanDrag
};
var parent = new Frame
{
Background = new SolidColorBrush(Microsoft.UI.Colors.LightCoral),
Width = 400,
Height = 300,
CanDrag = true,
Content = child
};

var parentDragStartingCount = 0;
var childDragStartingCount = 0;
parent.DragStarting += (_, _) => parentDragStartingCount++;
child.DragStarting += (_, _) => childDragStartingCount++;

await UITestHelper.Load(parent);

var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector");
using var mouse = injector.GetMouse();

mouse.MoveTo(child.GetAbsoluteBoundsRect().GetCenter());
await UITestHelper.WaitForIdle();
mouse.Press();
await UITestHelper.WaitForIdle();
mouse.MoveTo(child.GetAbsoluteBoundsRect().GetCenter() + new Windows.Foundation.Point(20, 0), steps: 10);
await UITestHelper.WaitForIdle();
mouse.Release();
await UITestHelper.WaitForIdle();

if (childCanDrag)
{
Assert.AreEqual(1, childDragStartingCount);
Assert.AreEqual(0, parentDragStartingCount);
}
else
{
Assert.AreEqual(0, childDragStartingCount);
Assert.AreEqual(1, parentDragStartingCount);
}
}

#endregion
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4654,7 +4654,6 @@ public async Task When_UpdateLayout_In_DragDropping()
Content = new TextBlock
{
AllowDrop = true,
CanDrag = true,
Height = 100,
Text = i.ToString()
}
Expand Down
13 changes: 6 additions & 7 deletions src/Uno.UI/UI/Input/GestureRecognizer.Gesture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ namespace Windows.UI.Input
{
public partial class GestureRecognizer
{
public static bool IsOutOfTapRange(Point p1, Point p2)
=> Math.Abs(p1.X - p2.X) > TapMaxXDelta
|| Math.Abs(p1.Y - p2.Y) > TapMaxYDelta;

/// <summary>
/// This is the state machine which handles the gesture ([Double|Right]Tapped and Holding gestures)
/// </summary>
internal class Gesture
private class Gesture
{
private readonly GestureRecognizer _recognizer;
private DispatcherQueueTimer? _holdingTimer;
Expand Down Expand Up @@ -57,7 +61,6 @@ public Gesture(GestureRecognizer recognizer, PointerPoint down)
{
_recognizer = recognizer;
Settings = recognizer._gestureSettings & GestureSettingsHelper.SupportedGestures; // Keep only flags of supported gestures, so we can more quickly disable us if possible
Settings |= GestureSettings.Tap; // On WinUI, Tap is always raised no matter the flag set on the recognizer

Down = down;
PointerIdentifier = GetPointerIdentifier(down);
Expand Down Expand Up @@ -130,7 +133,7 @@ public void PreventGestures(GestureSettings gestures)
}

Settings &= ~gestures;
if ((Settings & GestureSettingsHelper.SupportedGestures) == GestureSettings.None)
if (Settings == GestureSettings.None)
{
IsCompleted = true;
}
Expand Down Expand Up @@ -392,10 +395,6 @@ private static bool IsRightTapGesture(Gesture points, out bool isLongPress)

private static bool IsLongPress(PointerPoint down, PointerPoint current)
=> current.Timestamp - down.Timestamp > HoldMinDelayTicks;

public static bool IsOutOfTapRange(Point p1, Point p2)
=> Math.Abs(p1.X - p2.X) > TapMaxXDelta
|| Math.Abs(p1.Y - p2.Y) > TapMaxYDelta;
#endregion
}
}
Expand Down
35 changes: 24 additions & 11 deletions src/Uno.UI/UI/Input/GestureRecognizer.Manipulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ private enum ManipulationState
public bool IsTranslateYEnabled => _isTranslateYEnabled;
public bool IsRotateEnabled => _isRotateEnabled;
public bool IsScaleEnabled => _isScaleEnabled;
public bool IsDraggingEnabled => _isDraggingEnable;

internal static void AddPointer(GestureRecognizer recognizer, PointerPoint pointer)
{
Expand Down Expand Up @@ -576,7 +577,7 @@ private bool IsBeginningOfDragManipulation()
// those thresholds are lower than a Tap (and actually only 1px), which does not math the UWP behavior.
var down = _origins.Pointer1;
var current = _currents.Pointer1;
var isOutOfRange = Gesture.IsOutOfTapRange(down.Position, current.Position);
var isOutOfRange = IsOutOfTapRange(down.Position, current.Position);

switch (_deviceType)
{
Expand Down Expand Up @@ -607,6 +608,19 @@ private bool IsBeginningOfDragManipulation()
}
}

public void DisableDragging()
{
StopDragTimer();
if (_state is ManipulationState.Starting)
{
_isDraggingEnable = false;
}
else if (_state is ManipulationState.Started && IsDragManipulation)
{
Complete();
}
}

private bool ShouldStartInertia(ManipulationVelocities velocities)
=> _inertia is null
&& !IsDragManipulation
Expand Down Expand Up @@ -652,24 +666,23 @@ static bool isAngleNearYAxis(double slope)
=> Math.Abs(slope) >= Math.Tan(67.5 * Math.PI / 180);
}

internal struct Thresholds
{
public double TranslateX;
public double TranslateY;
public double Rotate; // Degrees
public double Expansion;
}
internal readonly record struct Thresholds(
double TranslateX,
double TranslateY,
double Rotate, // Degrees
double Expansion
);

// WARNING: This struct is ** MUTABLE **
private struct Points
{
public PointerPoint Pointer1;
private PointerPoint? _pointer2;

public ulong Timestamp;
public ulong Timestamp; // The timestamp of the latest pointer update to either pointer
public Point Center; // This is the center in ** absolute ** coordinates spaces (i.e. relative to the screen)
public float Distance;
public double Angle;
public float Distance; // The distance between the 2 points, or zero if !HasPointer2
public double Angle; // The angle between the horizontal axis and the line segment formed by the 2 points, or zero if !HasPointer2

public bool HasPointer2 => _pointer2 != null;

Expand Down
22 changes: 16 additions & 6 deletions src/Uno.UI/UI/Input/GestureRecognizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public partial class GestureRecognizer
private readonly Logger _log;
private IDictionary<uint, Gesture> _gestures = new Dictionary<uint, Gesture>(_defaultGesturesSize);
private Manipulation _manipulation;
private GestureSettings _gestureSettings;
private GestureSettings _gestureSettings = GestureSettings.Tap; // On WinUI, Tap is always raised no matter the flag set on the recognizer
private bool _isManipulationOrDragEnabled;

public GestureSettings GestureSettings
Expand Down Expand Up @@ -194,16 +194,26 @@ public void CompleteGesture()
}

/// <returns>The set of events that can be raised by this recognizer for this pointer ID</returns>
internal GestureSettings PreventEvents(uint pointerId, GestureSettings events)
internal GestureSettings PreventEvents(PointerIdentifier pointerId, GestureSettings events)
{
if (_gestures.TryGetValue(pointerId, out var gesture))
if ((events & GestureSettings.Drag) != 0 && (_manipulation?.IsActive(pointerId) ?? false))
{
gesture.PreventGestures(events & GestureSettingsHelper.SupportedGestures);
_manipulation?.DisableDragging();
}

return gesture.Settings;
var ret = GestureSettings.None;
if (_gestures.TryGetValue(pointerId.Id, out var gesture))
{
gesture.PreventGestures(events);
ret |= gesture.Settings;
}

if (_manipulation is not null && _manipulation.IsActive(pointerId) && _manipulation.IsDraggingEnabled)
{
ret |= GestureSettings.Drag;
}

return GestureSettings.None;
return ret;
}

#region Manipulations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private static bool IsMultiTapGesture((ulong id, ulong ts, Point position) previ

return previousTap.id == currentId
&& currentTs - previousTap.ts <= GestureRecognizer.MultiTapMaxDelayTicks
&& !GestureRecognizer.Gesture.IsOutOfTapRange(previousTap.position, currentPosition);
&& !GestureRecognizer.IsOutOfTapRange(previousTap.position, currentPosition);
}

partial void OnPointerPressedPartial(PointerRoutedEventArgs args)
Expand Down
29 changes: 23 additions & 6 deletions src/Uno.UI/UI/Xaml/UIElement.Pointers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Windows.ApplicationModel.DataTransfer.DragDrop;
using Windows.ApplicationModel.DataTransfer.DragDrop.Core;
using Windows.Devices.Haptics;
using Windows.Devices.Input;
using Windows.Foundation;
using Windows.UI.Core;
using Microsoft.UI.Xaml.Input;
Expand All @@ -22,12 +23,12 @@
using Uno.UI.Extensions;
using Uno.UI.Xaml;
using Uno.UI.Xaml.Core;
using PointerDeviceType = Windows.Devices.Input.PointerDeviceType;

#if HAS_UNO_WINUI
using Microsoft.UI.Input;
#else
using Windows.UI.Input;
using Windows.Devices.Input;
#endif

namespace Microsoft.UI.Xaml
Expand Down Expand Up @@ -558,19 +559,31 @@ partial void PrepareManagedGestureEventBubbling(RoutedEvent routedEvent, ref Rou
{
if (routedEvent == TappedEvent)
{
GestureRecognizer.PreventEvents(((TappedRoutedEventArgs)args).PointerId, GestureSettings.Tap);
var tappedArgs = (TappedRoutedEventArgs)args;
GestureRecognizer.PreventEvents(
new PointerIdentifier((PointerDeviceType)tappedArgs.PointerDeviceType, tappedArgs.PointerId),
GestureSettings.Tap);
}
else if (routedEvent == DoubleTappedEvent)
{
GestureRecognizer.PreventEvents(((DoubleTappedRoutedEventArgs)args).PointerId, GestureSettings.DoubleTap);
var doubleTappedArgs = (DoubleTappedRoutedEventArgs)args;
GestureRecognizer.PreventEvents(
new PointerIdentifier((PointerDeviceType)doubleTappedArgs.PointerDeviceType, doubleTappedArgs.PointerId),
GestureSettings.DoubleTap);
}
else if (routedEvent == RightTappedEvent)
{
GestureRecognizer.PreventEvents(((RightTappedRoutedEventArgs)args).PointerId, GestureSettings.RightTap);
var rightTappedArgs = (RightTappedRoutedEventArgs)args;
GestureRecognizer.PreventEvents(
new PointerIdentifier((PointerDeviceType)rightTappedArgs.PointerDeviceType, rightTappedArgs.PointerId),
GestureSettings.RightTap);
}
else if (routedEvent == HoldingEvent)
{
GestureRecognizer.PreventEvents(((HoldingRoutedEventArgs)args).PointerId, GestureSettings.Hold);
var holdingArgs = (HoldingRoutedEventArgs)args;
GestureRecognizer.PreventEvents(
new PointerIdentifier((PointerDeviceType)holdingArgs.PointerDeviceType, holdingArgs.PointerId),
GestureSettings.Hold);
}
}
}
Expand Down Expand Up @@ -606,7 +619,7 @@ private void UpdateRaisedGestureEventsFlag(PointerRoutedEventArgs args)
}

var pointerId = args.Pointer.PointerId;
args.GestureEventsAlreadyRaised |= GestureRecognizer.PreventEvents(pointerId, args.GestureEventsAlreadyRaised);
args.GestureEventsAlreadyRaised |= GestureRecognizer.PreventEvents(new PointerIdentifier((PointerDeviceType)args.Pointer.PointerDeviceType, args.Pointer.PointerId), args.GestureEventsAlreadyRaised);
}
#endregion

Expand Down Expand Up @@ -896,7 +909,11 @@ partial void PrepareManagedPointerEventBubbling(RoutedEvent routedEvent, ref Rou
// ptArgs.Pointer.IsInRange && ptArgs.IsPointCoordinatesOver(this) (and probably share it on all platforms).
var isOver = ptArgs.Pointer.IsInRange && (ptArgs.Pointer.PointerDeviceType, ptArgs.Pointer.IsInContact) switch
{
#if HAS_UNO_WINUI
(global::Microsoft.UI.Input.PointerDeviceType.Touch, false) => false,
#else
(PointerDeviceType.Touch, false) => false,
#endif
_ => ptArgs.IsPointCoordinatesOver(this),
};
#endif
Expand Down
Loading