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