committed by
GitHub
41 changed files with 1260 additions and 590 deletions
@ -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); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"$schema": "http://json.schemastore.org/launchsettings.json", |
||||
|
"profiles": { |
||||
|
"Compile Sandbox": { |
||||
|
"commandName": "Project", |
||||
|
"executablePath": "$(SolutionDir)\\src\\Avalonia.Build.Tasks\\bin\\Debug\\net6.0\\Avalonia.Build.Tasks.exe", |
||||
|
"commandLineArgs": "$(SolutionDir)\\samples\\Sandbox\\obj\\Debug\\net6.0\\Avalonia\\original.dll $(SolutionDir)\\samples\\Sandbox\\bin\\Debug\\net6.0\\Sandbox.dll.refs $(SolutionDir)\\out.dll" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue