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: Restore inertia #18294

Merged
merged 13 commits into from
Jan 16, 2025
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
<Page
x:Class="UITests.Windows_UI_Input.GestureRecognizerTests.Manipulation_Inertia"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.Windows_UI_Input.GestureRecognizerTests"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
x:Class="UITests.Windows_UI_Input.GestureRecognizerTests.Manipulation_Inertia"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:runtimeTests="using:RuntimeTests.Tests.Windows_UI_Xaml_Input.TestPages"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

<Grid
Background="Orange"
ManipulationMode="All"
ManipulationInertiaStarting="InertiaStarting"
ManipulationDelta="ManipDelta">

<Rectangle
x:Name="_element"
Width="200"
Height="200"
Fill="DeepPink"
RenderTransformOrigin=".5,.5" />

<Button VerticalAlignment="Top" HorizontalAlignment="Right" Content="Reset" Click="Reset" />
</Grid>
<runtimeTests:Manipulation_Inertia />
</Page>
Original file line number Diff line number Diff line change
@@ -1,76 +1,16 @@
using System;
using System.Linq;
using Windows.Foundation;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Uno.UI.Samples.Controls;

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

namespace UITests.Windows_UI_Input.GestureRecognizerTests
{
[Sample("Gesture Recognizer")]
[Sample("Gesture Recognizer", IsManualTest = true)]
public sealed partial class Manipulation_Inertia : Page
{
public Manipulation_Inertia()
{
this.InitializeComponent();
}

private long _inertiaStart = 0;

private void InertiaStarting(object sender, ManipulationInertiaStartingRoutedEventArgs e)
{
_inertiaStart = DateTimeOffset.UtcNow.Ticks;

//e.TranslationBehavior.DesiredDisplacement = 5;
//e.TranslationBehavior.DesiredDeceleration = .00001;

Log(@$"[INERTIA] Inertia starting: {F(default, e.Delta, e.Cumulative, e.Velocities)}
tr: ↘={e.TranslationBehavior.DesiredDeceleration} | ⌖={e.TranslationBehavior.DesiredDisplacement}
θ: ↘={e.RotationBehavior.DesiredDeceleration} | ⌖={e.RotationBehavior.DesiredRotation}
s: ↘={e.ExpansionBehavior.DesiredDeceleration} | ⌖={e.ExpansionBehavior.DesiredExpansion}");
}

private void ManipDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
if (!(_element.RenderTransform is CompositeTransform tr))
{
_element.RenderTransform = tr = new CompositeTransform();
}

tr.TranslateX = e.Cumulative.Translation.X;
tr.TranslateY = e.Cumulative.Translation.Y;
tr.Rotation = e.Cumulative.Rotation;
tr.ScaleX = e.Cumulative.Scale;
tr.ScaleY = e.Cumulative.Scale;

Log(
$"[DELTA] {F(e.Position, e.Delta, e.Cumulative, e.Velocities)} "
+ (e.IsInertial ? $"{TimeSpan.FromTicks(DateTimeOffset.UtcNow.Ticks - _inertiaStart).TotalMilliseconds} ms" : ""));
}

private void Reset(object sender, RoutedEventArgs e)
{
_element.RenderTransform = new CompositeTransform();
}

private static string F(Point position, ManipulationDelta delta, ManipulationDelta cumulative, ManipulationVelocities velocities)
=> $"@=[{position.X:000.00},{position.Y:000.00}] "
+ $"| X=(Σ:{cumulative.Translation.X:' '000.00;'-'000.00} / Δ:{delta.Translation.X:' '00.00;'-'00.00} / c:{velocities.Linear.X:F2}) "
+ $"| Y=(Σ:{cumulative.Translation.Y:' '000.00;'-'000.00} / Δ:{delta.Translation.Y:' '00.00;'-'00.00} / c:{velocities.Linear.Y:F2}) "
+ $"| θ=(Σ:{cumulative.Rotation:' '000.00;'-'000.00} / Δ:{delta.Rotation:' '00.00;'-'00.00} / c:{velocities.Angular:F2}) "
+ $"| s=(Σ:{cumulative.Scale:000.00} / Δ:{delta.Scale:00.00} / c:{velocities.Expansion:F2}) "
+ $"| e=(Σ:{cumulative.Expansion:' '000.00;'-'000.00} / Δ:{delta.Expansion:' '00.00;'-'00.00} / c:{velocities.Expansion:F2})";

private static void Log(string text)
=> global::System.Diagnostics.Debug.WriteLine(text);
}
}
51 changes: 36 additions & 15 deletions src/Uno.UI.RuntimeTests/Helpers/UITestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
#if !HAS_UNO
using System.Runtime.InteropServices;
#endif

#if HAS_UNO_WINUI || WINAPPSDK
using PointerDeviceType = Microsoft.UI.Input.PointerDeviceType;
#else
using PointerDeviceType = Windows.Devices.Input.PointerDeviceType;
#endif

namespace Uno.UI.RuntimeTests.Helpers;

// Note: This file contains a bunch of helpers that are expected to be moved to the test engine among the pointer injection work
Expand Down Expand Up @@ -323,6 +330,16 @@ public string TemplateId

public static class InputInjectorExtensions
{
public static IInjectedPointer GetPointer(this InputInjector injector, PointerDeviceType pointer)
=> pointer switch
{
PointerDeviceType.Touch => GetFinger(injector),
#if !WINAPPSDK
PointerDeviceType.Mouse => GetMouse(injector),
#endif
_ => throw new NotSupportedException($"Injection of {pointer} is not supported on this platform.")
};

public static Finger GetFinger(this InputInjector injector, uint id = 42)
=> new(injector, id);

Expand All @@ -336,7 +353,7 @@ public interface IInjectedPointer
{
void Press(Point position);

void MoveTo(Point position);
void MoveTo(Point position, uint? steps = null, uint? stepOffsetInMilliseconds = null);

void MoveBy(double deltaX = 0, double deltaY = 0);

Expand Down Expand Up @@ -366,17 +383,18 @@ public static void Press(this IInjectedPointer pointer, double x, double y)
public static void MoveTo(this IInjectedPointer pointer, double x, double y)
=> pointer.MoveTo(new(x, y));

public static void Drag(this IInjectedPointer pointer, Point from, Point to)
public static void Drag(this IInjectedPointer pointer, Point from, Point to, uint? steps = null, uint? stepOffsetInMilliseconds = null)
{
pointer.Press(from);
pointer.MoveTo(to);
pointer.MoveTo(to, steps, stepOffsetInMilliseconds);
pointer.Release();
}
}

public partial class Finger : IInjectedPointer, IDisposable
{
private const uint _defaultMoveSteps = 10;
private const uint _defaultStepOffsetInMilliseconds = 1;

private readonly InputInjector _injector;
private readonly uint _id;
Expand All @@ -400,12 +418,13 @@ public void Press(Point position)
}
}

void IInjectedPointer.MoveTo(Point position) => MoveTo(position);
public void MoveTo(Point position, uint steps = _defaultMoveSteps)
void IInjectedPointer.MoveTo(Point position, uint? steps, uint? stepOffsetInMilliseconds) =>
MoveTo(position, steps ?? _defaultMoveSteps, stepOffsetInMilliseconds ?? _defaultStepOffsetInMilliseconds);
public void MoveTo(Point position, uint steps = _defaultMoveSteps, uint stepOffsetInMilliseconds = _defaultStepOffsetInMilliseconds)
{
if (_currentPosition is { } current)
{
Inject(GetMove(current, position, steps));
Inject(GetMove(current, position, steps, stepOffsetInMilliseconds));
_currentPosition = position;
}
}
Expand Down Expand Up @@ -448,7 +467,7 @@ public static InjectedInputTouchInfo GetPress(uint id, Point position)
}
};

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

Expand All @@ -460,6 +479,7 @@ public static IEnumerable<InjectedInputTouchInfo> GetMove(Point fromPosition, Po
{
PointerInfo = new()
{
TimeOffsetInMilliseconds = stepOffsetInMilliseconds,
PixelLocation = At(fromPosition.X + step * stepX, fromPosition.Y + step * stepY),
PointerOptions = InjectedInputPointerOptions.Update
| InjectedInputPointerOptions.FirstButton
Expand Down Expand Up @@ -602,11 +622,10 @@ public void ReleaseAny()
}

public void MoveBy(double deltaX, double deltaY)
=> Inject(GetMoveBy(deltaX, deltaY));
=> Inject(GetMoveBy(deltaX, deltaY, 1));

void IInjectedPointer.MoveTo(Point position) => MoveTo(position);
public void MoveTo(Point position, uint? steps = null)
=> Inject(GetMoveTo(position.X, position.Y, steps));
public void MoveTo(Point position, uint? steps = null, uint? stepOffsetInMilliseconds = null)
=> Inject(GetMoveTo(position.X, position.Y, steps, stepOffsetInMilliseconds));

public void WheelUp() => Wheel(ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta);
public void WheelDown() => Wheel(-ScrollContentPresenter.ScrollViewerDefaultMouseWheelDelta);
Expand All @@ -616,14 +635,16 @@ public void MoveTo(Point position, uint? steps = null)
public void Wheel(double delta, bool isHorizontal = false, uint steps = 1)
=> Inject(GetWheel(delta, isHorizontal, steps));

private IEnumerable<InjectedInputMouseInfo> GetMoveTo(double x, double y, uint? steps)
private IEnumerable<InjectedInputMouseInfo> GetMoveTo(double x, double y, uint? steps, uint? stepOffsetInMilliseconds = null)
{
var x0 = Current.X;
var y0 = Current.Y;
var deltaX = x - x0;
var deltaY = y - y0;

steps ??= (uint)Math.Min(Math.Max(Math.Abs(deltaX), Math.Abs(deltaY)), 512);
stepOffsetInMilliseconds ??= 1;

if (steps is 0)
{
yield break;
Expand All @@ -641,7 +662,7 @@ private IEnumerable<InjectedInputMouseInfo> GetMoveTo(double x, double y, uint?
var newPositionX = (int)Math.Round(x0 + i * stepX);
var newPositionY = (int)Math.Round(y0 + i * stepY);

yield return GetMoveBy(newPositionX - prevPositionX, newPositionY - prevPositionY);
yield return GetMoveBy(newPositionX - prevPositionX, newPositionY - prevPositionY, stepOffsetInMilliseconds.Value);

prevPositionX = newPositionX;
prevPositionY = newPositionY;
Expand Down Expand Up @@ -677,12 +698,12 @@ private static InjectedInputMouseInfo GetRightPress()
MouseOptions = InjectedInputMouseOptions.RightDown,
};

private static InjectedInputMouseInfo GetMoveBy(double deltaX, double deltaY)
private static InjectedInputMouseInfo GetMoveBy(double deltaX, double deltaY, uint stepOffsetInMilliseconds)
=> new()
{
DeltaX = (int)deltaX,
DeltaY = (int)deltaY,
TimeOffsetInMilliseconds = 1,
TimeOffsetInMilliseconds = stepOffsetInMilliseconds,
MouseOptions = InjectedInputMouseOptions.MoveNoCoalesce,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public async Task When_UserInteraction()

var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector");
var finger = injector.GetFinger();
finger.Drag(new(position.Left + 50, position.Top + 50), new(position.Left + 100, position.Top + 50));
finger.Drag(new(position.Left + 50, position.Top + 50), new(position.Left + 100, position.Top + 50), stepOffsetInMilliseconds: 0);

string logs = await WaitTrackerLogs(tracker);
var helper = new TrackerAssertHelper(logs);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Input.Preview.Injection;
using FluentAssertions;
using RuntimeTests.Tests.Windows_UI_Xaml_Input.TestPages;
using Uno.Extensions;
using Uno.UI.RuntimeTests.Helpers;

#if WINAPPSDK
using Uno.UI.Toolkit.Extensions;
#endif

#if HAS_UNO_WINUI || WINAPPSDK
using PointerDeviceType = Microsoft.UI.Input.PointerDeviceType;
#else
using PointerDeviceType = Windows.Devices.Input.PointerDeviceType;
#endif

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Input
{
[TestClass]
public class Given_GestureRecognizer
{
[TestMethod]
[RunsOnUIThread]
#if !HAS_INPUT_INJECTOR || (!HAS_UNO_WINUI && !WINAPPSDK) // Requires pointer injection and WinUI API
[Ignore("This test is not supported on this platform.")]
#endif
#if !WINAPPSDK
[DataRow(PointerDeviceType.Mouse)]
#endif
[DataRow(PointerDeviceType.Touch)]
public async Task When_ManipulateWithVelocity_Then_InertiaKicksIn(PointerDeviceType type)
{
var sample = new Manipulation_Inertia();
await UITestHelper.Load(sample);

var started = false;
var completed = new TaskCompletionSource();
var completedTimeout = new CancellationTokenSource(15000);
using var _ = completedTimeout.Token.Register(() => completed.TrySetException(new TimeoutException("Cannot get complete in given delay.")));
sample.IsRunningChanged += (snd, isRunning) =>
{
if (isRunning)
{
started = true;
}
else
{
completed.TrySetResult();
}
};

var origin = sample.Element.GetAbsoluteBounds().GetCenter();
var pointer = (InputInjector.TryCreate() ?? throw new InvalidOperationException("Input injection is not supported on this device")).GetPointer(type);
pointer.Press(origin);
pointer.MoveTo(new Point(origin.X, origin.Y + 25), steps: 100); // 1 step per ms!

started.Should().Be(true, "Manipulation should have started.");

pointer.Release();

await completed.Task;

sample.Validate().Should().BeTrue();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Page
x:Class="RuntimeTests.Tests.Windows_UI_Xaml_Input.TestPages.Manipulation_Inertia"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">

<Grid Background="Orange">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<Grid
ManipulationMode="All"
ManipulationStarting="ManipStarting"
ManipulationInertiaStarting="InertiaStarting"
ManipulationDelta="ManipDelta"
ManipulationCompleted="ManipCompleted"
Background="Transparent">

<Rectangle
x:Name="_element"
Width="200"
Height="200"
Fill="DeepPink"
RenderTransformOrigin=".5,.5" />

<TextBlock x:Name="Output" VerticalAlignment="Top" FontSize="8" Text="Swipe the pink square, observe it to move WITH INERTIA, then click the validate button to ensure logs are valid." />
</Grid>

<StackPanel Grid.Column="1" Background="#33000000" VerticalAlignment="Stretch" Padding="5" Spacing="5">
<Button HorizontalAlignment="Stretch" Content="Reset" Click="Reset" />
<Button HorizontalAlignment="Stretch" Content="Validate" Click="Validate" />
</StackPanel>
</Grid>
</Page>
Loading
Loading