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 {