From 9bef2acafe18551bd0d717c316864677e6400814 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 14 Apr 2022 11:47:25 -0400 Subject: [PATCH 1/5] Move pointerover logic out of input device layer --- src/Avalonia.Controls/TopLevel.cs | 25 +- src/Avalonia.Input/IInputRoot.cs | 3 - src/Avalonia.Input/IKeyboardDevice.cs | 6 - src/Avalonia.Input/IMouseDevice.cs | 2 + src/Avalonia.Input/IPointerDevice.cs | 16 +- src/Avalonia.Input/KeyboardDevice.cs | 2 +- src/Avalonia.Input/MouseDevice.cs | 328 +++--------------- src/Avalonia.Input/PointerEventArgs.cs | 2 + src/Avalonia.Input/PointerOverPreProcessor.cs | 210 +++++++++++ src/Avalonia.Input/Raw/RawDragEvent.cs | 2 +- src/Avalonia.Input/Raw/RawInputHelpers.cs | 27 ++ src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 2 + src/Avalonia.Input/TouchDevice.cs | 47 ++- 13 files changed, 358 insertions(+), 314 deletions(-) create mode 100644 src/Avalonia.Input/PointerOverPreProcessor.cs create mode 100644 src/Avalonia.Input/Raw/RawInputHelpers.cs diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 55202dd20d..75a34659a2 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -15,7 +15,6 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls { @@ -87,6 +86,8 @@ namespace Avalonia.Controls private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; + private readonly PointerOverPreProcessor? _pointerOverPreProcessor; + private readonly IDisposable? _pointerOverPreProcessorSubscription; private Size _clientSize; private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; @@ -195,6 +196,9 @@ namespace Avalonia.Controls } impl.LostFocus += PlatformImpl_LostFocus; + + _pointerOverPreProcessor = new PointerOverPreProcessor(this); + _pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor); } /// @@ -283,9 +287,7 @@ namespace Avalonia.Controls /// IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!; - /// - /// Gets or sets the input element that the pointer is currently over. - /// + /// IInputElement? IInputRoot.PointerOverElement { get { return GetValue(PointerOverElementProperty); } @@ -378,10 +380,12 @@ namespace Avalonia.Controls Renderer?.Dispose(); Renderer = null!; - - (this as IInputRoot).MouseDevice?.TopLevelClosed(this); + + _pointerOverPreProcessor?.OnCompleted(); + _pointerOverPreProcessorSubscription?.Dispose(); + PlatformImpl = null; - + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); @@ -515,12 +519,17 @@ namespace Avalonia.Controls /// The event args. private void HandleInput(RawInputEventArgs e) { + if (e is RawPointerEventArgs pointerArgs) + { + pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); + } + _inputManager?.ProcessInput(e); } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) { - (this as IInputRoot).MouseDevice?.SceneInvalidated(this, e.DirtyRect); + _pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect); } void PlatformImpl_LostFocus() diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs index 3e2b8cc477..98e8699573 100644 --- a/src/Avalonia.Input/IInputRoot.cs +++ b/src/Avalonia.Input/IInputRoot.cs @@ -1,5 +1,3 @@ -using JetBrains.Annotations; - namespace Avalonia.Input { /// @@ -30,7 +28,6 @@ namespace Avalonia.Input /// /// Gets associated mouse device /// - [CanBeNull] IMouseDevice? MouseDevice { get; } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 9506dc36fb..d0e84e5ad0 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -50,12 +50,6 @@ namespace Avalonia.Input KeyboardMask = Alt | Control | Shift | Meta } - internal static class KeyModifiersUtils - { - public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); - } - public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged { IInputElement? FocusedElement { get; } diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index 272d1eb8d7..6b7f0e76e5 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -13,8 +13,10 @@ namespace Avalonia.Input [Obsolete("Use PointerEventArgs.GetPosition")] PixelPoint Position { get; } + [Obsolete] void TopLevelClosed(IInputRoot root); + [Obsolete] void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index 1f82cb1ed7..0096bb77bf 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -1,17 +1,31 @@ using System; using Avalonia.VisualTree; +using Avalonia.Input.Raw; namespace Avalonia.Input { public interface IPointerDevice : IInputDevice { + /// [Obsolete("Use IPointer")] IInputElement? Captured { get; } - + + /// [Obsolete("Use IPointer")] void Capture(IInputElement? control); + /// [Obsolete("Use PointerEventArgs.GetPosition")] Point GetPosition(IVisual relativeTo); + + /// + /// Gets a pointer for specific event args. + /// + /// + /// If pointer doesn't exist or wasn't yet created this method will return null. + /// + /// Raw pointer event args associated with the pointer. + /// The pointer. + IPointer? TryGetPointer(RawPointerEventArgs ev); } } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 3df717b8c4..0600b54618 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -188,7 +188,7 @@ namespace Avalonia.Input RoutedEvent = routedEvent, Device = this, Key = keyInput.Key, - KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), + KeyModifiers = keyInput.Modifiers.ToKeyModifiers(), Source = element, }; diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index a5d54bb047..5f8ab24b79 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -21,27 +21,17 @@ namespace Avalonia.Input private readonly Pointer _pointer; private bool _disposed; - private PixelPoint? _position; + private PixelPoint? _position; + private MouseButton _lastMouseDownButton; public MouseDevice(Pointer? pointer = null) { _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// + [Obsolete("Use IPointer instead")] public IInputElement? Captured => _pointer.Captured; - /// - /// Gets the mouse position, in screen coordinates. - /// [Obsolete("Use events instead")] public PixelPoint Position { @@ -49,15 +39,7 @@ namespace Avalonia.Input protected set => _position = value; } - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// + [Obsolete("Use IPointer instead")] public void Capture(IInputElement? control) { _pointer.Capture(control); @@ -90,39 +72,6 @@ namespace Avalonia.Input ProcessRawEvent(margs); } - public void TopLevelClosed(IInputRoot root) - { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } - - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) - { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); - - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } - } - } - int ButtonCount(PointerPointProperties props) { var rv = 0; @@ -138,7 +87,7 @@ namespace Avalonia.Input rv++; return rv; } - + private void ProcessRawEvent(RawPointerEventArgs e) { e = e ?? throw new ArgumentNullException(nameof(e)); @@ -147,15 +96,14 @@ namespace Avalonia.Input if(mouse._disposed) return; - if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return; - _position = e.Root.PointToScreen(e.Position); var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); + var keyModifiers = e.InputModifiers.ToKeyModifiers(); switch (e.Type) { case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); + case RawPointerEventType.NonClientLeftButtonDown: + LeaveWindow(); break; case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: @@ -163,10 +111,9 @@ namespace Avalonia.Input case RawPointerEventType.XButton1Down: case RawPointerEventType.XButton2Down: if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.LeftButtonUp: case RawPointerEventType.RightButtonUp: @@ -174,82 +121,50 @@ namespace Avalonia.Input case RawPointerEventType.XButton1Up: case RawPointerEventType.XButton2Up: if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); break; case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Magnify: - e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Rotate: - e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Swipe: - e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); break; } } - private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) + private void LeaveWindow() { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); } - PointerPointProperties CreateProperties(RawPointerEventArgs args) { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); + return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()); } - private MouseButton _lastMouseDownButton; private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers) + KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); + var source = _pointer.Captured ?? root.InputHitTest(p); - if (hit != null) + if (source != null) { - _pointer.Capture(hit); - var source = GetSource(hit); + _pointer.Capture(source); if (source != null) { var settings = AvaloniaLocator.Current.GetService(); @@ -275,23 +190,14 @@ namespace Avalonia.Input return false; } - private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers, Lazy?>? intermediatePoints) + private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties properties, KeyModifiers inputModifiers, Lazy?>? intermediatePoints, + IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } + var source = _pointer.Captured ?? hitTest; if (source is object) { @@ -306,13 +212,12 @@ namespace Avalonia.Input } private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) + KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); - var source = GetSource(hit); + var source = _pointer.Captured ?? hitTest; if (source is not null) { @@ -329,13 +234,12 @@ namespace Avalonia.Input private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) + Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); - var source = GetSource(hit); + var source = _pointer.Captured ?? hitTest; // KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform. // If Shift-Key is pressed and X is close to 0 we swap the Vector. @@ -356,16 +260,15 @@ namespace Avalonia.Input } private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); + var source = _pointer.Captured ?? hitTest; - if (hit != null) + if (source != null) { - var source = GetSource(hit); var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, _pointer, root, p, timestamp, props, inputModifiers, delta); @@ -377,16 +280,15 @@ namespace Avalonia.Input } private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); + var source = _pointer.Captured ?? hitTest; - if (hit != null) + if (source != null) { - var source = GetSource(hit); var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, _pointer, root, p, timestamp, props, inputModifiers, delta); @@ -398,16 +300,15 @@ namespace Avalonia.Input } private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); + var source = _pointer.Captured ?? hitTest; - if (hit != null) + if (source != null) { - var source = GetSource(hit); var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, _pointer, root, p, timestamp, props, inputModifiers, delta); @@ -418,154 +319,27 @@ namespace Avalonia.Input return false; } - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) + public void Dispose() { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } + _disposed = true; + _pointer?.Dispose(); } - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) + [Obsolete] + public void TopLevelClosed(IInputRoot root) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; + // no-op } - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) + [Obsolete] + public void SceneInvalidated(IInputRoot root, Rect rect) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); - - IInputElement? branch = null; - - IInputElement? el = element; - - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } + // no-op } - public void Dispose() + public IPointer? TryGetPointer(RawPointerEventArgs ev) { - _disposed = true; - _pointer?.Dispose(); + return _pointer; } } } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 0604d09dc4..5495802920 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -63,6 +63,8 @@ namespace Avalonia.Input } public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); + + public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer; } public IPointer Pointer { get; } diff --git a/src/Avalonia.Input/PointerOverPreProcessor.cs b/src/Avalonia.Input/PointerOverPreProcessor.cs new file mode 100644 index 0000000000..a38364197d --- /dev/null +++ b/src/Avalonia.Input/PointerOverPreProcessor.cs @@ -0,0 +1,210 @@ +using System; +using Avalonia.Input.Raw; + +namespace Avalonia.Input +{ + internal class PointerOverPreProcessor : IObserver + { + private IPointerDevice? _lastActivePointerDevice; + private (IPointer pointer, PixelPoint position)? _lastPointer; + + private readonly IInputRoot _inputRoot; + + public PointerOverPreProcessor(IInputRoot inputRoot) + { + _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot)); + } + + public void OnCompleted() + { + ClearPointerOver(); + } + + public void OnError(Exception error) + { + } + + public void OnNext(RawInputEventArgs value) + { + if (value is RawPointerEventArgs args + && args.Root == _inputRoot + && value.Device is IPointerDevice pointerDevice) + { + if (pointerDevice != _lastActivePointerDevice) + { + ClearPointerOver(); + + // Set last active device before processing input, because ClearPointerOver might be called and clear last device. + _lastActivePointerDevice = pointerDevice; + } + + if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown + && _lastPointer is (var lastPointer, var lastPosition)) + { + _lastPointer = null; + ClearPointerOver(lastPointer, args.Root, 0, args.Root.PointToClient(lastPosition), + new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), + args.InputModifiers.ToKeyModifiers()); + } + else if (pointerDevice.TryGetPointer(args) is IPointer pointer + && pointer.Type != PointerType.Touch) + { + var element = pointer.Captured ?? args.InputHitTestResult; + + SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position, + new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), + args.InputModifiers.ToKeyModifiers()); + } + } + } + + public void SceneInvalidated(Rect dirtyRect) + { + // Pointer is outside of the target area + if (_lastPointer is (var pointer, var position)) + { + var clientPoint = _inputRoot.PointToClient(position); + + if (dirtyRect.Contains(clientPoint)) + { + SetPointerOver(pointer, _inputRoot, _inputRoot.InputHitTest(clientPoint), 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); + } + else if (!_inputRoot.Bounds.Contains(clientPoint)) + { + ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + } + } + } + + private void ClearPointerOver() + { + if (_lastPointer is (var pointer, var _)) + { + ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + } + _lastPointer = null; + _lastActivePointerDevice = null; + } + + private void ClearPointerOver(IPointer pointer, IInputRoot root, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + var element = root.PointerOverElement; + if (element is null) + { + return; + } + + // Do not pass rootVisual, when we have unknown (negative) position, + // so GetPosition won't return invalid values. + var hasPosition = position.X >= 0 && position.Y >= 0; + var e = new PointerEventArgs(InputElement.PointerLeaveEvent, element, pointer, + hasPosition ? root : null, hasPosition ? position : default, + timestamp, properties, inputModifiers); + + if (element != null && !element.IsAttachedToVisualTree) + { + // element has been removed from visual tree so do top down cleanup + if (root.IsPointerOver) + { + ClearChildrenPointerOver(e, root, true); + } + } + while (element != null) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + element = (IInputElement?)element.VisualParent; + } + + root.PointerOverElement = null; + _lastActivePointerDevice = null; + _lastPointer = null; + } + + private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsPointerOver) + { + ClearChildrenPointerOver(e, el, true); + break; + } + } + if (clearRoot) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + } + } + + private void SetPointerOver(IPointer pointer, IInputRoot root, IInputElement? element, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + var pointerOverElement = root.PointerOverElement; + + if (element != pointerOverElement) + { + if (element != null) + { + SetPointerOverToElement(pointer, root, element, timestamp, position, properties, inputModifiers); + } + else + { + ClearPointerOver(pointer, root, timestamp, position, properties, inputModifiers); + } + } + } + + private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + IInputElement? branch = null; + + IInputElement? el = element; + + while (el != null) + { + if (el.IsPointerOver) + { + branch = el; + break; + } + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement; + + var e = new PointerEventArgs(InputElement.PointerLeaveEvent, el, pointer, root, position, + timestamp, properties, inputModifiers); + if (el != null && branch != null && !el.IsAttachedToVisualTree) + { + ClearChildrenPointerOver(e, branch, false); + } + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement = element; + _lastPointer = (pointer, root.PointToScreen(position)); + + e.RoutedEvent = InputElement.PointerEnterEvent; + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + } + } +} diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 6e9ce20ff1..652bad7115 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input.Raw Location = location; Data = data; Effects = effects; - KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); + KeyModifiers = modifiers.ToKeyModifiers(); #pragma warning disable CS0618 // Type or member is obsolete Modifiers = (InputModifiers)modifiers; #pragma warning restore CS0618 // Type or member is obsolete diff --git a/src/Avalonia.Input/Raw/RawInputHelpers.cs b/src/Avalonia.Input/Raw/RawInputHelpers.cs new file mode 100644 index 0000000000..9d329bae59 --- /dev/null +++ b/src/Avalonia.Input/Raw/RawInputHelpers.cs @@ -0,0 +1,27 @@ +using Avalonia.Input.Raw; + +namespace Avalonia.Input +{ + internal static class RawInputHelpers + { + public static KeyModifiers ToKeyModifiers(this RawInputModifiers modifiers) => + (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); + + public static PointerUpdateKind ToUpdateKind(this RawPointerEventType type) => type switch + { + RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, + RawPointerEventType.RightButtonDown => PointerUpdateKind.RightButtonPressed, + RawPointerEventType.RightButtonUp => PointerUpdateKind.RightButtonReleased, + RawPointerEventType.MiddleButtonDown => PointerUpdateKind.MiddleButtonPressed, + RawPointerEventType.MiddleButtonUp => PointerUpdateKind.MiddleButtonReleased, + RawPointerEventType.XButton1Down => PointerUpdateKind.XButton1Pressed, + RawPointerEventType.XButton1Up => PointerUpdateKind.XButton1Released, + RawPointerEventType.XButton2Down => PointerUpdateKind.XButton2Pressed, + RawPointerEventType.XButton2Up => PointerUpdateKind.XButton2Released, + RawPointerEventType.TouchBegin => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.TouchEnd => PointerUpdateKind.LeftButtonReleased, + _ => PointerUpdateKind.Other + }; + } +} diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index c157fa059c..8b9d7c161d 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -120,6 +120,8 @@ namespace Avalonia.Input.Raw /// only valid for Move and TouchUpdate /// public Lazy?>? IntermediatePoints { get; set; } + + internal IInputElement? InputHitTestResult { get; set; } } public struct RawPointerPoint diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 20cafb9e8e..54dcc4051e 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -3,24 +3,26 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.VisualTree; namespace Avalonia.Input { /// /// Handles raw touch events + /// /// /// This class is supposed to be used on per-toplevel basis, don't use a shared one /// - /// - public class TouchDevice : IInputDevice, IDisposable + public class TouchDevice : IPointerDevice, IDisposable { private readonly Dictionary _pointers = new Dictionary(); private bool _disposed; private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; - KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); + private Pointer? _lastPointer; + + IInputElement? IPointerDevice.Captured => _lastPointer?.Captured; RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) { @@ -30,6 +32,10 @@ namespace Avalonia.Input return rv; } + void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control); + + Point IPointerDevice.GetPosition(IVisual relativeTo) => default; + public void ProcessRawEvent(RawInputEventArgs ev) { if (ev.Handled || _disposed) @@ -39,15 +45,18 @@ namespace Avalonia.Input { if (args.Type == RawPointerEventType.TouchEnd) return; - var hit = args.Root.InputHitTest(args.Position); + var hit = args.InputHitTestResult; _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Touch, _pointers.Count == 0); pointer.Capture(hit); } - + _lastPointer = pointer; var target = pointer.Captured ?? args.Root; + var updateKind = args.Type.ToUpdateKind(); + var keyModifier = args.InputModifiers.ToKeyModifiers(); + if (args.Type == RawPointerEventType.TouchBegin) { if (_pointers.Count > 1) @@ -73,9 +82,8 @@ namespace Avalonia.Input target.RaiseEvent(new PointerPressedEventArgs(target, pointer, args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), - PointerUpdateKind.LeftButtonPressed), - GetKeyModifiers(args.InputModifiers), _clickCount)); + new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind), + keyModifier, _clickCount)); } if (args.Type == RawPointerEventType.TouchEnd) @@ -85,10 +93,10 @@ namespace Avalonia.Input { target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, false), - PointerUpdateKind.LeftButtonReleased), - GetKeyModifiers(args.InputModifiers), MouseButton.Left)); + new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind), + keyModifier, MouseButton.Left)); } + _lastPointer = null; } if (args.Type == RawPointerEventType.TouchCancel) @@ -96,18 +104,16 @@ namespace Avalonia.Input _pointers.Remove(args.TouchPointId); using (pointer) pointer.Capture(null); + _lastPointer = null; } if (args.Type == RawPointerEventType.TouchUpdate) { - var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), - GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); + new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind), + keyModifier, args.IntermediatePoints)); } - - } public void Dispose() @@ -121,5 +127,12 @@ namespace Avalonia.Input p.Dispose(); } + public IPointer? TryGetPointer(RawPointerEventArgs ev) + { + return ev is RawTouchEventArgs args + && _pointers.TryGetValue(args.TouchPointId, out var pointer) + ? pointer + : null; + } } } From 8fba608156ecc275f90a44d6b7af1aaa2a7e15f5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 14 Apr 2022 11:47:35 -0400 Subject: [PATCH 2/5] Add more tests for pointerover --- .../TopLevelTests.cs | 21 +- .../MouseDeviceTests.cs | 177 ------ .../PointerOverTests.cs | 533 ++++++++++++++++++ 3 files changed, 536 insertions(+), 195 deletions(-) create mode 100644 tests/Avalonia.Input.UnitTests/PointerOverTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 9c2d760733..db6349cc5a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -193,6 +193,9 @@ namespace Avalonia.Controls.UnitTests public void Impl_Input_Should_Pass_Input_To_InputManager() { var inputManagerMock = new Mock(); + inputManagerMock.DefaultValue = DefaultValue.Mock; + inputManagerMock.SetupAllProperties(); + var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object); using (UnitTestApplication.Start(services)) @@ -249,24 +252,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Close_Should_Notify_MouseDevice() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var impl = new Mock(); - var mouseDevice = new Mock(); - impl.SetupAllProperties(); - impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object); - - var target = new TestTopLevel(impl.Object); - - impl.Object.Closed(); - - mouseDevice.Verify(x => x.TopLevelClosed(target)); - } - } - [Fact] public void Close_Should_Dispose_LayoutManager() { diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 223f458f25..e8f8244d83 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -1,13 +1,10 @@ using Avalonia.Controls; using Avalonia.Input.Raw; -using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.UnitTests; -using Avalonia.VisualTree; using Moq; using System; -using System.Collections.Generic; using Xunit; namespace Avalonia.Input.UnitTests @@ -34,160 +31,6 @@ namespace Avalonia.Input.UnitTests } #pragma warning restore CS0618 // Type or member is obsolete - [Fact] - public void MouseMove_Should_Update_IsPointerOver() - { - var renderer = new Mock(); - - using (TestApplication(renderer.Object)) - { - var inputManager = InputManager.Instance; - - Canvas canvas; - Border border; - Decorator decorator; - - var root = new TestRoot - { - MouseDevice = new MouseDevice(), - Renderer = renderer.Object, - Child = new Panel - { - Children = - { - (canvas = new Canvas()), - (border = new Border - { - Child = decorator = new Decorator(), - }) - } - } - }; - - SetHit(renderer, decorator); - SendMouseMove(inputManager, root); - - Assert.True(decorator.IsPointerOver); - Assert.True(border.IsPointerOver); - Assert.False(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - - SetHit(renderer, canvas); - SendMouseMove(inputManager, root); - - Assert.False(decorator.IsPointerOver); - Assert.False(border.IsPointerOver); - Assert.True(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - } - } - - [Fact] - public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True() - { - var renderer = new Mock(); - - using (TestApplication(renderer.Object)) - { - var inputManager = InputManager.Instance; - - Canvas canvas; - Border border; - Decorator decorator; - - var root = new TestRoot - { - MouseDevice = new MouseDevice(), - Renderer = renderer.Object, - Child = new Panel - { - Children = - { - (canvas = new Canvas()), - (border = new Border - { - Child = decorator = new Decorator(), - }) - } - } - }; - - SetHit(renderer, canvas); - SendMouseMove(inputManager, root); - - Assert.False(decorator.IsPointerOver); - Assert.False(border.IsPointerOver); - Assert.True(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - - // Ensure that e.Handled is reset between controls. - decorator.PointerEnter += (s, e) => e.Handled = true; - - SetHit(renderer, decorator); - SendMouseMove(inputManager, root); - - Assert.True(decorator.IsPointerOver); - Assert.True(border.IsPointerOver); - Assert.False(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - } - } - - [Fact] - public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order() - { - var renderer = new Mock(); - var result = new List<(object, string)>(); - - void HandleEvent(object sender, PointerEventArgs e) - { - result.Add((sender, e.RoutedEvent.Name)); - } - - using (TestApplication(renderer.Object)) - { - var inputManager = InputManager.Instance; - - Canvas canvas; - Border border; - Decorator decorator; - - var root = new TestRoot - { - MouseDevice = new MouseDevice(), - Renderer = renderer.Object, - Child = new Panel - { - Children = - { - (canvas = new Canvas()), - (border = new Border - { - Child = decorator = new Decorator(), - }) - } - } - }; - - SetHit(renderer, canvas); - SendMouseMove(inputManager, root); - - AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator); - SetHit(renderer, decorator); - SendMouseMove(inputManager, root); - - Assert.Equal( - new[] - { - ((object)canvas, "PointerLeave"), - ((object)decorator, "PointerEnter"), - ((object)border, "PointerEnter"), - }, - result); - } - } - - [Fact] public void GetPosition_Should_Respect_Control_RenderTransform() { @@ -216,17 +59,6 @@ namespace Avalonia.Input.UnitTests } } - private void AddEnterLeaveHandlers( - EventHandler handler, - params IControl[] controls) - { - foreach (var c in controls) - { - c.PointerEnter += handler; - c.PointerLeave += handler; - } - } - private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point()) { inputManager.ProcessInput(new RawPointerEventArgs( @@ -238,15 +70,6 @@ namespace Avalonia.Input.UnitTests RawInputModifiers.None)); } - private void SetHit(Mock renderer, IControl hit) - { - renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) - .Returns(new[] { hit }); - - renderer.Setup(x => x.HitTestFirst(It.IsAny(), It.IsAny(), It.IsAny>())) - .Returns(hit); - } - private IDisposable TestApplication(IRenderer renderer) { return UnitTestApplication.Start( diff --git a/tests/Avalonia.Input.UnitTests/PointerOverTests.cs b/tests/Avalonia.Input.UnitTests/PointerOverTests.cs new file mode 100644 index 0000000000..14acf9f946 --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/PointerOverTests.cs @@ -0,0 +1,533 @@ +#nullable enable +using System; +using System.Collections.Generic; + +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.UnitTests; +using Avalonia.VisualTree; + +using Moq; + +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class PointerOverTests + { + // https://github.com/AvaloniaUI/Avalonia/issues/2821 + [Fact] + public void Close_Should_Remove_PointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = CreatePointerDeviceMock().Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.True(canvas.IsPointerOver); + + impl.Object.Closed!(); + + Assert.False(canvas.IsPointerOver); + } + + [Fact] + public void MouseMove_Should_Update_IsPointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = CreatePointerDeviceMock().Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + SetHit(renderer, decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.True(decorator.IsPointerOver); + Assert.True(border.IsPointerOver); + Assert.False(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.False(decorator.IsPointerOver); + Assert.False(border.IsPointerOver); + Assert.True(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + } + + + [Fact] + public void TouchMove_Should_Not_Set_IsPointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.False(canvas.IsPointerOver); + Assert.False(root.IsPointerOver); + } + + [Fact] + public void HitTest_Should_Be_Ignored_If_Element_Captured() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var pointer = new Mock(); + var device = CreatePointerDeviceMock(pointer.Object).Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + SetHit(renderer, canvas); + pointer.SetupGet(p => p.Captured).Returns(decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.True(decorator.IsPointerOver); + Assert.True(border.IsPointerOver); + Assert.False(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + } + + [Fact] + public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = CreatePointerDeviceMock().Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.False(decorator.IsPointerOver); + Assert.False(border.IsPointerOver); + Assert.True(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + + // Ensure that e.Handled is reset between controls. + root.PointerMoved += (s, e) => e.Handled = true; + decorator.PointerEnter += (s, e) => e.Handled = true; + + SetHit(renderer, decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.True(decorator.IsPointerOver); + Assert.True(border.IsPointerOver); + Assert.False(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + } + + [Fact] + public void Pointer_Enter_Move_Leave_Should_Be_Followed() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + var result = new List<(object?, string)>(); + + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name)); + } + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + AddEnterLeaveHandlers(HandleEvent, canvas, decorator); + + // Enter decorator + SetHit(renderer, decorator); + SetMove(deviceMock, root, decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + + // Leave decorator + SetHit(renderer, canvas); + SetMove(deviceMock, root, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + + Assert.Equal( + new[] + { + ((object?)decorator, "PointerEnter"), + (decorator, "PointerMove"), + (decorator, "PointerLeave"), + (canvas, "PointerEnter"), + (canvas, "PointerMove") + }, + result); + } + + [Fact] + public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + var result = new List<(object?, string)>(); + + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name)); + } + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + + AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator); + + SetHit(renderer, decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + + Assert.Equal( + new[] + { + ((object?)canvas, "PointerLeave"), + (decorator, "PointerEnter"), + (border, "PointerEnter"), + }, + result); + } + + // https://github.com/AvaloniaUI/Avalonia/issues/7896 + [Fact] + public void PointerEnter_Leave_Should_Set_Correct_Position() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var expectedPosition = new Point(15, 15); + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + var result = new List<(object?, string, Point)>(); + + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null))); + } + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + AddEnterLeaveHandlers(HandleEvent, root, canvas); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition)); + + SetHit(renderer, null); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition)); + + Assert.Equal( + new[] + { + ((object?)canvas, "PointerEnter", expectedPosition), + (root, "PointerEnter", expectedPosition), + (canvas, "PointerLeave", expectedPosition), + (root, "PointerLeave", expectedPosition) + }, + result); + } + + [Fact] + public void Render_Invalidation_Should_Affect_PointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + + var invalidateRect = new Rect(0, 0, 15, 15); + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + // Let input know about latest device. + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + Assert.True(canvas.IsPointerOver); + + SetHit(renderer, canvas); + renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect)); + Assert.True(canvas.IsPointerOver); + + // Raise SceneInvalidated again, but now hide element from the hittest. + SetHit(renderer, null); + renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect)); + Assert.False(canvas.IsPointerOver); + } + + // https://github.com/AvaloniaUI/Avalonia/issues/7748 + [Fact] + public void LeaveWindow_Should_Reset_PointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + + var lastClientPosition = new Point(1, 5); + var invalidateRect = new Rect(0, 0, 15, 15); + var result = new List<(object?, string, Point)>(); + + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null))); + } + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + AddEnterLeaveHandlers(HandleEvent, root, canvas); + + // Init pointer over. + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition)); + Assert.True(canvas.IsPointerOver); + + // Send LeaveWindow. + impl.Object.Input!(new RawPointerEventArgs(deviceMock.Object, 0, root, RawPointerEventType.LeaveWindow, new Point(), default)); + Assert.False(canvas.IsPointerOver); + + Assert.Equal( + new[] + { + ((object?)canvas, "PointerEnter", lastClientPosition), + (root, "PointerEnter", lastClientPosition), + (canvas, "PointerLeave", lastClientPosition), + (root, "PointerLeave", lastClientPosition), + }, + result); + } + + private static void AddEnterLeaveHandlers( + EventHandler handler, + params IInputElement[] controls) + { + foreach (var c in controls) + { + c.PointerEnter += handler; + c.PointerLeave += handler; + c.PointerMoved += handler; + } + } + + private static void SetHit(Mock renderer, IControl? hit) + { + renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(hit is null ? Array.Empty() : new[] { hit }); + + renderer.Setup(x => x.HitTestFirst(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(hit); + } + + private static void SetMove(Mock deviceMock, IInputRoot root, IInputElement element) + { + deviceMock.Setup(d => d.ProcessRawEvent(It.IsAny())) + .Callback(() => element.RaiseEvent(CreatePointerMovedArgs(root, element))); + } + + private static Mock CreateTopLevelImplMock(IRenderer renderer) + { + var impl = new Mock(); + impl.DefaultValue = DefaultValue.Mock; + impl.SetupAllProperties(); + impl.SetupGet(r => r.RenderScaling).Returns(1); + impl.Setup(r => r.CreateRenderer(It.IsAny())).Returns(renderer); + impl.Setup(r => r.PointToScreen(It.IsAny())).Returns(p => new PixelPoint((int)p.X, (int)p.Y)); + impl.Setup(r => r.PointToClient(It.IsAny())).Returns(p => new Point(p.X, p.Y)); + return impl; + } + + private static IInputRoot CreateInputRoot(IWindowImpl impl, IControl child) + { + var root = new Window(impl) + { + Width = 100, + Height = 100, + Content = child, + Template = new FuncControlTemplate((w, _) => new ContentPresenter + { + Content = w.Content + }) + }; + root.Show(); + return root; + } + + private static IInputRoot CreateInputRoot(IRenderer renderer, IControl child) + { + return CreateInputRoot(CreateTopLevelImplMock(renderer).Object, child); + } + + private static RawPointerEventArgs CreateRawPointerMovedArgs( + IPointerDevice pointerDevice, + IInputRoot root, + Point? positition = null) + { + return new RawPointerEventArgs(pointerDevice, 0, root, RawPointerEventType.Move, + positition ?? default, default); + } + + private static PointerEventArgs CreatePointerMovedArgs( + IInputRoot root, IInputElement? source, Point? positition = null) + { + return new PointerEventArgs(InputElement.PointerMovedEvent, source, new Mock().Object, root, + positition ?? default, default, PointerPointProperties.None, KeyModifiers.None); + } + + private static Mock CreatePointerDeviceMock( + IPointer? pointer = null, + PointerType pointerType = PointerType.Mouse) + { + if (pointer is null) + { + var pointerMock = new Mock(); + pointerMock.SetupGet(p => p.Type).Returns(pointerType); + pointer = pointerMock.Object; + } + + var pointerDevice = new Mock(); + pointerDevice.Setup(d => d.TryGetPointer(It.IsAny())) + .Returns(pointer); + + return pointerDevice; + } + } +} From 45916251515cbe354833366fb91c43decf1541c6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 14 Apr 2022 11:47:38 -0400 Subject: [PATCH 3/5] Api compat --- src/Avalonia.Input/ApiCompatBaseline.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt index 93c4cba5a6..6e68e7aed9 100644 --- a/src/Avalonia.Input/ApiCompatBaseline.txt +++ b/src/Avalonia.Input/ApiCompatBaseline.txt @@ -14,6 +14,7 @@ MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_TextInput MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_TextInputOptionsQuery(System.EventHandler)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Input.IPointer Avalonia.Input.IPointerDevice.TryGetPointer(Avalonia.Input.Raw.RawPointerEventArgs)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetActive(System.Boolean)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetActive(System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetClient(Avalonia.Input.TextInput.ITextInputMethodClient)' is present in the implementation but not in the contract. From 3c8cc3610f5559f8b8f2610faf8ca8ab0fdb3cb6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 14 Apr 2022 11:47:56 -0400 Subject: [PATCH 4/5] Use InputHitTest instead of GetInputElementsAt+Linq in dev tools --- .../Diagnostics/ViewModels/MainViewModel.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index e08c5bc8dd..d92bbb742b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -7,7 +7,6 @@ using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.Threading; -using System.Reactive.Linq; using System.Linq; namespace Avalonia.Diagnostics.ViewModels @@ -59,8 +58,8 @@ namespace Avalonia.Diagnostics.ViewModels .Subscribe(e => { PointerOverRoot = e.Root; - PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault(); - }); + PointerOverElement = e.Root.InputHitTest(e.Position); + }); #nullable restore } Console = new ConsoleViewModel(UpdateConsoleContext); From be8bcd2be7bb31321af83a6c0ba99f87a07d67cc Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 15 Apr 2022 12:06:45 -0400 Subject: [PATCH 5/5] Update src/Avalonia.Base/Input/PointerOverPreProcessor.cs --- src/Avalonia.Base/Input/PointerOverPreProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index a38364197d..d22252893d 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -60,7 +60,6 @@ namespace Avalonia.Input public void SceneInvalidated(Rect dirtyRect) { - // Pointer is outside of the target area if (_lastPointer is (var pointer, var position)) { var clientPoint = _inputRoot.PointToClient(position);