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)
{
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);

4
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));
}
/// <inheritdoc/>
@ -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));
}
/// <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.
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<int, Vector> _activeLogicalGestureScrolls;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> 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<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/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{

18
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);
}
}
}
}
}

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,
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;
static Gestures()

39
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<InputElement, PointerReleasedEventArgs>(
"PointerReleased",
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>
/// Defines the <see cref="PointerWheelChanged"/> event.
@ -148,6 +157,7 @@ namespace Avalonia.Input
private bool _isFocused;
private bool _isPointerOver;
private GestureRecognizerCollection _gestureRecognizers;
/// <summary>
/// Initializes static members of the <see cref="InputElement"/> class.
@ -166,6 +176,7 @@ namespace Avalonia.Input
PointerMovedEvent.AddClassHandler<InputElement>(x => x.OnPointerMoved);
PointerPressedEvent.AddClassHandler<InputElement>(x => x.OnPointerPressed);
PointerReleasedEvent.AddClassHandler<InputElement>(x => x.OnPointerReleased);
PointerCaptureLostEvent.AddClassHandler<InputElement>(x => x.OnPointerCaptureLost);
PointerWheelChangedEvent.AddClassHandler<InputElement>(x => x.OnPointerWheelChanged);
PseudoClass<InputElement, bool>(IsEnabledCoreProperty, x => !x, ":disabled");
@ -263,6 +274,16 @@ namespace Avalonia.Input
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>
/// Occurs when the mouse wheen is scrolled over the control.
/// </summary>
@ -370,6 +391,9 @@ namespace Avalonia.Input
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
public GestureRecognizerCollection GestureRecognizers
=> _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this));
/// <summary>
/// Focuses the control.
/// </summary>
@ -460,6 +484,8 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnPointerMoved(PointerEventArgs e)
{
if (_gestureRecognizers?.HandlePointerMoved(e) == true)
e.Handled = true;
}
/// <summary>
@ -468,6 +494,8 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnPointerPressed(PointerPressedEventArgs e)
{
if (_gestureRecognizers?.HandlePointerPressed(e) == true)
e.Handled = true;
}
/// <summary>
@ -476,6 +504,17 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
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>

114
src/Avalonia.Input/MouseDevice.cs

@ -14,18 +14,14 @@ namespace Avalonia.Input
/// <summary>
/// Represents a mouse device.
/// </summary>
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);
/// <summary>
/// Gets the control that is currently capturing by the mouse, if any.
/// </summary>
@ -34,27 +30,9 @@ namespace Avalonia.Input
/// within the control's bounds or not. To set the mouse capture, call the
/// <see cref="Capture"/> method.
/// </remarks>
public IInputElement 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);
}
[Obsolete("Use IPointer instead")]
public IInputElement Captured => _pointer.Captured;
_captured = value;
}
}
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
@ -75,8 +53,7 @@ namespace Avalonia.Input
/// </remarks>
public virtual void Capture(IInputElement control)
{
// TODO: Check visibility and enabled state before setting capture.
Captured = control;
_pointer.Capture(control);
}
/// <summary>
@ -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<ArgumentNullException>(device != 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;
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<IPlatformSettings>();
@ -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<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(hit != null);
return Captured ??
return _pointer.Captured ??
(hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault();
}
@ -318,22 +296,22 @@ namespace Avalonia.Input
{
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,
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<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(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<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(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<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(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);

44
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<IInputElement>(control1.GetSelfAndVisualAncestors().OfType<IInputElement>());
return control2.GetSelfAndVisualAncestors().OfType<IInputElement>().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<IInputElement>())
{
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);
}
}

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

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

1
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")]

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);
_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));
}
}

11
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}">
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
/>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="horizontalScrollBar"
Orientation="Horizontal"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
@ -32,4 +39,4 @@
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Style>

4
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));
}

41
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));
}
}

6
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
{

4
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]

Loading…
Cancel
Save