Skip to content

Commit

Permalink
fix(pointers): [Skia] fix captured release to early when crossing bou…
Browse files Browse the repository at this point in the history
…nds of the capturing element + invalid dispatch to a transformed elements
  • Loading branch information
dr1rrb committed May 12, 2023
1 parent 57248d0 commit e1a1929
Show file tree
Hide file tree
Showing 12 changed files with 521 additions and 216 deletions.
14 changes: 0 additions & 14 deletions src/Uno.UI.RuntimeTests/Extensions/RectExtensions.cs

This file was deleted.

12 changes: 7 additions & 5 deletions src/Uno.UI.RuntimeTests/Helpers/UITestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public static void Drag(this IInjectedPointer pointer, Point from, Point to)

public class Finger : IInjectedPointer, IDisposable
{
private const int _defaultMoveSteps = 10;
private const uint _defaultMoveSteps = 10;

private readonly InputInjector _injector;
private readonly uint _id;
Expand All @@ -106,7 +106,7 @@ public void Press(Point position)
}

void IInjectedPointer.MoveTo(Point position) => MoveTo(position);
public void MoveTo(Point position, int steps = _defaultMoveSteps)
public void MoveTo(Point position, uint steps = _defaultMoveSteps)
{
if (_currentPosition is { } current)
{
Expand All @@ -116,7 +116,7 @@ public void MoveTo(Point position, int steps = _defaultMoveSteps)
}

void IInjectedPointer.MoveBy(double deltaX, double deltaY) => MoveBy(deltaX, deltaY);
public void MoveBy(double deltaX, double deltaY, int steps = _defaultMoveSteps)
public void MoveBy(double deltaX, double deltaY, uint steps = _defaultMoveSteps)
{
if (_currentPosition is { } current)
{
Expand Down Expand Up @@ -157,11 +157,13 @@ public static InjectedInputTouchInfo GetPress(uint id, Point position)
}
};

public static IEnumerable<InjectedInputTouchInfo> GetMove(Point fromPosition, Point toPosition, int steps = _defaultMoveSteps)
public static IEnumerable<InjectedInputTouchInfo> GetMove(Point fromPosition, Point toPosition, uint steps = _defaultMoveSteps)
{
steps += 1; // We need to send at least the final location, but steps refers to the number of intermediate points

var stepX = (toPosition.X - fromPosition.X) / steps;
var stepY = (toPosition.Y - fromPosition.Y) / steps;
for (var step = 0; step <= steps; step++)
for (var step = 1; step <= steps; step++)
{
yield return new()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
using Windows.UI.Input.Preview.Injection;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using FluentAssertions;
using Uno.UI.RuntimeTests.Extensions;
using Private.Infrastructure;
using Uno.Extensions;

namespace Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;

Expand Down Expand Up @@ -62,6 +67,67 @@ public async Task When_VisibilityChangesWhileDispatching_Then_RecomputeOriginalS
Assert.IsFalse(failed, "The pointer should not have been dispatched to the col2 as it has been set to visibility collapsed.");
}

[TestMethod]
#if !__SKIA__
[Ignore("Pointer injection supported only on skia for now.")]
#endif
public async Task When_LeaveElementWhileManipulating_Then_CaptureNotLost()
{
if (Private.Infrastructure.TestServices.WindowHelper.IsXamlIsland)
{
Assert.Inconclusive("Pointer injection is not supported yet on XamlIsland");
return;
}

Border sut;
TranslateTransform transform;
var ui = new Grid
{
Width = 128,
Height = 512,
Children =
{
(sut = new Border
{
Name = "SUT-Border",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Width = 16,
Height = Windows.UI.Input.GestureRecognizer.Manipulation.StartTouch.TranslateY * 3,
Background = new SolidColorBrush(Colors.DeepPink),
ManipulationMode = ManipulationModes.TranslateY,
RenderTransform = (transform = new TranslateTransform())
}),
}
};

await UITestHelper.Load(ui);

var exited = false;
sut.ManipulationDelta += (snd, e) => transform.Y = e.Cumulative.Translation.Y;
sut.PointerExited += (snd, e) => exited = true;

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

finger.Press(sut.GetAbsoluteBounds().GetCenter());

// Start manipulation
finger.MoveBy(0, 50, steps: 50);
transform.Y.Should().NotBe(0, "Manipulation should have started");

// Cause a fast move that will trigger a pointer leave
// Note: This might not be the WinUI behavior, should we receive a pointer leave when the element is capturing the pointer?
exited.Should().BeFalse();
finger.MoveBy(0, 50, steps: 0);
exited.Should().BeTrue();

// Confirm that even if we got a leave, pointer is still captured and we are still receiving manipulation events
var intermediatePosition = transform.Y;
finger.MoveBy(0, 50);
transform.Y.Should().Be(intermediatePosition + 50);
}

[TestMethod]
#if !__SKIA__
[Ignore("Pointer injection supported only on skia for now.")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using FluentAssertions.Execution;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Input.Preview.Injection;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
Expand All @@ -13,6 +15,7 @@
using Windows.UI.ViewManagement;
using static Private.Infrastructure.TestServices;
using Uno.Disposables;
using Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
{
Expand Down Expand Up @@ -667,5 +670,97 @@ public async Task When_DoublyNested_BringIntoView()
}

#endif

[TestMethod]
[RunsOnUIThread]
#if !__SKIA__
[Ignore("Pointer injection supported only on skia for now.")]
#endif
public async Task When_TouchScroll_Then_NestedElementReceivePointerEvents()
{
if (Private.Infrastructure.TestServices.WindowHelper.IsXamlIsland)
{
Assert.Inconclusive("Pointer injection is not supported yet on XamlIsland");
return;
}

var nested = new Border
{
Height = 4192,
Width = 256,
Background = new SolidColorBrush(Colors.DeepPink)
};
var sut = new ScrollViewer
{
Height = 512,
Width = 256,
Content = nested
};

var events = new List<string>();
nested.PointerEntered += (snd, e) => events.Add("enter");
nested.PointerExited += (snd, e) => events.Add("exited");
nested.PointerPressed += (snd, e) => events.Add("pressed");
nested.PointerReleased += (snd, e) => events.Add("release");
nested.PointerCanceled += (snd, e) => events.Add("cancel");

WindowHelper.WindowContent = new Grid { Children = { sut } };
await WindowHelper.WaitForLoaded(nested);
await WindowHelper.WaitForIdle();

var input = InputInjector.TryCreate() ?? throw new InvalidOperationException("Pointer injection not available on this platform.");
using var finger = input.GetFinger();

var sutLocation = sut.GetAbsoluteBounds().Location;
finger.Drag(sutLocation.Offset(5, 480), sutLocation.Offset(5, 5));

events.Should().BeEquivalentTo("enter", "pressed", "release", "exited"); // TODO: Exited is not injected by the InputInjector (but it is with native)
}

[TestMethod]
[RunsOnUIThread]
#if !__SKIA__
[Ignore("Pointer injection supported only on skia for now.")]
#endif
public async Task When_TouchTap_Then_NestedElementReceivePointerEvents()
{
if (Private.Infrastructure.TestServices.WindowHelper.IsXamlIsland)
{
Assert.Inconclusive("Pointer injection is not supported yet on XamlIsland");
return;
}

var nested = new Border
{
Height = 4192,
Width = 256,
Background = new SolidColorBrush(Colors.DeepPink)
};
var sut = new ScrollViewer
{
Height = 512,
Width = 256,
Content = nested
};

var events = new List<string>();
nested.PointerEntered += (snd, e) => events.Add("enter");
nested.PointerExited += (snd, e) => events.Add("exited");
nested.PointerPressed += (snd, e) => events.Add("pressed");
nested.PointerReleased += (snd, e) => events.Add("release");
nested.PointerCanceled += (snd, e) => events.Add("cancel");

WindowHelper.WindowContent = new Grid { Children = { sut } };
await WindowHelper.WaitForLoaded(nested);
await WindowHelper.WaitForIdle();

var input = InputInjector.TryCreate() ?? throw new InvalidOperationException("Pointer injection not available on this platform.");
using var finger = input.GetFinger();

var sutLocation = sut.GetAbsoluteBounds().Location;
finger.Drag(sutLocation.Offset(5, 480), sutLocation.Offset(5, 5));

events.Should().BeEquivalentTo("enter", "pressed", "release", "exited"); // TODO: Exited is not injected by the InputInjector (but it is with native)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel.Appointments;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.UI.RuntimeTests.Extensions;
using Uno.UI.RuntimeTests.Helpers;
using Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media.VisualTreeHelperPages;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Media;
using FluentAssertions;
using Uno.Extensions;
using Uno.UI.RuntimeTests.Tests.Uno_UI_Xaml_Core;
using static Private.Infrastructure.TestServices;

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Media
Expand Down Expand Up @@ -96,6 +101,89 @@ public async Task When_Nested_In_Native_View()
}
}

[TestMethod]
[RunsOnUIThread]
public async Task When_HitTestTransformedElement()
{
Border root, transformed, nested;
root = new Border
{
Name = "Root",
Width = 512,
Height = 512,
Background = new SolidColorBrush(Colors.DeepSkyBlue),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Child = transformed = new Border
{
Name = "Transformed",
Width = 128,
Height = 128,
Background = new SolidColorBrush(Colors.DeepPink),
RenderTransform = new TranslateTransform { X = 128, Y = 128 },
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = nested = new Border
{
Name = "Nested",
Width = 64,
Height = 64,
Background = new SolidColorBrush(Colors.Chartreuse),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
}
}
};

var position = (await UITestHelper.Load(root)).Location;

VisualTreeHelper.HitTest(position.Offset(256), root.XamlRoot).element!.Name!.Should().Be("Root");
VisualTreeHelper.HitTest(position.Offset(256 + 65), root.XamlRoot).element!.Name!.Should().Be("Transformed");
VisualTreeHelper.HitTest(position.Offset(256 + 128), root.XamlRoot).element!.Name!.Should().Be("Nested");
}

[TestMethod]
[RunsOnUIThread]
public async Task When_HitTestScaledElement()
{
Border root, transformed, nested;
root = new Border
{
Name = "Root",
Width = 512,
Height = 512,
Background = new SolidColorBrush(Colors.DeepSkyBlue),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Child = transformed = new Border
{
Name = "Transformed",
Width = 128,
Height = 128,
Background = new SolidColorBrush(Colors.DeepPink),
RenderTransform = new ScaleTransform { ScaleX = 2, ScaleY = 2 },
RenderTransformOrigin = new Point(.5,.5),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = nested = new Border
{
Name = "Nested",
Width = 64,
Height = 64,
Background = new SolidColorBrush(Colors.Chartreuse),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
}
}
};

var position = (await UITestHelper.Load(root)).Location;

VisualTreeHelper.HitTest(position.Offset(128 - 5), root.XamlRoot).element!.Name!.Should().Be("Root");
VisualTreeHelper.HitTest(position.Offset(128 + 5), root.XamlRoot).element!.Name!.Should().Be("Transformed");
VisualTreeHelper.HitTest(position.Offset(256 - 60), root.XamlRoot).element!.Name!.Should().Be("Nested");
}

private static IEnumerable<Point> GetPointsInside(Rect rect, double perimeterOffset)
{
if (perimeterOffset >= rect.Width || perimeterOffset >= rect.Height)
Expand Down

0 comments on commit e1a1929

Please sign in to comment.