Browse Source

Merge pull request #7905 from AvaloniaUI/refactor-pointerover

"PointerOver" refactoring, moved shared code from (Pen)MouseDevice to the top level
pull/7994/head
Max Katz 5 years ago
committed by GitHub
parent
commit
a7133f0919
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      src/Avalonia.Base/Input/IInputRoot.cs
  2. 6
      src/Avalonia.Base/Input/IKeyboardDevice.cs
  3. 2
      src/Avalonia.Base/Input/IMouseDevice.cs
  4. 16
      src/Avalonia.Base/Input/IPointerDevice.cs
  5. 2
      src/Avalonia.Base/Input/KeyboardDevice.cs
  6. 328
      src/Avalonia.Base/Input/MouseDevice.cs
  7. 2
      src/Avalonia.Base/Input/PointerEventArgs.cs
  8. 209
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  9. 2
      src/Avalonia.Base/Input/Raw/RawDragEvent.cs
  10. 27
      src/Avalonia.Base/Input/Raw/RawInputHelpers.cs
  11. 2
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  12. 47
      src/Avalonia.Base/Input/TouchDevice.cs
  13. 25
      src/Avalonia.Controls/TopLevel.cs
  14. 5
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  15. 175
      tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
  16. 534
      tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
  17. 21
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

3
src/Avalonia.Base/Input/IInputRoot.cs

@ -1,5 +1,3 @@
using JetBrains.Annotations;
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>
@ -30,7 +28,6 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets associated mouse device /// Gets associated mouse device
/// </summary> /// </summary>
[CanBeNull]
IMouseDevice? MouseDevice { get; } IMouseDevice? MouseDevice { get; }
} }
} }

6
src/Avalonia.Base/Input/IKeyboardDevice.cs

@ -50,12 +50,6 @@ namespace Avalonia.Input
KeyboardMask = Alt | Control | Shift | Meta KeyboardMask = Alt | Control | Shift | Meta
} }
internal static class KeyModifiersUtils
{
public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) =>
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
}
public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged
{ {
IInputElement? FocusedElement { get; } IInputElement? FocusedElement { get; }

2
src/Avalonia.Base/Input/IMouseDevice.cs

@ -13,8 +13,10 @@ namespace Avalonia.Input
[Obsolete("Use PointerEventArgs.GetPosition")] [Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; } PixelPoint Position { get; }
[Obsolete]
void TopLevelClosed(IInputRoot root); void TopLevelClosed(IInputRoot root);
[Obsolete]
void SceneInvalidated(IInputRoot root, Rect rect); void SceneInvalidated(IInputRoot root, Rect rect);
} }
} }

16
src/Avalonia.Base/Input/IPointerDevice.cs

@ -1,17 +1,31 @@
using System; using System;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Input.Raw;
namespace Avalonia.Input namespace Avalonia.Input
{ {
public interface IPointerDevice : IInputDevice public interface IPointerDevice : IInputDevice
{ {
/// <inheritdoc cref="IPointer.Captured" />
[Obsolete("Use IPointer")] [Obsolete("Use IPointer")]
IInputElement? Captured { get; } IInputElement? Captured { get; }
/// <inheritdoc cref="IPointer.Capture(IInputElement?)" />
[Obsolete("Use IPointer")] [Obsolete("Use IPointer")]
void Capture(IInputElement? control); void Capture(IInputElement? control);
/// <inheritdoc cref="PointerEventArgs.GetPosition(IVisual?)" />
[Obsolete("Use PointerEventArgs.GetPosition")] [Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo); Point GetPosition(IVisual relativeTo);
/// <summary>
/// Gets a pointer for specific event args.
/// </summary>
/// <remarks>
/// If pointer doesn't exist or wasn't yet created this method will return null.
/// </remarks>
/// <param name="ev">Raw pointer event args associated with the pointer.</param>
/// <returns>The pointer.</returns>
IPointer? TryGetPointer(RawPointerEventArgs ev);
} }
} }

2
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -188,7 +188,7 @@ namespace Avalonia.Input
RoutedEvent = routedEvent, RoutedEvent = routedEvent,
Device = this, Device = this,
Key = keyInput.Key, Key = keyInput.Key,
KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), KeyModifiers = keyInput.Modifiers.ToKeyModifiers(),
Source = element, Source = element,
}; };

328
src/Avalonia.Base/Input/MouseDevice.cs

@ -21,27 +21,17 @@ namespace Avalonia.Input
private readonly Pointer _pointer; private readonly Pointer _pointer;
private bool _disposed; private bool _disposed;
private PixelPoint? _position; private PixelPoint? _position;
private MouseButton _lastMouseDownButton;
public MouseDevice(Pointer? pointer = null) public MouseDevice(Pointer? pointer = null)
{ {
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
} }
/// <summary>
/// Gets the control that is currently capturing by the mouse, if any.
/// </summary>
/// <remarks>
/// 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
/// <see cref="Capture"/> method.
/// </remarks>
[Obsolete("Use IPointer instead")] [Obsolete("Use IPointer instead")]
public IInputElement? Captured => _pointer.Captured; public IInputElement? Captured => _pointer.Captured;
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
[Obsolete("Use events instead")] [Obsolete("Use events instead")]
public PixelPoint Position public PixelPoint Position
{ {
@ -49,15 +39,7 @@ namespace Avalonia.Input
protected set => _position = value; protected set => _position = value;
} }
/// <summary> [Obsolete("Use IPointer instead")]
/// Captures mouse input to the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <remarks>
/// 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 <see cref="Captured"/> property.
/// </remarks>
public void Capture(IInputElement? control) public void Capture(IInputElement? control)
{ {
_pointer.Capture(control); _pointer.Capture(control);
@ -90,39 +72,6 @@ namespace Avalonia.Input
ProcessRawEvent(margs); 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) int ButtonCount(PointerPointProperties props)
{ {
var rv = 0; var rv = 0;
@ -138,7 +87,7 @@ namespace Avalonia.Input
rv++; rv++;
return rv; return rv;
} }
private void ProcessRawEvent(RawPointerEventArgs e) private void ProcessRawEvent(RawPointerEventArgs e)
{ {
e = e ?? throw new ArgumentNullException(nameof(e)); e = e ?? throw new ArgumentNullException(nameof(e));
@ -147,15 +96,14 @@ namespace Avalonia.Input
if(mouse._disposed) if(mouse._disposed)
return; return;
if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return;
_position = e.Root.PointToScreen(e.Position); _position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e); var props = CreateProperties(e);
var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); var keyModifiers = e.InputModifiers.ToKeyModifiers();
switch (e.Type) switch (e.Type)
{ {
case RawPointerEventType.LeaveWindow: case RawPointerEventType.LeaveWindow:
LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); case RawPointerEventType.NonClientLeftButtonDown:
LeaveWindow();
break; break;
case RawPointerEventType.LeftButtonDown: case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown: case RawPointerEventType.RightButtonDown:
@ -163,10 +111,9 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Down: case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down: case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1) 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 else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
props, keyModifiers);
break; break;
case RawPointerEventType.LeftButtonUp: case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp: case RawPointerEventType.RightButtonUp:
@ -174,82 +121,50 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Up: case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up: case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0) 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 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; break;
case RawPointerEventType.Move: 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; break;
case RawPointerEventType.Wheel: 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; break;
case RawPointerEventType.Magnify: 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; break;
case RawPointerEventType.Rotate: 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; break;
case RawPointerEventType.Swipe: 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; break;
} }
} }
private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, private void LeaveWindow()
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
_position = null; _position = null;
ClearPointerOver(this, timestamp, root, properties, inputModifiers);
} }
PointerPointProperties CreateProperties(RawPointerEventArgs args) PointerPointProperties CreateProperties(RawPointerEventArgs args)
{ {
return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind());
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);
} }
private MouseButton _lastMouseDownButton;
private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p,
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); 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); _pointer.Capture(source);
var source = GetSource(hit);
if (source != null) if (source != null)
{ {
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>(); var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
@ -275,23 +190,14 @@ namespace Avalonia.Input
return false; return false;
} }
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints) PointerPointProperties properties, KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints,
IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
IInputElement? source; var source = _pointer.Captured ?? hitTest;
if (_pointer.Captured == null)
{
source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers);
}
else
{
SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers);
source = _pointer.Captured;
}
if (source is object) if (source is object)
{ {
@ -306,13 +212,12 @@ namespace Avalonia.Input
} }
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, 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)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
var source = GetSource(hit);
if (source is not null) if (source is not null)
{ {
@ -329,13 +234,12 @@ namespace Avalonia.Input
private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, PointerPointProperties props,
Vector delta, KeyModifiers inputModifiers) Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
var source = GetSource(hit);
// KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform. // 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. // 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, 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)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); 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, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _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, 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)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); 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, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _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, 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)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); 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, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _pointer, root, p, timestamp, props, inputModifiers, delta);
@ -418,154 +319,27 @@ namespace Avalonia.Input
return false; return false;
} }
private IInteractive? GetSource(IVisual? hit) public void Dispose()
{
if (hit is null)
return null;
return _pointer.Captured ??
(hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType<IInteractive>().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)
{ {
foreach (IInputElement el in element.VisualChildren) _disposed = true;
{ _pointer?.Dispose();
if (el.IsPointerOver)
{
ClearChildrenPointerOver(e, el, true);
break;
}
}
if(clearRoot)
{
e.Source = element;
e.Handled = false;
element.RaiseEvent(e);
}
} }
private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, [Obsolete]
PointerPointProperties properties, public void TopLevelClosed(IInputRoot root)
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); // no-op
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;
} }
private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, [Obsolete]
PointerPointProperties properties, public void SceneInvalidated(IInputRoot root, Rect rect)
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); // no-op
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;
}
} }
public void Dispose() public IPointer? TryGetPointer(RawPointerEventArgs ev)
{ {
_disposed = true; return _pointer;
_pointer?.Dispose();
} }
} }
} }

2
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -63,6 +63,8 @@ namespace Avalonia.Input
} }
public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
} }
public IPointer Pointer { get; } public IPointer Pointer { get; }

209
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@ -0,0 +1,209 @@
using System;
using Avalonia.Input.Raw;
namespace Avalonia.Input
{
internal class PointerOverPreProcessor : IObserver<RawInputEventArgs>
{
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)
{
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;
}
}
}
}

2
src/Avalonia.Base/Input/Raw/RawDragEvent.cs

@ -20,7 +20,7 @@ namespace Avalonia.Input.Raw
Location = location; Location = location;
Data = data; Data = data;
Effects = effects; Effects = effects;
KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); KeyModifiers = modifiers.ToKeyModifiers();
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)modifiers; Modifiers = (InputModifiers)modifiers;
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete

27
src/Avalonia.Base/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
};
}
}

2
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@ -120,6 +120,8 @@ namespace Avalonia.Input.Raw
/// only valid for Move and TouchUpdate /// only valid for Move and TouchUpdate
/// </summary> /// </summary>
public Lazy<IReadOnlyList<RawPointerPoint>?>? IntermediatePoints { get; set; } public Lazy<IReadOnlyList<RawPointerPoint>?>? IntermediatePoints { get; set; }
internal IInputElement? InputHitTestResult { get; set; }
} }
public struct RawPointerPoint public struct RawPointerPoint

47
src/Avalonia.Base/Input/TouchDevice.cs

@ -3,24 +3,26 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>
/// Handles raw touch events /// Handles raw touch events
/// </summary>
/// <remarks> /// <remarks>
/// This class is supposed to be used on per-toplevel basis, don't use a shared one /// This class is supposed to be used on per-toplevel basis, don't use a shared one
/// </remarks> /// </remarks>
/// </summary> public class TouchDevice : IPointerDevice, IDisposable
public class TouchDevice : IInputDevice, IDisposable
{ {
private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>(); private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
private bool _disposed; private bool _disposed;
private int _clickCount; private int _clickCount;
private Rect _lastClickRect; private Rect _lastClickRect;
private ulong _lastClickTime; private ulong _lastClickTime;
KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => private Pointer? _lastPointer;
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
IInputElement? IPointerDevice.Captured => _lastPointer?.Captured;
RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
{ {
@ -30,6 +32,10 @@ namespace Avalonia.Input
return rv; return rv;
} }
void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control);
Point IPointerDevice.GetPosition(IVisual relativeTo) => default;
public void ProcessRawEvent(RawInputEventArgs ev) public void ProcessRawEvent(RawInputEventArgs ev)
{ {
if (ev.Handled || _disposed) if (ev.Handled || _disposed)
@ -39,15 +45,18 @@ namespace Avalonia.Input
{ {
if (args.Type == RawPointerEventType.TouchEnd) if (args.Type == RawPointerEventType.TouchEnd)
return; return;
var hit = args.Root.InputHitTest(args.Position); var hit = args.InputHitTestResult;
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Touch, _pointers.Count == 0); PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit); pointer.Capture(hit);
} }
_lastPointer = pointer;
var target = pointer.Captured ?? args.Root; var target = pointer.Captured ?? args.Root;
var updateKind = args.Type.ToUpdateKind();
var keyModifier = args.InputModifiers.ToKeyModifiers();
if (args.Type == RawPointerEventType.TouchBegin) if (args.Type == RawPointerEventType.TouchBegin)
{ {
if (_pointers.Count > 1) if (_pointers.Count > 1)
@ -73,9 +82,8 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerPressedEventArgs(target, pointer, target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp, args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
PointerUpdateKind.LeftButtonPressed), keyModifier, _clickCount));
GetKeyModifiers(args.InputModifiers), _clickCount));
} }
if (args.Type == RawPointerEventType.TouchEnd) if (args.Type == RawPointerEventType.TouchEnd)
@ -85,10 +93,10 @@ namespace Avalonia.Input
{ {
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp, args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, false), new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
PointerUpdateKind.LeftButtonReleased), keyModifier, MouseButton.Left));
GetKeyModifiers(args.InputModifiers), MouseButton.Left));
} }
_lastPointer = null;
} }
if (args.Type == RawPointerEventType.TouchCancel) if (args.Type == RawPointerEventType.TouchCancel)
@ -96,18 +104,16 @@ namespace Avalonia.Input
_pointers.Remove(args.TouchPointId); _pointers.Remove(args.TouchPointId);
using (pointer) using (pointer)
pointer.Capture(null); pointer.Capture(null);
_lastPointer = null;
} }
if (args.Type == RawPointerEventType.TouchUpdate) if (args.Type == RawPointerEventType.TouchUpdate)
{ {
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); keyModifier, args.IntermediatePoints));
} }
} }
public void Dispose() public void Dispose()
@ -121,5 +127,12 @@ namespace Avalonia.Input
p.Dispose(); p.Dispose();
} }
public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
return ev is RawTouchEventArgs args
&& _pointers.TryGetValue(args.TouchPointId, out var pointer)
? pointer
: null;
}
} }
} }

25
src/Avalonia.Controls/TopLevel.cs

@ -15,7 +15,6 @@ using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -87,6 +86,8 @@ namespace Avalonia.Controls
private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler;
private readonly IPlatformRenderInterface? _renderInterface; private readonly IPlatformRenderInterface? _renderInterface;
private readonly IGlobalStyles? _globalStyles; private readonly IGlobalStyles? _globalStyles;
private readonly PointerOverPreProcessor? _pointerOverPreProcessor;
private readonly IDisposable? _pointerOverPreProcessorSubscription;
private Size _clientSize; private Size _clientSize;
private Size? _frameSize; private Size? _frameSize;
private WindowTransparencyLevel _actualTransparencyLevel; private WindowTransparencyLevel _actualTransparencyLevel;
@ -195,6 +196,9 @@ namespace Avalonia.Controls
} }
impl.LostFocus += PlatformImpl_LostFocus; impl.LostFocus += PlatformImpl_LostFocus;
_pointerOverPreProcessor = new PointerOverPreProcessor(this);
_pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor);
} }
/// <summary> /// <summary>
@ -283,9 +287,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!; IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!;
/// <summary> /// <inheritdoc/>
/// Gets or sets the input element that the pointer is currently over.
/// </summary>
IInputElement? IInputRoot.PointerOverElement IInputElement? IInputRoot.PointerOverElement
{ {
get { return GetValue(PointerOverElementProperty); } get { return GetValue(PointerOverElementProperty); }
@ -378,10 +380,12 @@ namespace Avalonia.Controls
Renderer?.Dispose(); Renderer?.Dispose();
Renderer = null!; Renderer = null!;
(this as IInputRoot).MouseDevice?.TopLevelClosed(this); _pointerOverPreProcessor?.OnCompleted();
_pointerOverPreProcessorSubscription?.Dispose();
PlatformImpl = null; PlatformImpl = null;
var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
@ -515,12 +519,17 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
private void HandleInput(RawInputEventArgs e) private void HandleInput(RawInputEventArgs e)
{ {
if (e is RawPointerEventArgs pointerArgs)
{
pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position);
}
_inputManager?.ProcessInput(e); _inputManager?.ProcessInput(e);
} }
private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
{ {
(this as IInputRoot).MouseDevice?.SceneInvalidated(this, e.DirtyRect); _pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect);
} }
void PlatformImpl_LostFocus() void PlatformImpl_LostFocus()

5
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -7,7 +7,6 @@ using Avalonia.Diagnostics.Models;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Threading; using Avalonia.Threading;
using System.Reactive.Linq;
using System.Linq; using System.Linq;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
@ -59,8 +58,8 @@ namespace Avalonia.Diagnostics.ViewModels
.Subscribe(e => .Subscribe(e =>
{ {
PointerOverRoot = e.Root; PointerOverRoot = e.Root;
PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault(); PointerOverElement = e.Root.InputHitTest(e.Position);
}); });
#nullable restore #nullable restore
} }
Console = new ConsoleViewModel(UpdateConsoleContext); Console = new ConsoleViewModel(UpdateConsoleContext);

175
tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs

@ -6,7 +6,6 @@ using Avalonia.Input.Raw;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq; using Moq;
using Xunit; using Xunit;
@ -34,160 +33,6 @@ namespace Avalonia.Base.UnitTests.Input
} }
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
[Fact]
public void MouseMove_Should_Update_IsPointerOver()
{
var renderer = new Mock<IRenderer>();
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<IRenderer>();
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<IRenderer>();
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] [Fact]
public void GetPosition_Should_Respect_Control_RenderTransform() public void GetPosition_Should_Respect_Control_RenderTransform()
{ {
@ -216,17 +61,6 @@ namespace Avalonia.Base.UnitTests.Input
} }
} }
private void AddEnterLeaveHandlers(
EventHandler<PointerEventArgs> 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()) private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point())
{ {
inputManager.ProcessInput(new RawPointerEventArgs( inputManager.ProcessInput(new RawPointerEventArgs(
@ -238,15 +72,6 @@ namespace Avalonia.Base.UnitTests.Input
RawInputModifiers.None)); RawInputModifiers.None));
} }
private void SetHit(Mock<IRenderer> renderer, IControl hit)
{
renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(new[] { hit });
renderer.Setup(x => x.HitTestFirst(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(hit);
}
private IDisposable TestApplication(IRenderer renderer) private IDisposable TestApplication(IRenderer renderer)
{ {
return UnitTestApplication.Start( return UnitTestApplication.Start(

534
tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs

@ -0,0 +1,534 @@
#nullable enable
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Input
{
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<IRenderer>();
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<IRenderer>();
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<IRenderer>();
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<IRenderer>();
var pointer = new Mock<IPointer>();
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<IRenderer>();
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<IRenderer>();
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<IRenderer>();
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<IRenderer>();
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<IRenderer>();
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<IRenderer>();
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<PointerEventArgs> handler,
params IInputElement[] controls)
{
foreach (var c in controls)
{
c.PointerEnter += handler;
c.PointerLeave += handler;
c.PointerMoved += handler;
}
}
private static void SetHit(Mock<IRenderer> renderer, IControl? hit)
{
renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(hit is null ? Array.Empty<IControl>() : new[] { hit });
renderer.Setup(x => x.HitTestFirst(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(hit);
}
private static void SetMove(Mock<IPointerDevice> deviceMock, IInputRoot root, IInputElement element)
{
deviceMock.Setup(d => d.ProcessRawEvent(It.IsAny<RawPointerEventArgs>()))
.Callback(() => element.RaiseEvent(CreatePointerMovedArgs(root, element)));
}
private static Mock<IWindowImpl> CreateTopLevelImplMock(IRenderer renderer)
{
var impl = new Mock<IWindowImpl>();
impl.DefaultValue = DefaultValue.Mock;
impl.SetupAllProperties();
impl.SetupGet(r => r.RenderScaling).Returns(1);
impl.Setup(r => r.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer);
impl.Setup(r => r.PointToScreen(It.IsAny<Point>())).Returns<Point>(p => new PixelPoint((int)p.X, (int)p.Y));
impl.Setup(r => r.PointToClient(It.IsAny<PixelPoint>())).Returns<PixelPoint>(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<Window>((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<IPointer>().Object, root,
positition ?? default, default, PointerPointProperties.None, KeyModifiers.None);
}
private static Mock<IPointerDevice> CreatePointerDeviceMock(
IPointer? pointer = null,
PointerType pointerType = PointerType.Mouse)
{
if (pointer is null)
{
var pointerMock = new Mock<IPointer>();
pointerMock.SetupGet(p => p.Type).Returns(pointerType);
pointer = pointerMock.Object;
}
var pointerDevice = new Mock<IPointerDevice>();
pointerDevice.Setup(d => d.TryGetPointer(It.IsAny<RawPointerEventArgs>()))
.Returns(pointer);
return pointerDevice;
}
}
}

21
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@ -193,6 +193,9 @@ namespace Avalonia.Controls.UnitTests
public void Impl_Input_Should_Pass_Input_To_InputManager() public void Impl_Input_Should_Pass_Input_To_InputManager()
{ {
var inputManagerMock = new Mock<IInputManager>(); var inputManagerMock = new Mock<IInputManager>();
inputManagerMock.DefaultValue = DefaultValue.Mock;
inputManagerMock.SetupAllProperties();
var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object); var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object);
using (UnitTestApplication.Start(services)) 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<ITopLevelImpl>();
var mouseDevice = new Mock<IMouseDevice>();
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] [Fact]
public void Close_Should_Dispose_LayoutManager() public void Close_Should_Dispose_LayoutManager()
{ {

Loading…
Cancel
Save