committed by
GitHub
41 changed files with 1260 additions and 590 deletions
@ -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); |
|||
} |
|||
} |
|||
|
|||
@ -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