Skip to content

Commit

Permalink
Merge pull request #18294 from unoplatform/dev/dr/inertia
Browse files Browse the repository at this point in the history
fix: Restore inertia
  • Loading branch information
MartinZikmund authored Jan 16, 2025
2 parents 8ee94d1 + b7ca2d9 commit f67d844
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 117 deletions.
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

0 comments on commit f67d844

Please sign in to comment.