diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs
index cc9e6b7444..70f26288af 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -252,7 +252,6 @@ namespace Avalonia.Controls
if (e.MouseButton == MouseButton.Left)
{
- e.Device.Capture(this);
IsPressed = true;
e.Handled = true;
@@ -270,7 +269,6 @@ namespace Avalonia.Controls
if (IsPressed && e.MouseButton == MouseButton.Left)
{
- e.Device.Capture(null);
IsPressed = false;
e.Handled = true;
@@ -282,6 +280,11 @@ namespace Avalonia.Controls
}
}
+ protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ IsPressed = false;
+ }
+
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
{
base.UpdateDataValidation(property, status);
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.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 30330ef9ac..e7d8018a42 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@@ -64,6 +65,7 @@ namespace Avalonia.Controls.Presenters
private Vector _offset;
private IDisposable _logicalScrollSubscription;
private Size _viewport;
+ private Dictionary _activeLogicalGestureScrolls;
///
/// Initializes static members of the class.
@@ -81,6 +83,7 @@ namespace Avalonia.Controls.Presenters
public ScrollContentPresenter()
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
+ AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
@@ -227,6 +230,72 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
+ // Arbitrary chosen value, probably need to ask ILogicalScrollable
+ private const int LogicalScrollItemSize = 50;
+ private void OnScrollGesture(object sender, ScrollGestureEventArgs e)
+ {
+ if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
+ {
+ var scrollable = Child as ILogicalScrollable;
+ bool isLogical = scrollable?.IsLogicalScrollEnabled == true;
+
+ double x = Offset.X;
+ double y = Offset.Y;
+
+ Vector delta = default;
+ if (isLogical)
+ _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta);
+ delta += e.Delta;
+
+ if (Extent.Height > Viewport.Height)
+ {
+ double dy;
+ if (isLogical)
+ {
+ var logicalUnits = delta.Y / LogicalScrollItemSize;
+ delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize);
+ dy = logicalUnits * scrollable.ScrollSize.Height;
+ }
+ else
+ dy = delta.Y;
+
+
+ y += dy;
+ y = Math.Max(y, 0);
+ y = Math.Min(y, Extent.Height - Viewport.Height);
+ }
+
+ if (Extent.Width > Viewport.Width)
+ {
+ double dx;
+ if (isLogical)
+ {
+ var logicalUnits = delta.X / LogicalScrollItemSize;
+ delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize);
+ dx = logicalUnits * scrollable.ScrollSize.Width;
+ }
+ else
+ dx = delta.X;
+ x += dx;
+ x = Math.Max(x, 0);
+ x = Math.Min(x, Extent.Width - Viewport.Width);
+ }
+
+ if (isLogical)
+ {
+ if (_activeLogicalGestureScrolls == null)
+ _activeLogicalGestureScrolls = new Dictionary();
+ _activeLogicalGestureScrolls[e.Id] = delta;
+ }
+
+ Offset = new Vector(x, y);
+ e.Handled = true;
+ }
+ }
+
+ private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e)
+ => _activeLogicalGestureScrolls?.Remove(e.Id);
+
///
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs
index d6537ebbca..fc2c118132 100644
--- a/src/Avalonia.Controls/TabControl.cs
+++ b/src/Avalonia.Controls/TabControl.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
@@ -8,6 +9,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
+using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -166,10 +168,24 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
- if (e.MouseButton == MouseButton.Left)
+ if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ if (e.MouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse)
+ {
+ var container = GetContainerFromEventSource(e.Source);
+ if (container != null
+ && container.GetVisualsAt(e.GetPosition(container))
+ .Any(c => container == c || container.IsVisualAncestorOf(c)))
+ {
+ e.Handled = UpdateSelectionFromEventSource(e.Source);
+ }
+ }
+ }
}
}
diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
new file mode 100644
index 0000000000..91b224e65a
--- /dev/null
+++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.LogicalTree;
+using Avalonia.Styling;
+
+namespace Avalonia.Input.GestureRecognizers
+{
+ public class GestureRecognizerCollection : IReadOnlyCollection, IGestureRecognizerActionsDispatcher
+ {
+ private readonly IInputElement _inputElement;
+ private List _recognizers;
+ private Dictionary _pointerGrabs;
+
+
+ public GestureRecognizerCollection(IInputElement inputElement)
+ {
+ _inputElement = inputElement;
+ }
+
+ public void Add(IGestureRecognizer recognizer)
+ {
+ if (_recognizers == null)
+ {
+ // We initialize the collection when the first recognizer is added
+ _recognizers = new List();
+ _pointerGrabs = new Dictionary();
+ }
+
+ _recognizers.Add(recognizer);
+ recognizer.Initialize(_inputElement, this);
+
+ // Hacks to make bindings work
+
+ if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical)
+ {
+ logical.SetParent(logicalParent);
+ if (recognizer is IStyleable styleableRecognizer
+ && _inputElement is IStyleable styleableParent)
+ styleableRecognizer.Bind(StyledElement.TemplatedParentProperty,
+ styleableParent.GetObservable(StyledElement.TemplatedParentProperty));
+ }
+ }
+
+ static readonly List s_Empty = new List();
+
+ public IEnumerator GetEnumerator()
+ => _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ public int Count => _recognizers?.Count ?? 0;
+
+
+ internal bool HandlePointerPressed(PointerPressedEventArgs e)
+ {
+ if (_recognizers == null)
+ return false;
+ foreach (var r in _recognizers)
+ {
+ if(e.Handled)
+ break;
+ r.PointerPressed(e);
+ }
+
+ return e.Handled;
+ }
+
+ internal bool HandlePointerReleased(PointerReleasedEventArgs e)
+ {
+ if (_recognizers == null)
+ return false;
+ if (_pointerGrabs.TryGetValue(e.Pointer, out var capture))
+ {
+ capture.PointerReleased(e);
+ }
+ else
+ foreach (var r in _recognizers)
+ {
+ if (e.Handled)
+ break;
+ r.PointerReleased(e);
+ }
+ return e.Handled;
+ }
+
+ internal bool HandlePointerMoved(PointerEventArgs e)
+ {
+ if (_recognizers == null)
+ return false;
+ if (_pointerGrabs.TryGetValue(e.Pointer, out var capture))
+ {
+ capture.PointerMoved(e);
+ }
+ else
+ foreach (var r in _recognizers)
+ {
+ if (e.Handled)
+ break;
+ r.PointerMoved(e);
+ }
+ return e.Handled;
+ }
+
+ internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ if (_recognizers == null)
+ return;
+ _pointerGrabs.Remove(e.Pointer);
+ foreach (var r in _recognizers)
+ {
+ if(e.Handled)
+ break;
+ r.PointerCaptureLost(e);
+ }
+ }
+
+ void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer)
+ {
+ pointer.Capture(_inputElement);
+ _pointerGrabs[pointer] = recognizer;
+ }
+
+ }
+}
diff --git a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs
new file mode 100644
index 0000000000..b8ba9e529c
--- /dev/null
+++ b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs
@@ -0,0 +1,23 @@
+namespace Avalonia.Input.GestureRecognizers
+{
+ public interface IGestureRecognizer
+ {
+ void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions);
+ void PointerPressed(PointerPressedEventArgs e);
+ void PointerReleased(PointerReleasedEventArgs e);
+ void PointerMoved(PointerEventArgs e);
+ void PointerCaptureLost(PointerCaptureLostEventArgs e);
+ }
+
+ public interface IGestureRecognizerActionsDispatcher
+ {
+ void Capture(IPointer pointer, IGestureRecognizer recognizer);
+ }
+
+ public enum GestureRecognizerResult
+ {
+ None,
+ Capture,
+ ReleaseCapture
+ }
+}
diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
new file mode 100644
index 0000000000..4f3c7c0bba
--- /dev/null
+++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Diagnostics;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+
+namespace Avalonia.Input.GestureRecognizers
+{
+ public class ScrollGestureRecognizer
+ : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
+ IGestureRecognizer
+ {
+ private bool _scrolling;
+ private Point _trackedRootPoint;
+ private IPointer _tracking;
+ private IInputElement _target;
+ private IGestureRecognizerActionsDispatcher _actions;
+ private bool _canHorizontallyScroll;
+ private bool _canVerticallyScroll;
+ private int _gestureId;
+
+ // Movement per second
+ private Vector _inertia;
+ private ulong? _lastMoveTimestamp;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty CanHorizontallyScrollProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(CanHorizontallyScroll),
+ o => o.CanHorizontallyScroll,
+ (o, v) => o.CanHorizontallyScroll = v);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty CanVerticallyScrollProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(CanVerticallyScroll),
+ o => o.CanVerticallyScroll,
+ (o, v) => o.CanVerticallyScroll = v);
+
+ ///
+ /// Gets or sets a value indicating whether the content can be scrolled horizontally.
+ ///
+ public bool CanHorizontallyScroll
+ {
+ get => _canHorizontallyScroll;
+ set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the content can be scrolled horizontally.
+ ///
+ public bool CanVerticallyScroll
+ {
+ get => _canVerticallyScroll;
+ set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
+ }
+
+
+ public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
+ {
+ _target = target;
+ _actions = actions;
+ }
+
+ public void PointerPressed(PointerPressedEventArgs e)
+ {
+ if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch)
+ {
+ EndGesture();
+ _tracking = e.Pointer;
+ _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)
+ {
+ var rootPoint = e.GetPosition(null);
+ if (!_scrolling)
+ {
+ if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance)
+ _scrolling = true;
+ if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance)
+ _scrolling = true;
+ if (_scrolling)
+ {
+ _actions.Capture(e.Pointer, this);
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+
+ public void PointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ if (e.Pointer == _tracking) EndGesture();
+ }
+
+ void EndGesture()
+ {
+ _tracking = null;
+ if (_scrolling)
+ {
+ _inertia = default;
+ _scrolling = false;
+ _target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
+ _gestureId = 0;
+ _lastMoveTimestamp = null;
+ }
+
+ }
+
+
+ public void PointerReleased(PointerReleasedEventArgs e)
+ {
+ if (e.Pointer == _tracking && _scrolling)
+ {
+ e.Handled = true;
+ 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/Gestures.cs b/src/Avalonia.Input/Gestures.cs
index 23b0ad466e..65195394ab 100644
--- a/src/Avalonia.Input/Gestures.cs
+++ b/src/Avalonia.Input/Gestures.cs
@@ -18,6 +18,14 @@ namespace Avalonia.Input
RoutingStrategies.Bubble,
typeof(Gestures));
+ public static readonly RoutedEvent ScrollGestureEvent =
+ RoutedEvent.Register(
+ "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
+
+ public static readonly RoutedEvent ScrollGestureEndedEvent =
+ RoutedEvent.Register(
+ "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
+
private static WeakReference s_lastPress;
static Gestures()
diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs
index 07e04486ec..7c687f0d7e 100644
--- a/src/Avalonia.Input/InputElement.cs
+++ b/src/Avalonia.Input/InputElement.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@@ -127,6 +128,14 @@ namespace Avalonia.Input
RoutedEvent.Register(
"PointerReleased",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the routed event.
+ ///
+ public static readonly RoutedEvent PointerCaptureLostEvent =
+ RoutedEvent.Register(
+ "PointerCaptureLost",
+ RoutingStrategies.Direct);
///
/// Defines the event.
@@ -148,6 +157,7 @@ namespace Avalonia.Input
private bool _isFocused;
private bool _isPointerOver;
+ private GestureRecognizerCollection _gestureRecognizers;
///
/// Initializes static members of the class.
@@ -166,6 +176,7 @@ namespace Avalonia.Input
PointerMovedEvent.AddClassHandler(x => x.OnPointerMoved);
PointerPressedEvent.AddClassHandler(x => x.OnPointerPressed);
PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased);
+ PointerCaptureLostEvent.AddClassHandler(x => x.OnPointerCaptureLost);
PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged);
PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
@@ -263,6 +274,16 @@ namespace Avalonia.Input
remove { RemoveHandler(PointerReleasedEvent, value); }
}
+ ///
+ /// Occurs when the control or its child control loses the pointer capture for any reason,
+ /// event will not be triggered for a parent control if capture was transferred to another child of that parent control
+ ///
+ public event EventHandler PointerCaptureLost
+ {
+ add => AddHandler(PointerCaptureLostEvent, value);
+ remove => RemoveHandler(PointerCaptureLostEvent, value);
+ }
+
///
/// Occurs when the mouse wheen is scrolled over the control.
///
@@ -370,6 +391,9 @@ namespace Avalonia.Input
public List KeyBindings { get; } = new List();
+ public GestureRecognizerCollection GestureRecognizers
+ => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this));
+
///
/// Focuses the control.
///
@@ -460,6 +484,8 @@ namespace Avalonia.Input
/// The event args.
protected virtual void OnPointerMoved(PointerEventArgs e)
{
+ if (_gestureRecognizers?.HandlePointerMoved(e) == true)
+ e.Handled = true;
}
///
@@ -468,6 +494,8 @@ namespace Avalonia.Input
/// The event args.
protected virtual void OnPointerPressed(PointerPressedEventArgs e)
{
+ if (_gestureRecognizers?.HandlePointerPressed(e) == true)
+ e.Handled = true;
}
///
@@ -476,6 +504,17 @@ namespace Avalonia.Input
/// The event args.
protected virtual void OnPointerReleased(PointerReleasedEventArgs e)
{
+ if (_gestureRecognizers?.HandlePointerReleased(e) == true)
+ e.Handled = true;
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// The event args.
+ protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
+ {
+ _gestureRecognizers?.HandlePointerCaptureLost(e);
}
///
diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs
index 90d9c37bd4..4c4d679087 100644
--- a/src/Avalonia.Input/MouseDevice.cs
+++ b/src/Avalonia.Input/MouseDevice.cs
@@ -14,18 +14,14 @@ namespace Avalonia.Input
///
/// Represents a mouse device.
///
- public class MouseDevice : IMouseDevice, IPointer
+ public class MouseDevice : IMouseDevice
{
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
- private IInputElement _captured;
- private IDisposable _capturedSubscription;
- PointerType IPointer.Type => PointerType.Mouse;
- bool IPointer.IsPrimary => true;
- int IPointer.Id { get; } = Pointer.GetNextFreeId();
-
+ private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
+
///
/// Gets the control that is currently capturing by the mouse, if any.
///
@@ -34,27 +30,9 @@ namespace Avalonia.Input
/// within the control's bounds or not. To set the mouse capture, call the
/// method.
///
- public IInputElement Captured
- {
- get => _captured;
- protected set
- {
- _capturedSubscription?.Dispose();
- _capturedSubscription = null;
-
- if (value != null)
- {
- _capturedSubscription = Observable.FromEventPattern(
- x => value.DetachedFromVisualTree += x,
- x => value.DetachedFromVisualTree -= x)
- .Take(1)
- .Subscribe(_ => Captured = null);
- }
+ [Obsolete("Use IPointer instead")]
+ public IInputElement Captured => _pointer.Captured;
- _captured = value;
- }
- }
-
///
/// Gets the mouse position, in screen coordinates.
///
@@ -75,8 +53,7 @@ namespace Avalonia.Input
///
public virtual void Capture(IInputElement control)
{
- // TODO: Check visibility and enabled state before setting capture.
- Captured = control;
+ _pointer.Capture(control);
}
///
@@ -110,13 +87,13 @@ namespace Avalonia.Input
if (rect.Contains(clientPoint))
{
- if (Captured == null)
+ if (_pointer.Captured == null)
{
- SetPointerOver(this, root, clientPoint, InputModifiers.None);
+ SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, InputModifiers.None);
}
else
{
- SetPointerOver(this, root, Captured, InputModifiers.None);
+ SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, InputModifiers.None);
}
}
}
@@ -144,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);
@@ -159,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);
}
@@ -195,7 +172,7 @@ namespace Avalonia.Input
rv.IsLeftButtonPressed = false;
if (args.Type == RawPointerEventType.MiddleButtonUp)
rv.IsMiddleButtonPressed = false;
- if (args.Type == RawPointerEventType.RightButtonDown)
+ if (args.Type == RawPointerEventType.RightButtonUp)
rv.IsRightButtonPressed = false;
return rv;
}
@@ -212,8 +189,8 @@ namespace Avalonia.Input
if (hit != null)
{
- IInteractive source = GetSource(hit);
-
+ _pointer.Capture(hit);
+ var source = GetSource(hit);
if (source != null)
{
var settings = AvaloniaLocator.Current.GetService();
@@ -229,8 +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, this, root, p, properties, inputModifiers, _clickCount);
-
+ var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount);
source.RaiseEvent(e);
return e.Handled;
}
@@ -239,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);
@@ -247,24 +223,24 @@ namespace Avalonia.Input
IInputElement source;
- if (Captured == null)
+ if (_pointer.Captured == null)
{
- source = SetPointerOver(this, root, p, inputModifiers);
+ source = SetPointerOver(this, timestamp, root, p, inputModifiers);
}
else
{
- SetPointerOver(this, root, Captured, inputModifiers);
- source = Captured;
+ SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers);
+ source = _pointer.Captured;
}
- var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, this, root,
- p, properties, inputModifiers);
+ var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
+ 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);
@@ -275,16 +251,18 @@ namespace Avalonia.Input
if (hit != null)
{
var source = GetSource(hit);
- var e = new PointerReleasedEventArgs(source, this, root, p, props, inputModifiers, _lastMouseDownButton);
+ var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers,
+ _lastMouseDownButton);
source?.RaiseEvent(e);
+ _pointer.Capture(null);
return e.Handled;
}
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)
{
@@ -296,7 +274,7 @@ namespace Avalonia.Input
if (hit != null)
{
var source = GetSource(hit);
- var e = new PointerWheelEventArgs(source, this, root, p, props, inputModifiers, delta);
+ var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta);
source?.RaiseEvent(e);
return e.Handled;
@@ -309,7 +287,7 @@ namespace Avalonia.Input
{
Contract.Requires(hit != null);
- return Captured ??
+ return _pointer.Captured ??
(hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault();
}
@@ -318,22 +296,22 @@ namespace Avalonia.Input
{
Contract.Requires(root != null);
- return Captured ?? root.InputHitTest(p);
+ 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, this, null, default,
- new PointerPointProperties(inputModifiers), inputModifiers);
+ return new PointerEventArgs(ev, source, _pointer, null, default,
+ 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)
{
@@ -370,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);
@@ -381,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);
@@ -414,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/Pointer.cs b/src/Avalonia.Input/Pointer.cs
index bdf2501b32..890ad57024 100644
--- a/src/Avalonia.Input/Pointer.cs
+++ b/src/Avalonia.Input/Pointer.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.Linq;
+using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
@@ -9,23 +11,40 @@ namespace Avalonia.Input
private static int s_NextFreePointerId = 1000;
public static int GetNextFreeId() => s_NextFreePointerId++;
- public Pointer(int id, PointerType type, bool isPrimary, IInputElement implicitlyCaptured)
+ public Pointer(int id, PointerType type, bool isPrimary)
{
Id = id;
Type = type;
IsPrimary = isPrimary;
- ImplicitlyCaptured = implicitlyCaptured;
- if (ImplicitlyCaptured != null)
- ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached;
}
public int Id { get; }
+ IInputElement FindCommonParent(IInputElement control1, IInputElement control2)
+ {
+ if (control1 == null || control2 == null)
+ return null;
+ var seen = new HashSet(control1.GetSelfAndVisualAncestors().OfType());
+ return control2.GetSelfAndVisualAncestors().OfType().FirstOrDefault(seen.Contains);
+ }
+
public void Capture(IInputElement control)
{
if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached;
+ var oldCapture = control;
Captured = control;
+ if (oldCapture != null)
+ {
+ var commonParent = FindCommonParent(control, oldCapture);
+ foreach (var notifyTarget in oldCapture.GetSelfAndVisualAncestors().OfType())
+ {
+ if (notifyTarget == commonParent)
+ break;
+ notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this));
+ }
+ }
+
if (Captured != null)
Captured.DetachedFromVisualTree += OnCaptureDetached;
}
@@ -38,26 +57,11 @@ namespace Avalonia.Input
Capture(GetNextCapture(e.Parent));
}
- private void OnImplicitCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
- {
- ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached;
- ImplicitlyCaptured = GetNextCapture(e.Parent);
- if (ImplicitlyCaptured != null)
- ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached;
- }
public IInputElement Captured { get; private set; }
- public IInputElement ImplicitlyCaptured { get; private set; }
- public IInputElement GetEffectiveCapture() => Captured ?? ImplicitlyCaptured;
public PointerType Type { get; }
public bool IsPrimary { get; }
- public void Dispose()
- {
- if (ImplicitlyCaptured != null)
- ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached;
- if (Captured != null)
- Captured.DetachedFromVisualTree -= OnCaptureDetached;
- }
+ public void Dispose() => Capture(null);
}
}
diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs
index 1d07190a81..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;
}
@@ -116,4 +122,15 @@ namespace Avalonia.Input
[Obsolete()]
public MouseButton MouseButton { get; private set; }
}
+
+ public class PointerCaptureLostEventArgs : RoutedEventArgs
+ {
+ public IPointer Pointer { get; }
+
+ public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
+ {
+ Pointer = pointer;
+ Source = source;
+ }
+ }
}
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/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs
index 7025965f83..3a8d358931 100644
--- a/src/Avalonia.Input/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Input/Properties/AssemblyInfo.cs
@@ -5,3 +5,4 @@ using System.Reflection;
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")]
diff --git a/src/Avalonia.Input/ScrollGestureEventArgs.cs b/src/Avalonia.Input/ScrollGestureEventArgs.cs
new file mode 100644
index 0000000000..a682e8f0a4
--- /dev/null
+++ b/src/Avalonia.Input/ScrollGestureEventArgs.cs
@@ -0,0 +1,29 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input
+{
+ public class ScrollGestureEventArgs : RoutedEventArgs
+ {
+ public int Id { get; }
+ public Vector Delta { get; }
+ private static int _nextId = 1;
+
+ public static int GetNextFreeId() => _nextId++;
+
+ public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
+ {
+ Id = id;
+ Delta = delta;
+ }
+ }
+
+ public class ScrollGestureEndedEventArgs : RoutedEventArgs
+ {
+ public int Id { get; }
+
+ public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
+ {
+ Id = id;
+ }
+ }
+}
diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs
index e9715bd87c..7f473bb320 100644
--- a/src/Avalonia.Input/TouchDevice.cs
+++ b/src/Avalonia.Input/TouchDevice.cs
@@ -35,28 +35,30 @@ namespace Avalonia.Input
var hit = args.Root.InputHitTest(args.Position);
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
- PointerType.Touch, _pointers.Count == 0, hit);
+ PointerType.Touch, _pointers.Count == 0);
+ pointer.Capture(hit);
}
- var target = pointer.GetEffectiveCapture() ?? args.Root;
+ var target = pointer.Captured ?? args.Root;
if (args.Type == RawPointerEventType.TouchBegin)
{
- var modifiers = GetModifiers(args.InputModifiers, false);
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
- args.Root, args.Position, new PointerPointProperties(modifiers),
- modifiers));
+ args.Root, args.Position, ev.Timestamp,
+ new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)),
+ GetModifiers(args.InputModifiers, false)));
}
if (args.Type == RawPointerEventType.TouchEnd)
{
_pointers.Remove(args.TouchPointId);
- var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
using (pointer)
{
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
- args.Root, args.Position, new PointerPointProperties(modifiers),
- modifiers, pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
+ args.Root, args.Position, ev.Timestamp,
+ new PointerPointProperties(GetModifiers(args.InputModifiers, false)),
+ GetModifiers(args.InputModifiers, pointer.IsPrimary),
+ pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
}
}
@@ -64,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/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml
index 63440921d6..3e130cad67 100644
--- a/src/Avalonia.Themes.Default/ScrollViewer.xaml
+++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml
@@ -12,7 +12,14 @@
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
- Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/>
+ Viewport="{TemplateBinding Viewport, Mode=TwoWay}">
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 475f7da6b7..9e8b2d58a6 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -637,8 +637,6 @@ namespace Avalonia.Win32
{
foreach (var touchInput in touchInputs)
{
- var pt = new POINT {X = touchInput.X / 100, Y = touchInput.Y / 100};
- UnmanagedMethods.ScreenToClient(_hwnd, ref pt);
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner,
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ?
@@ -646,7 +644,7 @@ namespace Avalonia.Win32
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
- new Point(pt.X, pt.Y),
+ PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
WindowsKeyboardDevice.Instance.Modifiers,
touchInput.Id));
}
diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
index d6542d23f0..373bbaed75 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;
@@ -6,22 +7,9 @@ namespace Avalonia.Controls.UnitTests
{
public class MouseTestHelper
{
-
- class TestPointer : IPointer
- {
- public int Id { get; } = Pointer.GetNextFreeId();
-
- public void Capture(IInputElement control)
- {
- Captured = control;
- }
-
- public IInputElement Captured { get; set; }
- public PointerType Type => PointerType.Mouse;
- public bool IsPrimary => true;
- }
-
- TestPointer _pointer = new TestPointer();
+ private Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
+ private ulong _nextStamp = 1;
+ private ulong Timestamp() => _nextStamp++;
private InputModifiers _pressedButtons;
public IInputElement Captured => _pointer.Captured;
@@ -49,8 +37,10 @@ namespace Avalonia.Controls.UnitTests
public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
InputModifiers modifiers = default, int clickCount = 1)
- => Down(target, target, mouseButton, position, modifiers, clickCount);
-
+ {
+ Down(target, target, mouseButton, position, modifiers, clickCount);
+ }
+
public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left,
Point position = default, InputModifiers modifiers = default, int clickCount = 1)
{
@@ -61,7 +51,8 @@ namespace Avalonia.Controls.UnitTests
else
{
_pressedButton = mouseButton;
- target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, props,
+ _pointer.Capture((IInputElement)target);
+ target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
GetModifiers(modifiers), clickCount));
}
}
@@ -70,7 +61,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,8 +75,12 @@ 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,
+ {
+ _pointer.Capture(null);
+ target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position,
+ Timestamp(), props,
GetModifiers(modifiers), _pressedButton));
+ }
else
Move(target, source, position);
}
@@ -103,13 +98,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
{
diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
index 8f1c071695..983f541c2a 100644
--- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
+++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Input.UnitTests
public class MouseDeviceTests
{
[Fact]
- public void Capture_Is_Cleared_When_Control_Removed()
+ public void Capture_Is_Transferred_To_Parent_When_Control_Removed()
{
Canvas control;
var root = new TestRoot
@@ -29,7 +29,7 @@ namespace Avalonia.Input.UnitTests
root.Child = null;
- Assert.Null(target.Captured);
+ Assert.Same(root, target.Captured);
}
[Fact]