diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index d0ab0a0c8b..5f01c233b8 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -339,7 +339,7 @@ namespace Avalonia.Controls
var point = e.GetPointerPoint(null);
RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
- point.Properties, e.InputModifiers));
+ e.Timestamp, point.Properties, e.InputModifiers));
}
///
@@ -349,7 +349,7 @@ namespace Avalonia.Controls
var point = e.GetPointerPoint(null);
RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
- point.Properties, e.InputModifiers));
+ e.Timestamp, point.Properties, e.InputModifiers));
}
///
diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
index e560230bfe..4f3c7c0bba 100644
--- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
+++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
@@ -1,5 +1,7 @@
using System;
+using System.Diagnostics;
using Avalonia.Interactivity;
+using Avalonia.Threading;
namespace Avalonia.Input.GestureRecognizers
{
@@ -16,6 +18,10 @@ namespace Avalonia.Input.GestureRecognizers
private bool _canVerticallyScroll;
private int _gestureId;
+ // Movement per second
+ private Vector _inertia;
+ private ulong? _lastMoveTimestamp;
+
///
/// Defines the property.
///
@@ -63,14 +69,19 @@ namespace Avalonia.Input.GestureRecognizers
{
if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch)
{
+ EndGesture();
_tracking = e.Pointer;
- _scrolling = false;
+ _gestureId = ScrollGestureEventArgs.GetNextFreeId();;
_trackedRootPoint = e.GetPosition(null);
}
}
// Arbitrary chosen value, probably need to move that to platform settings or something
private const double ScrollStartDistance = 30;
+
+ // Pixels per second speed that is considered to be the stop of inertiall scroll
+ private const double InertialScrollSpeedEnd = 5;
+
public void PointerMoved(PointerEventArgs e)
{
if (e.Pointer == _tracking)
@@ -85,14 +96,20 @@ namespace Avalonia.Input.GestureRecognizers
if (_scrolling)
{
_actions.Capture(e.Pointer, this);
- _gestureId = ScrollGestureEventArgs.GetNextFreeId();
}
}
if (_scrolling)
{
var vector = _trackedRootPoint - rootPoint;
+ var elapsed = _lastMoveTimestamp.HasValue ?
+ TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) :
+ TimeSpan.Zero;
+
+ _lastMoveTimestamp = e.Timestamp;
_trackedRootPoint = rootPoint;
+ if (elapsed.TotalSeconds > 0)
+ _inertia = vector / elapsed.TotalSeconds;
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true;
}
@@ -109,8 +126,11 @@ namespace Avalonia.Input.GestureRecognizers
_tracking = null;
if (_scrolling)
{
+ _inertia = default;
_scrolling = false;
_target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
+ _gestureId = 0;
+ _lastMoveTimestamp = null;
}
}
@@ -118,11 +138,45 @@ namespace Avalonia.Input.GestureRecognizers
public void PointerReleased(PointerReleasedEventArgs e)
{
- // TODO: handle inertia
if (e.Pointer == _tracking && _scrolling)
{
e.Handled = true;
- EndGesture();
+ if (_inertia == default
+ || e.Timestamp == 0
+ || _lastMoveTimestamp == 0
+ || e.Timestamp - _lastMoveTimestamp > 200)
+ EndGesture();
+ else
+ {
+ var savedGestureId = _gestureId;
+ var st = Stopwatch.StartNew();
+ var lastTime = TimeSpan.Zero;
+ DispatcherTimer.Run(() =>
+ {
+ // Another gesture has started, finish the current one
+ if (_gestureId != savedGestureId)
+ {
+ return false;
+ }
+
+ var elapsedSinceLastTick = st.Elapsed - lastTime;
+ lastTime = st.Elapsed;
+
+ var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds);
+ var distance = speed * elapsedSinceLastTick.TotalSeconds;
+ _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));
+
+
+
+ if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
+ {
+ EndGesture();
+ return false;
+ }
+
+ return true;
+ }, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background);
+ }
}
}
}
diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs
index 05840660e2..a62a4dc62f 100644
--- a/src/Avalonia.Input/MouseDevice.cs
+++ b/src/Avalonia.Input/MouseDevice.cs
@@ -89,11 +89,11 @@ namespace Avalonia.Input
{
if (_pointer.Captured == null)
{
- SetPointerOver(this, root, clientPoint, InputModifiers.None);
+ SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, InputModifiers.None);
}
else
{
- SetPointerOver(this, root, _pointer.Captured, InputModifiers.None);
+ SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, InputModifiers.None);
}
}
}
@@ -121,13 +121,13 @@ namespace Avalonia.Input
switch (e.Type)
{
case RawPointerEventType.LeaveWindow:
- LeaveWindow(mouse, e.Root, e.InputModifiers);
+ LeaveWindow(mouse, e.Timestamp, e.Root, e.InputModifiers);
break;
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown:
if (ButtonCount(props) > 1)
- e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
props, e.InputModifiers);
@@ -136,25 +136,25 @@ namespace Avalonia.Input
case RawPointerEventType.RightButtonUp:
case RawPointerEventType.MiddleButtonUp:
if (ButtonCount(props) != 0)
- e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
else
- e.Handled = MouseUp(mouse, e.Root, e.Position, props, e.InputModifiers);
+ e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
break;
case RawPointerEventType.Move:
- e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
break;
case RawPointerEventType.Wheel:
- e.Handled = MouseWheel(mouse, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
+ e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
break;
}
}
- private void LeaveWindow(IMouseDevice device, IInputRoot root, InputModifiers inputModifiers)
+ private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
{
Contract.Requires(device != null);
Contract.Requires(root != null);
- ClearPointerOver(this, root, inputModifiers);
+ ClearPointerOver(this, timestamp, root, inputModifiers);
}
@@ -206,7 +206,7 @@ namespace Avalonia.Input
_lastClickRect = new Rect(p, new Size())
.Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2));
_lastMouseDownButton = properties.GetObsoleteMouseButton();
- var e = new PointerPressedEventArgs(source, _pointer, root, p, properties, inputModifiers, _clickCount);
+ var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount);
source.RaiseEvent(e);
return e.Handled;
}
@@ -215,7 +215,7 @@ namespace Avalonia.Input
return false;
}
- private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties properties,
+ private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
InputModifiers inputModifiers)
{
Contract.Requires(device != null);
@@ -225,22 +225,22 @@ namespace Avalonia.Input
if (_pointer.Captured == null)
{
- source = SetPointerOver(this, root, p, inputModifiers);
+ source = SetPointerOver(this, timestamp, root, p, inputModifiers);
}
else
{
- SetPointerOver(this, root, _pointer.Captured, inputModifiers);
+ SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers);
source = _pointer.Captured;
}
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
- p, properties, inputModifiers);
+ p, timestamp, properties, inputModifiers);
source?.RaiseEvent(e);
return e.Handled;
}
- private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties props,
+ private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
InputModifiers inputModifiers)
{
Contract.Requires(device != null);
@@ -251,7 +251,8 @@ namespace Avalonia.Input
if (hit != null)
{
var source = GetSource(hit);
- var e = new PointerReleasedEventArgs(source, _pointer, root, p, props, inputModifiers, _lastMouseDownButton);
+ var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers,
+ _lastMouseDownButton);
source?.RaiseEvent(e);
_pointer.Capture(null);
@@ -261,7 +262,7 @@ namespace Avalonia.Input
return false;
}
- private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p,
+ private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props,
Vector delta, InputModifiers inputModifiers)
{
@@ -273,7 +274,7 @@ namespace Avalonia.Input
if (hit != null)
{
var source = GetSource(hit);
- var e = new PointerWheelEventArgs(source, _pointer, root, p, props, inputModifiers, delta);
+ var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta);
source?.RaiseEvent(e);
return e.Handled;
@@ -298,19 +299,19 @@ namespace Avalonia.Input
return _pointer.Captured ?? root.InputHitTest(p);
}
- PointerEventArgs CreateSimpleEvent(RoutedEvent ev, IInteractive source, InputModifiers inputModifiers)
+ PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, InputModifiers inputModifiers)
{
return new PointerEventArgs(ev, source, _pointer, null, default,
- new PointerPointProperties(inputModifiers), inputModifiers);
+ timestamp, new PointerPointProperties(inputModifiers), inputModifiers);
}
- private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers)
+ private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
{
Contract.Requires(device != null);
Contract.Requires(root != null);
var element = root.PointerOverElement;
- var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, element, inputModifiers);
+ var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, inputModifiers);
if (element!=null && !element.IsAttachedToVisualTree)
{
@@ -347,7 +348,7 @@ namespace Avalonia.Input
}
}
- private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p, InputModifiers inputModifiers)
+ private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, InputModifiers inputModifiers)
{
Contract.Requires(device != null);
Contract.Requires(root != null);
@@ -358,18 +359,18 @@ namespace Avalonia.Input
{
if (element != null)
{
- SetPointerOver(device, root, element, inputModifiers);
+ SetPointerOver(device, timestamp, root, element, inputModifiers);
}
else
{
- ClearPointerOver(device, root, inputModifiers);
+ ClearPointerOver(device, timestamp, root, inputModifiers);
}
}
return element;
}
- private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element, InputModifiers inputModifiers)
+ private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, InputModifiers inputModifiers)
{
Contract.Requires(device != null);
Contract.Requires(root != null);
@@ -391,7 +392,7 @@ namespace Avalonia.Input
el = root.PointerOverElement;
- var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, el, inputModifiers);
+ var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, inputModifiers);
if (el!=null && branch!=null && !el.IsAttachedToVisualTree)
{
ClearChildrenPointerOver(e,branch,false);
diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs
index 37d9ade839..c827822192 100644
--- a/src/Avalonia.Input/PointerEventArgs.cs
+++ b/src/Avalonia.Input/PointerEventArgs.cs
@@ -17,7 +17,9 @@ namespace Avalonia.Input
public PointerEventArgs(RoutedEvent routedEvent,
IInteractive source,
IPointer pointer,
- IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties,
+ IVisual rootVisual, Point rootVisualPosition,
+ ulong timestamp,
+ PointerPointProperties properties,
InputModifiers modifiers)
: base(routedEvent)
{
@@ -26,6 +28,7 @@ namespace Avalonia.Input
_rootVisualPosition = rootVisualPosition;
_properties = properties;
Pointer = pointer;
+ Timestamp = timestamp;
InputModifiers = modifiers;
}
@@ -50,6 +53,7 @@ namespace Avalonia.Input
}
public IPointer Pointer { get; }
+ public ulong Timestamp { get; }
private IPointerDevice _device;
@@ -86,11 +90,13 @@ namespace Avalonia.Input
public PointerPressedEventArgs(
IInteractive source,
IPointer pointer,
- IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties,
+ IVisual rootVisual, Point rootVisualPosition,
+ ulong timestamp,
+ PointerPointProperties properties,
InputModifiers modifiers,
int obsoleteClickCount = 1)
- : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, properties,
- modifiers)
+ : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
+ timestamp, properties, modifiers)
{
_obsoleteClickCount = obsoleteClickCount;
}
@@ -105,10 +111,10 @@ namespace Avalonia.Input
{
public PointerReleasedEventArgs(
IInteractive source, IPointer pointer,
- IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, InputModifiers modifiers,
- MouseButton obsoleteMouseButton)
+ IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
+ PointerPointProperties properties, InputModifiers modifiers, MouseButton obsoleteMouseButton)
: base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition,
- properties, modifiers)
+ timestamp, properties, modifiers)
{
MouseButton = obsoleteMouseButton;
}
diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Input/PointerWheelEventArgs.cs
index b409cc81bd..de1badfe96 100644
--- a/src/Avalonia.Input/PointerWheelEventArgs.cs
+++ b/src/Avalonia.Input/PointerWheelEventArgs.cs
@@ -11,9 +11,10 @@ namespace Avalonia.Input
public Vector Delta { get; set; }
public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
- Point rootVisualPosition,
+ Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, InputModifiers modifiers, Vector delta)
- : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, properties, modifiers)
+ : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,
+ timestamp, properties, modifiers)
{
Delta = delta;
}
diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs
index 8db2b125a6..7f473bb320 100644
--- a/src/Avalonia.Input/TouchDevice.cs
+++ b/src/Avalonia.Input/TouchDevice.cs
@@ -44,7 +44,7 @@ namespace Avalonia.Input
if (args.Type == RawPointerEventType.TouchBegin)
{
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
- args.Root, args.Position,
+ args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)),
GetModifiers(args.InputModifiers, false)));
}
@@ -55,7 +55,7 @@ namespace Avalonia.Input
using (pointer)
{
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
- args.Root, args.Position,
+ args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, false)),
GetModifiers(args.InputModifiers, pointer.IsPrimary),
pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
@@ -66,7 +66,7 @@ namespace Avalonia.Input
{
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
- args.Position, new PointerPointProperties(modifiers), modifiers));
+ args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers));
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
index d6542d23f0..153473a8a0 100644
--- a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
+++ b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
@@ -1,3 +1,4 @@
+using System.Reactive;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@@ -22,6 +23,8 @@ namespace Avalonia.Controls.UnitTests
}
TestPointer _pointer = new TestPointer();
+ private ulong _nextStamp = 1;
+ private ulong Timestamp() => _nextStamp++;
private InputModifiers _pressedButtons;
public IInputElement Captured => _pointer.Captured;
@@ -61,7 +64,7 @@ namespace Avalonia.Controls.UnitTests
else
{
_pressedButton = mouseButton;
- target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, props,
+ target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
GetModifiers(modifiers), clickCount));
}
}
@@ -70,7 +73,7 @@ namespace Avalonia.Controls.UnitTests
public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default)
{
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position,
- new PointerPointProperties(_pressedButtons), GetModifiers(modifiers)));
+ Timestamp(), new PointerPointProperties(_pressedButtons), GetModifiers(modifiers)));
}
public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
@@ -84,7 +87,8 @@ namespace Avalonia.Controls.UnitTests
_pressedButtons = (_pressedButtons | conv) ^ conv;
var props = new PointerPointProperties(_pressedButtons);
if (ButtonCount(props) == 0)
- target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, props,
+ target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position,
+ Timestamp(), props,
GetModifiers(modifiers), _pressedButton));
else
Move(target, source, position);
@@ -103,13 +107,13 @@ namespace Avalonia.Controls.UnitTests
public void Enter(IInteractive target)
{
target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default,
- new PointerPointProperties(_pressedButtons), _pressedButtons));
+ Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons));
}
public void Leave(IInteractive target)
{
target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default,
- new PointerPointProperties(_pressedButtons), _pressedButtons));
+ Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons));
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
index fb3a5bfefb..ba4d6ca9c5 100644
--- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
@@ -11,14 +11,14 @@ namespace Avalonia.Controls.UnitTests.Platform
public class DefaultMenuInteractionHandlerTests
{
static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source)
- => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default);
+ => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, new PointerPointProperties(), default);
static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source,
- new FakePointer(), (IVisual)source, default, new PointerPointProperties {IsLeftButtonPressed = true},
+ new FakePointer(), (IVisual)source, default,0, new PointerPointProperties {IsLeftButtonPressed = true},
default);
static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source,
- new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default, MouseButton.Left);
+ new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(), default, MouseButton.Left);
public class TopLevel
{