Browse Source

Merge pull request #2595 from AvaloniaUI/pointer-captures

Pointer captures + touch scroll
pull/2629/head
Nikita Tsukanov 7 years ago
committed by GitHub
parent
commit
9ebeeb0efb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      src/Avalonia.Controls/Button.cs
  2. 4
      src/Avalonia.Controls/MenuItem.cs
  3. 69
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  4. 18
      src/Avalonia.Controls/TabControl.cs
  5. 127
      src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
  6. 23
      src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs
  7. 183
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  8. 8
      src/Avalonia.Input/Gestures.cs
  9. 39
      src/Avalonia.Input/InputElement.cs
  10. 114
      src/Avalonia.Input/MouseDevice.cs
  11. 44
      src/Avalonia.Input/Pointer.cs
  12. 31
      src/Avalonia.Input/PointerEventArgs.cs
  13. 5
      src/Avalonia.Input/PointerWheelEventArgs.cs
  14. 1
      src/Avalonia.Input/Properties/AssemblyInfo.cs
  15. 29
      src/Avalonia.Input/ScrollGestureEventArgs.cs
  16. 20
      src/Avalonia.Input/TouchDevice.cs
  17. 11
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  18. 4
      src/Windows/Avalonia.Win32/WindowImpl.cs
  19. 41
      tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
  20. 6
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
  21. 4
      tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

7
src/Avalonia.Controls/Button.cs

@ -252,7 +252,6 @@ namespace Avalonia.Controls
if (e.MouseButton == MouseButton.Left) if (e.MouseButton == MouseButton.Left)
{ {
e.Device.Capture(this);
IsPressed = true; IsPressed = true;
e.Handled = true; e.Handled = true;
@ -270,7 +269,6 @@ namespace Avalonia.Controls
if (IsPressed && e.MouseButton == MouseButton.Left) if (IsPressed && e.MouseButton == MouseButton.Left)
{ {
e.Device.Capture(null);
IsPressed = false; IsPressed = false;
e.Handled = true; 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) protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
{ {
base.UpdateDataValidation(property, status); base.UpdateDataValidation(property, status);

4
src/Avalonia.Controls/MenuItem.cs

@ -339,7 +339,7 @@ namespace Avalonia.Controls
var point = e.GetPointerPoint(null); var point = e.GetPointerPoint(null);
RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position, RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
point.Properties, e.InputModifiers)); e.Timestamp, point.Properties, e.InputModifiers));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -349,7 +349,7 @@ namespace Avalonia.Controls
var point = e.GetPointerPoint(null); var point = e.GetPointerPoint(null);
RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position, RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
point.Properties, e.InputModifiers)); e.Timestamp, point.Properties, e.InputModifiers));
} }
/// <summary> /// <summary>

69
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
@ -64,6 +65,7 @@ namespace Avalonia.Controls.Presenters
private Vector _offset; private Vector _offset;
private IDisposable _logicalScrollSubscription; private IDisposable _logicalScrollSubscription;
private Size _viewport; private Size _viewport;
private Dictionary<int, Vector> _activeLogicalGestureScrolls;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class. /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -81,6 +83,7 @@ namespace Avalonia.Controls.Presenters
public ScrollContentPresenter() public ScrollContentPresenter()
{ {
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
} }
@ -227,6 +230,72 @@ namespace Avalonia.Controls.Presenters
return finalSize; 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<int, Vector>();
_activeLogicalGestureScrolls[e.Id] = delta;
}
Offset = new Vector(x, y);
e.Handled = true;
}
}
private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e)
=> _activeLogicalGestureScrolls?.Remove(e.Id);
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e) protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{ {

18
src/Avalonia.Controls/TabControl.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // 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.Generators;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
@ -8,6 +9,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -166,10 +168,24 @@ namespace Avalonia.Controls
{ {
base.OnPointerPressed(e); base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left) if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse)
{ {
e.Handled = UpdateSelectionFromEventSource(e.Source); 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);
}
}
}
} }
} }

127
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<IGestureRecognizer>, IGestureRecognizerActionsDispatcher
{
private readonly IInputElement _inputElement;
private List<IGestureRecognizer> _recognizers;
private Dictionary<IPointer, IGestureRecognizer> _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<IGestureRecognizer>();
_pointerGrabs = new Dictionary<IPointer, IGestureRecognizer>();
}
_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<IGestureRecognizer> s_Empty = new List<IGestureRecognizer>();
public IEnumerator<IGestureRecognizer> 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;
}
}
}

23
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
}
}

183
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;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll,
(o, v) => o.CanHorizontallyScroll = v);
/// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll,
(o, v) => o.CanVerticallyScroll = v);
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanHorizontallyScroll
{
get => _canHorizontallyScroll;
set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
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);
}
}
}
}
}

8
src/Avalonia.Input/Gestures.cs

@ -18,6 +18,14 @@ namespace Avalonia.Input
RoutingStrategies.Bubble, RoutingStrategies.Bubble,
typeof(Gestures)); typeof(Gestures));
public static readonly RoutedEvent<ScrollGestureEventArgs> ScrollGestureEvent =
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<ScrollGestureEventArgs> ScrollGestureEndedEvent =
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
private static WeakReference s_lastPress; private static WeakReference s_lastPress;
static Gestures() static Gestures()

39
src/Avalonia.Input/InputElement.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -127,6 +128,14 @@ namespace Avalonia.Input
RoutedEvent.Register<InputElement, PointerReleasedEventArgs>( RoutedEvent.Register<InputElement, PointerReleasedEventArgs>(
"PointerReleased", "PointerReleased",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble); RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerCaptureLost"/> routed event.
/// </summary>
public static readonly RoutedEvent<PointerCaptureLostEventArgs> PointerCaptureLostEvent =
RoutedEvent.Register<InputElement, PointerCaptureLostEventArgs>(
"PointerCaptureLost",
RoutingStrategies.Direct);
/// <summary> /// <summary>
/// Defines the <see cref="PointerWheelChanged"/> event. /// Defines the <see cref="PointerWheelChanged"/> event.
@ -148,6 +157,7 @@ namespace Avalonia.Input
private bool _isFocused; private bool _isFocused;
private bool _isPointerOver; private bool _isPointerOver;
private GestureRecognizerCollection _gestureRecognizers;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="InputElement"/> class. /// Initializes static members of the <see cref="InputElement"/> class.
@ -166,6 +176,7 @@ namespace Avalonia.Input
PointerMovedEvent.AddClassHandler<InputElement>(x => x.OnPointerMoved); PointerMovedEvent.AddClassHandler<InputElement>(x => x.OnPointerMoved);
PointerPressedEvent.AddClassHandler<InputElement>(x => x.OnPointerPressed); PointerPressedEvent.AddClassHandler<InputElement>(x => x.OnPointerPressed);
PointerReleasedEvent.AddClassHandler<InputElement>(x => x.OnPointerReleased); PointerReleasedEvent.AddClassHandler<InputElement>(x => x.OnPointerReleased);
PointerCaptureLostEvent.AddClassHandler<InputElement>(x => x.OnPointerCaptureLost);
PointerWheelChangedEvent.AddClassHandler<InputElement>(x => x.OnPointerWheelChanged); PointerWheelChangedEvent.AddClassHandler<InputElement>(x => x.OnPointerWheelChanged);
PseudoClass<InputElement, bool>(IsEnabledCoreProperty, x => !x, ":disabled"); PseudoClass<InputElement, bool>(IsEnabledCoreProperty, x => !x, ":disabled");
@ -263,6 +274,16 @@ namespace Avalonia.Input
remove { RemoveHandler(PointerReleasedEvent, value); } remove { RemoveHandler(PointerReleasedEvent, value); }
} }
/// <summary>
/// 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
/// </summary>
public event EventHandler<PointerCaptureLostEventArgs> PointerCaptureLost
{
add => AddHandler(PointerCaptureLostEvent, value);
remove => RemoveHandler(PointerCaptureLostEvent, value);
}
/// <summary> /// <summary>
/// Occurs when the mouse wheen is scrolled over the control. /// Occurs when the mouse wheen is scrolled over the control.
/// </summary> /// </summary>
@ -370,6 +391,9 @@ namespace Avalonia.Input
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>(); public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
public GestureRecognizerCollection GestureRecognizers
=> _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this));
/// <summary> /// <summary>
/// Focuses the control. /// Focuses the control.
/// </summary> /// </summary>
@ -460,6 +484,8 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected virtual void OnPointerMoved(PointerEventArgs e) protected virtual void OnPointerMoved(PointerEventArgs e)
{ {
if (_gestureRecognizers?.HandlePointerMoved(e) == true)
e.Handled = true;
} }
/// <summary> /// <summary>
@ -468,6 +494,8 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected virtual void OnPointerPressed(PointerPressedEventArgs e) protected virtual void OnPointerPressed(PointerPressedEventArgs e)
{ {
if (_gestureRecognizers?.HandlePointerPressed(e) == true)
e.Handled = true;
} }
/// <summary> /// <summary>
@ -476,6 +504,17 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected virtual void OnPointerReleased(PointerReleasedEventArgs e) protected virtual void OnPointerReleased(PointerReleasedEventArgs e)
{ {
if (_gestureRecognizers?.HandlePointerReleased(e) == true)
e.Handled = true;
}
/// <summary>
/// Called before the <see cref="PointerCaptureLost"/> event occurs.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
_gestureRecognizers?.HandlePointerCaptureLost(e);
} }
/// <summary> /// <summary>

114
src/Avalonia.Input/MouseDevice.cs

@ -14,18 +14,14 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Represents a mouse device. /// Represents a mouse device.
/// </summary> /// </summary>
public class MouseDevice : IMouseDevice, IPointer public class MouseDevice : IMouseDevice
{ {
private int _clickCount; private int _clickCount;
private Rect _lastClickRect; private Rect _lastClickRect;
private ulong _lastClickTime; private ulong _lastClickTime;
private IInputElement _captured;
private IDisposable _capturedSubscription;
PointerType IPointer.Type => PointerType.Mouse; private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
bool IPointer.IsPrimary => true;
int IPointer.Id { get; } = Pointer.GetNextFreeId();
/// <summary> /// <summary>
/// Gets the control that is currently capturing by the mouse, if any. /// Gets the control that is currently capturing by the mouse, if any.
/// </summary> /// </summary>
@ -34,27 +30,9 @@ namespace Avalonia.Input
/// within the control's bounds or not. To set the mouse capture, call the /// within the control's bounds or not. To set the mouse capture, call the
/// <see cref="Capture"/> method. /// <see cref="Capture"/> method.
/// </remarks> /// </remarks>
public IInputElement Captured [Obsolete("Use IPointer instead")]
{ public IInputElement Captured => _pointer.Captured;
get => _captured;
protected set
{
_capturedSubscription?.Dispose();
_capturedSubscription = null;
if (value != null)
{
_capturedSubscription = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => value.DetachedFromVisualTree += x,
x => value.DetachedFromVisualTree -= x)
.Take(1)
.Subscribe(_ => Captured = null);
}
_captured = value;
}
}
/// <summary> /// <summary>
/// Gets the mouse position, in screen coordinates. /// Gets the mouse position, in screen coordinates.
/// </summary> /// </summary>
@ -75,8 +53,7 @@ namespace Avalonia.Input
/// </remarks> /// </remarks>
public virtual void Capture(IInputElement control) public virtual void Capture(IInputElement control)
{ {
// TODO: Check visibility and enabled state before setting capture. _pointer.Capture(control);
Captured = control;
} }
/// <summary> /// <summary>
@ -110,13 +87,13 @@ namespace Avalonia.Input
if (rect.Contains(clientPoint)) 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 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) switch (e.Type)
{ {
case RawPointerEventType.LeaveWindow: case RawPointerEventType.LeaveWindow:
LeaveWindow(mouse, e.Root, e.InputModifiers); LeaveWindow(mouse, e.Timestamp, e.Root, e.InputModifiers);
break; break;
case RawPointerEventType.LeftButtonDown: case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown: case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown: case RawPointerEventType.MiddleButtonDown:
if (ButtonCount(props) > 1) 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 else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
props, e.InputModifiers); props, e.InputModifiers);
@ -159,25 +136,25 @@ namespace Avalonia.Input
case RawPointerEventType.RightButtonUp: case RawPointerEventType.RightButtonUp:
case RawPointerEventType.MiddleButtonUp: case RawPointerEventType.MiddleButtonUp:
if (ButtonCount(props) != 0) 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 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; break;
case RawPointerEventType.Move: 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; break;
case RawPointerEventType.Wheel: 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; break;
} }
} }
private void LeaveWindow(IMouseDevice device, IInputRoot root, InputModifiers inputModifiers) private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(root != null);
ClearPointerOver(this, root, inputModifiers); ClearPointerOver(this, timestamp, root, inputModifiers);
} }
@ -195,7 +172,7 @@ namespace Avalonia.Input
rv.IsLeftButtonPressed = false; rv.IsLeftButtonPressed = false;
if (args.Type == RawPointerEventType.MiddleButtonUp) if (args.Type == RawPointerEventType.MiddleButtonUp)
rv.IsMiddleButtonPressed = false; rv.IsMiddleButtonPressed = false;
if (args.Type == RawPointerEventType.RightButtonDown) if (args.Type == RawPointerEventType.RightButtonUp)
rv.IsRightButtonPressed = false; rv.IsRightButtonPressed = false;
return rv; return rv;
} }
@ -212,8 +189,8 @@ namespace Avalonia.Input
if (hit != null) if (hit != null)
{ {
IInteractive source = GetSource(hit); _pointer.Capture(hit);
var source = GetSource(hit);
if (source != null) if (source != null)
{ {
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>(); var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
@ -229,8 +206,7 @@ namespace Avalonia.Input
_lastClickRect = new Rect(p, new Size()) _lastClickRect = new Rect(p, new Size())
.Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2));
_lastMouseDownButton = properties.GetObsoleteMouseButton(); _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); source.RaiseEvent(e);
return e.Handled; return e.Handled;
} }
@ -239,7 +215,7 @@ namespace Avalonia.Input
return false; 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) InputModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); Contract.Requires<ArgumentNullException>(device != null);
@ -247,24 +223,24 @@ namespace Avalonia.Input
IInputElement source; IInputElement source;
if (Captured == null) if (_pointer.Captured == null)
{ {
source = SetPointerOver(this, root, p, inputModifiers); source = SetPointerOver(this, timestamp, root, p, inputModifiers);
} }
else else
{ {
SetPointerOver(this, root, Captured, inputModifiers); SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers);
source = Captured; source = _pointer.Captured;
} }
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, this, root, var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
p, properties, inputModifiers); p, timestamp, properties, inputModifiers);
source?.RaiseEvent(e); source?.RaiseEvent(e);
return e.Handled; 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) InputModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); Contract.Requires<ArgumentNullException>(device != null);
@ -275,16 +251,18 @@ namespace Avalonia.Input
if (hit != null) if (hit != null)
{ {
var source = GetSource(hit); 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); source?.RaiseEvent(e);
_pointer.Capture(null);
return e.Handled; return e.Handled;
} }
return false; return false;
} }
private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, PointerPointProperties props,
Vector delta, InputModifiers inputModifiers) Vector delta, InputModifiers inputModifiers)
{ {
@ -296,7 +274,7 @@ namespace Avalonia.Input
if (hit != null) if (hit != null)
{ {
var source = GetSource(hit); 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); source?.RaiseEvent(e);
return e.Handled; return e.Handled;
@ -309,7 +287,7 @@ namespace Avalonia.Input
{ {
Contract.Requires<ArgumentNullException>(hit != null); Contract.Requires<ArgumentNullException>(hit != null);
return Captured ?? return _pointer.Captured ??
(hit as IInteractive) ?? (hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault(); hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault();
} }
@ -318,22 +296,22 @@ namespace Avalonia.Input
{ {
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(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, 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<ArgumentNullException>(device != null); Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(root != null);
var element = root.PointerOverElement; var element = root.PointerOverElement;
var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, element, inputModifiers); var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, inputModifiers);
if (element!=null && !element.IsAttachedToVisualTree) 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<ArgumentNullException>(device != null); Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(root != null);
@ -381,18 +359,18 @@ namespace Avalonia.Input
{ {
if (element != null) if (element != null)
{ {
SetPointerOver(device, root, element, inputModifiers); SetPointerOver(device, timestamp, root, element, inputModifiers);
} }
else else
{ {
ClearPointerOver(device, root, inputModifiers); ClearPointerOver(device, timestamp, root, inputModifiers);
} }
} }
return element; 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<ArgumentNullException>(device != null); Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(root != null);
@ -414,7 +392,7 @@ namespace Avalonia.Input
el = root.PointerOverElement; 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) if (el!=null && branch!=null && !el.IsAttachedToVisualTree)
{ {
ClearChildrenPointerOver(e,branch,false); ClearChildrenPointerOver(e,branch,false);

44
src/Avalonia.Input/Pointer.cs

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
@ -9,23 +11,40 @@ namespace Avalonia.Input
private static int s_NextFreePointerId = 1000; private static int s_NextFreePointerId = 1000;
public static int GetNextFreeId() => s_NextFreePointerId++; 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; Id = id;
Type = type; Type = type;
IsPrimary = isPrimary; IsPrimary = isPrimary;
ImplicitlyCaptured = implicitlyCaptured;
if (ImplicitlyCaptured != null)
ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached;
} }
public int Id { get; } public int Id { get; }
IInputElement FindCommonParent(IInputElement control1, IInputElement control2)
{
if (control1 == null || control2 == null)
return null;
var seen = new HashSet<IInputElement>(control1.GetSelfAndVisualAncestors().OfType<IInputElement>());
return control2.GetSelfAndVisualAncestors().OfType<IInputElement>().FirstOrDefault(seen.Contains);
}
public void Capture(IInputElement control) public void Capture(IInputElement control)
{ {
if (Captured != null) if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached; Captured.DetachedFromVisualTree -= OnCaptureDetached;
var oldCapture = control;
Captured = control; Captured = control;
if (oldCapture != null)
{
var commonParent = FindCommonParent(control, oldCapture);
foreach (var notifyTarget in oldCapture.GetSelfAndVisualAncestors().OfType<IInputElement>())
{
if (notifyTarget == commonParent)
break;
notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this));
}
}
if (Captured != null) if (Captured != null)
Captured.DetachedFromVisualTree += OnCaptureDetached; Captured.DetachedFromVisualTree += OnCaptureDetached;
} }
@ -38,26 +57,11 @@ namespace Avalonia.Input
Capture(GetNextCapture(e.Parent)); 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 Captured { get; private set; }
public IInputElement ImplicitlyCaptured { get; private set; }
public IInputElement GetEffectiveCapture() => Captured ?? ImplicitlyCaptured;
public PointerType Type { get; } public PointerType Type { get; }
public bool IsPrimary { get; } public bool IsPrimary { get; }
public void Dispose() public void Dispose() => Capture(null);
{
if (ImplicitlyCaptured != null)
ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached;
if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached;
}
} }
} }

31
src/Avalonia.Input/PointerEventArgs.cs

@ -17,7 +17,9 @@ namespace Avalonia.Input
public PointerEventArgs(RoutedEvent routedEvent, public PointerEventArgs(RoutedEvent routedEvent,
IInteractive source, IInteractive source,
IPointer pointer, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, IVisual rootVisual, Point rootVisualPosition,
ulong timestamp,
PointerPointProperties properties,
InputModifiers modifiers) InputModifiers modifiers)
: base(routedEvent) : base(routedEvent)
{ {
@ -26,6 +28,7 @@ namespace Avalonia.Input
_rootVisualPosition = rootVisualPosition; _rootVisualPosition = rootVisualPosition;
_properties = properties; _properties = properties;
Pointer = pointer; Pointer = pointer;
Timestamp = timestamp;
InputModifiers = modifiers; InputModifiers = modifiers;
} }
@ -50,6 +53,7 @@ namespace Avalonia.Input
} }
public IPointer Pointer { get; } public IPointer Pointer { get; }
public ulong Timestamp { get; }
private IPointerDevice _device; private IPointerDevice _device;
@ -86,11 +90,13 @@ namespace Avalonia.Input
public PointerPressedEventArgs( public PointerPressedEventArgs(
IInteractive source, IInteractive source,
IPointer pointer, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, IVisual rootVisual, Point rootVisualPosition,
ulong timestamp,
PointerPointProperties properties,
InputModifiers modifiers, InputModifiers modifiers,
int obsoleteClickCount = 1) int obsoleteClickCount = 1)
: base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, properties, : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
modifiers) timestamp, properties, modifiers)
{ {
_obsoleteClickCount = obsoleteClickCount; _obsoleteClickCount = obsoleteClickCount;
} }
@ -105,10 +111,10 @@ namespace Avalonia.Input
{ {
public PointerReleasedEventArgs( public PointerReleasedEventArgs(
IInteractive source, IPointer pointer, IInteractive source, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, InputModifiers modifiers, IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
MouseButton obsoleteMouseButton) PointerPointProperties properties, InputModifiers modifiers, MouseButton obsoleteMouseButton)
: base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition,
properties, modifiers) timestamp, properties, modifiers)
{ {
MouseButton = obsoleteMouseButton; MouseButton = obsoleteMouseButton;
} }
@ -116,4 +122,15 @@ namespace Avalonia.Input
[Obsolete()] [Obsolete()]
public MouseButton MouseButton { get; private set; } 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;
}
}
} }

5
src/Avalonia.Input/PointerWheelEventArgs.cs

@ -11,9 +11,10 @@ namespace Avalonia.Input
public Vector Delta { get; set; } public Vector Delta { get; set; }
public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
Point rootVisualPosition, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, InputModifiers modifiers, Vector delta) 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; Delta = delta;
} }

1
src/Avalonia.Input/Properties/AssemblyInfo.cs

@ -5,3 +5,4 @@ using System.Reflection;
using Avalonia.Metadata; using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")]

29
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;
}
}
}

20
src/Avalonia.Input/TouchDevice.cs

@ -35,28 +35,30 @@ namespace Avalonia.Input
var hit = args.Root.InputHitTest(args.Position); var hit = args.Root.InputHitTest(args.Position);
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), _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) if (args.Type == RawPointerEventType.TouchBegin)
{ {
var modifiers = GetModifiers(args.InputModifiers, false);
target.RaiseEvent(new PointerPressedEventArgs(target, pointer, target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, new PointerPointProperties(modifiers), args.Root, args.Position, ev.Timestamp,
modifiers)); new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)),
GetModifiers(args.InputModifiers, false)));
} }
if (args.Type == RawPointerEventType.TouchEnd) if (args.Type == RawPointerEventType.TouchEnd)
{ {
_pointers.Remove(args.TouchPointId); _pointers.Remove(args.TouchPointId);
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
using (pointer) using (pointer)
{ {
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, new PointerPointProperties(modifiers), args.Root, args.Position, ev.Timestamp,
modifiers, pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); 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); var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, new PointerPointProperties(modifiers), modifiers)); args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers));
} }
} }

11
src/Avalonia.Themes.Default/ScrollViewer.xaml

@ -12,7 +12,14 @@
Extent="{TemplateBinding Extent, Mode=TwoWay}" Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}" Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/> Viewport="{TemplateBinding Viewport, Mode=TwoWay}">
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
/>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="horizontalScrollBar" <ScrollBar Name="horizontalScrollBar"
Orientation="Horizontal" Orientation="Horizontal"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}" Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
@ -32,4 +39,4 @@
</Grid> </Grid>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

4
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -637,8 +637,6 @@ namespace Avalonia.Win32
{ {
foreach (var touchInput in touchInputs) 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, Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner, _owner,
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ? touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ?
@ -646,7 +644,7 @@ namespace Avalonia.Win32
touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ? touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin : RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate, RawPointerEventType.TouchUpdate,
new Point(pt.X, pt.Y), PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
WindowsKeyboardDevice.Instance.Modifiers, WindowsKeyboardDevice.Instance.Modifiers,
touchInput.Id)); touchInput.Id));
} }

41
tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs

@ -1,3 +1,4 @@
using System.Reactive;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -6,22 +7,9 @@ namespace Avalonia.Controls.UnitTests
{ {
public class MouseTestHelper public class MouseTestHelper
{ {
private Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
class TestPointer : IPointer private ulong _nextStamp = 1;
{ private ulong Timestamp() => _nextStamp++;
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 InputModifiers _pressedButtons; private InputModifiers _pressedButtons;
public IInputElement Captured => _pointer.Captured; 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, public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
InputModifiers modifiers = default, int clickCount = 1) 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, public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left,
Point position = default, InputModifiers modifiers = default, int clickCount = 1) Point position = default, InputModifiers modifiers = default, int clickCount = 1)
{ {
@ -61,7 +51,8 @@ namespace Avalonia.Controls.UnitTests
else else
{ {
_pressedButton = mouseButton; _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)); GetModifiers(modifiers), clickCount));
} }
} }
@ -70,7 +61,7 @@ namespace Avalonia.Controls.UnitTests
public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default) public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default)
{ {
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position, 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, public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
@ -84,8 +75,12 @@ namespace Avalonia.Controls.UnitTests
_pressedButtons = (_pressedButtons | conv) ^ conv; _pressedButtons = (_pressedButtons | conv) ^ conv;
var props = new PointerPointProperties(_pressedButtons); var props = new PointerPointProperties(_pressedButtons);
if (ButtonCount(props) == 0) 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)); GetModifiers(modifiers), _pressedButton));
}
else else
Move(target, source, position); Move(target, source, position);
} }
@ -103,13 +98,13 @@ namespace Avalonia.Controls.UnitTests
public void Enter(IInteractive target) public void Enter(IInteractive target)
{ {
target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default, 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) public void Leave(IInteractive target)
{ {
target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default, target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default,
new PointerPointProperties(_pressedButtons), _pressedButtons)); Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons));
} }
} }

6
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@ -11,14 +11,14 @@ namespace Avalonia.Controls.UnitTests.Platform
public class DefaultMenuInteractionHandlerTests public class DefaultMenuInteractionHandlerTests
{ {
static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) 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, 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); default);
static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, 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 public class TopLevel
{ {

4
tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

@ -15,7 +15,7 @@ namespace Avalonia.Input.UnitTests
public class MouseDeviceTests public class MouseDeviceTests
{ {
[Fact] [Fact]
public void Capture_Is_Cleared_When_Control_Removed() public void Capture_Is_Transferred_To_Parent_When_Control_Removed()
{ {
Canvas control; Canvas control;
var root = new TestRoot var root = new TestRoot
@ -29,7 +29,7 @@ namespace Avalonia.Input.UnitTests
root.Child = null; root.Child = null;
Assert.Null(target.Captured); Assert.Same(root, target.Captured);
} }
[Fact] [Fact]

Loading…
Cancel
Save