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
{
/// <summary>
@ -30,7 +28,6 @@ namespace Avalonia.Input
/// <summary>
/// Gets associated mouse device
/// </summary>
[CanBeNull]
IMouseDevice? MouseDevice { get; }
}
}

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

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

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

@ -1,17 +1,31 @@
using System;
using Avalonia.VisualTree;
using Avalonia.Input.Raw;
namespace Avalonia.Input
{
public interface IPointerDevice : IInputDevice
{
/// <inheritdoc cref="IPointer.Captured" />
[Obsolete("Use IPointer")]
IInputElement? Captured { get; }
/// <inheritdoc cref="IPointer.Capture(IInputElement?)" />
[Obsolete("Use IPointer")]
void Capture(IInputElement? control);
/// <inheritdoc cref="PointerEventArgs.GetPosition(IVisual?)" />
[Obsolete("Use PointerEventArgs.GetPosition")]
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,
Device = this,
Key = keyInput.Key,
KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers),
KeyModifiers = keyInput.Modifiers.ToKeyModifiers(),
Source = element,
};

328
src/Avalonia.Base/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);
}
/// <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")]
public IInputElement? Captured => _pointer.Captured;
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
[Obsolete("Use events instead")]
public PixelPoint Position
{
@ -49,15 +39,7 @@ namespace Avalonia.Input
protected set => _position = value;
}
/// <summary>
/// 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>
[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<IPlatformSettings>();
@ -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<IReadOnlyList<RawPointerPoint>?>? intermediatePoints)
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties properties, KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? 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<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)
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;
}
}
}

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

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

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
/// </summary>
public Lazy<IReadOnlyList<RawPointerPoint>?>? IntermediatePoints { get; set; }
internal IInputElement? InputHitTestResult { get; set; }
}
public struct RawPointerPoint

47
src/Avalonia.Base/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
{
/// <summary>
/// Handles raw touch events
/// </summary>
/// <remarks>
/// This class is supposed to be used on per-toplevel basis, don't use a shared one
/// </remarks>
/// </summary>
public class TouchDevice : IInputDevice, IDisposable
public class TouchDevice : IPointerDevice, IDisposable
{
private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
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;
}
}
}

25
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);
}
/// <summary>
@ -283,9 +287,7 @@ namespace Avalonia.Controls
/// </summary>
IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!;
/// <summary>
/// Gets or sets the input element that the pointer is currently over.
/// </summary>
/// <inheritdoc/>
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
/// <param name="e">The event args.</param>
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()

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

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

@ -6,7 +6,6 @@ using Avalonia.Input.Raw;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
@ -34,160 +33,6 @@ namespace Avalonia.Base.UnitTests.Input
}
#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]
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())
{
inputManager.ProcessInput(new RawPointerEventArgs(
@ -238,15 +72,6 @@ namespace Avalonia.Base.UnitTests.Input
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)
{
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()
{
var inputManagerMock = new Mock<IInputManager>();
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<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]
public void Close_Should_Dispose_LayoutManager()
{

Loading…
Cancel
Save