123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- #nullable enable
- using System;
- using System.Collections.Generic;
- using Avalonia.Controls;
- using Avalonia.Headless;
- using Avalonia.Input;
- using Avalonia.Input.Raw;
- using Avalonia.Media;
- using Avalonia.Rendering;
- using Avalonia.UnitTests;
- using Moq;
- using Xunit;
- namespace Avalonia.Base.UnitTests.Input
- {
- public class PointerOverTests : PointerTestsBase
- {
- // https://github.com/AvaloniaUI/Avalonia/issues/2821
- [Fact]
- public void Close_Should_Remove_PointerOver()
- {
- using var app = UnitTestApplication.Start(new TestServices(
- inputManager: new InputManager(),
- renderInterface: new HeadlessPlatformRenderInterface()));
- var renderer = new Mock<IHitTester>();
- var device = CreatePointerDeviceMock().Object;
- var impl = CreateTopLevelImplMock();
- Canvas canvas;
- var root = CreateInputRoot(impl.Object, new Panel
- {
- Children =
- {
- (canvas = new Canvas())
- }
- }, renderer.Object);
- 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<IHitTester>();
- var device = CreatePointerDeviceMock().Object;
- var impl = CreateTopLevelImplMock();
- Canvas canvas;
- Border border;
- Decorator decorator;
- var root = CreateInputRoot(impl.Object, new Panel
- {
- Children =
- {
- (canvas = new Canvas()),
- (border = new Border
- {
- Child = decorator = new Decorator(),
- })
- }
- }, renderer.Object);
- 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<IHitTester>();
- var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object;
- var impl = CreateTopLevelImplMock();
- Canvas canvas;
- var root = CreateInputRoot(impl.Object, new Panel
- {
- Children =
- {
- (canvas = new Canvas())
- }
- }, renderer.Object);
- SetHit(renderer, canvas);
- impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
- Assert.False(canvas.IsPointerOver);
- Assert.False(root.IsPointerOver);
- }
- [Fact]
- public void HitTest_Should_Ignore_Non_Captured_Elements()
- {
- using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock<IHitTester>();
- var pointer = new Mock<IPointer>();
- var device = CreatePointerDeviceMock(pointer.Object).Object;
- var impl = CreateTopLevelImplMock();
- Canvas canvas;
- Border border;
- Decorator decorator;
- var root = CreateInputRoot(impl.Object, new Panel
- {
- Children =
- {
- (canvas = new Canvas()),
- (border = new Border
- {
- Child = decorator = new Decorator(),
- })
- }
- }, renderer.Object);
- pointer.SetupGet(p => p.Captured).Returns(decorator);
- // Move the pointer over the canvas: the captured decorator should lose the pointer over state.
- SetHit(renderer, canvas);
- impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
- Assert.False(decorator.IsPointerOver);
- Assert.False(border.IsPointerOver);
- Assert.False(canvas.IsPointerOver);
- Assert.False(root.IsPointerOver);
- // Move back the pointer over the decorator: raise events normally for it since it's captured.
- 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 IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True()
- {
- using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock<IHitTester>();
- var device = CreatePointerDeviceMock().Object;
- var impl = CreateTopLevelImplMock();
- Canvas canvas;
- Border border;
- Decorator decorator;
- var root = CreateInputRoot(impl.Object, new Panel
- {
- Children =
- {
- (canvas = new Canvas()),
- (border = new Border
- {
- Child = decorator = new Decorator(),
- })
- }
- }, renderer.Object);
- 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.PointerEntered += (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<IHitTester>();
- var deviceMock = CreatePointerDeviceMock();
- var impl = CreateTopLevelImplMock();
- 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(),
- })
- }
- }, renderer.Object);
- AddEnteredExitedHandlers(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, nameof(InputElement.PointerEntered)),
- (decorator, nameof(InputElement.PointerMoved)),
- (decorator, nameof(InputElement.PointerExited)),
- (canvas, nameof(InputElement.PointerEntered)),
- (canvas, nameof(InputElement.PointerMoved))
- },
- result);
- }
- [Fact]
- public void PointerEntered_Exited_Should_Be_Raised_In_Correct_Order()
- {
- using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock<IHitTester>();
- var deviceMock = CreatePointerDeviceMock();
- var impl = CreateTopLevelImplMock();
- 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(),
- })
- }
- }, renderer.Object);
- SetHit(renderer, canvas);
- impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
- AddEnteredExitedHandlers(HandleEvent, root, canvas, border, decorator);
- SetHit(renderer, decorator);
- impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
- Assert.Equal(
- new[]
- {
- ((object?)canvas, nameof(InputElement.PointerExited)),
- (decorator, nameof(InputElement.PointerEntered)),
- (border, nameof(InputElement.PointerEntered)),
- },
- result);
- }
- // https://github.com/AvaloniaUI/Avalonia/issues/7896
- [Fact]
- public void PointerEntered_Exited_Should_Set_Correct_Position()
- {
- using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var expectedPosition = new Point(15, 15);
- var renderer = new Mock<IHitTester>();
- var deviceMock = CreatePointerDeviceMock();
- var impl = CreateTopLevelImplMock();
- 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())
- }
- }, renderer.Object);
- AddEnteredExitedHandlers(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, nameof(InputElement.PointerEntered), expectedPosition),
- (root, nameof(InputElement.PointerEntered), expectedPosition),
- (canvas, nameof(InputElement.PointerExited), expectedPosition),
- (root, nameof(InputElement.PointerExited), expectedPosition)
- },
- result);
- }
- void RaiseSceneInvalidated(TopLevel tl) =>
- tl.Renderer.TriggerSceneInvalidatedForUnitTests(new Rect(0, 0, 10000, 10000));
- [Fact]
- public void Render_Invalidation_Should_Affect_PointerOver()
- {
- using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock<IHitTester>();
- var deviceMock = CreatePointerDeviceMock();
- var impl = CreateTopLevelImplMock();
- var invalidateRect = new Rect(0, 0, 15, 15);
- var lastClientPosition = new Point(1, 5);
- 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 = (Window)CreateInputRoot(impl.Object, new Panel
- {
- Children =
- {
- (canvas = new Canvas())
- }
- }, renderer.Object);
- AddEnteredExitedHandlers(HandleEvent, root, canvas);
- // Let input know about latest device.
- SetHit(renderer, canvas);
- impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition));
- Assert.True(canvas.IsPointerOver);
- SetHit(renderer, canvas);
- RaiseSceneInvalidated(root);
- Assert.True(canvas.IsPointerOver);
- // Raise SceneInvalidated again, but now hide element from the hittest.
- SetHit(renderer, null);
- RaiseSceneInvalidated(root);
- Assert.False(canvas.IsPointerOver);
- Assert.Equal(
- new[]
- {
- ((object?)canvas, nameof(InputElement.PointerEntered), lastClientPosition),
- (root, nameof(InputElement.PointerEntered), lastClientPosition),
- (canvas, nameof(InputElement.PointerExited), lastClientPosition),
- (root, nameof(InputElement.PointerExited), lastClientPosition),
- },
- result);
- }
- [Fact]
- public void PointerOver_Invalidation_Should_Use_Previously_Captured_Element()
- {
- using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock<IHitTester>();
- var deviceMock = CreatePointerDeviceMock();
- var impl = CreateTopLevelImplMock();
- var invalidateRect = new Rect(0, 0, 15, 15);
- Canvas canvas1, canvas2;
- var root = CreateInputRoot(impl.Object, new Panel
- {
- Children =
- {
- (canvas1 = new Canvas()),
- (canvas2 = new Canvas())
- }
- }, renderer.Object);
- canvas1.PointerMoved += (s, a) => a.Pointer.Capture(canvas1);
- // Let input know about latest device.
- SetHit(renderer, canvas1);
- impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
- Assert.True(canvas1.IsPointerOver);
- Assert.False(canvas2.IsPointerOver);
- SetHit(renderer, canvas2);
- RaiseSceneInvalidated(root);
- Assert.False(canvas1.IsPointerOver);
- Assert.True(canvas2.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<IHitTester>();
- var deviceMock = CreatePointerDeviceMock();
- var impl = CreateTopLevelImplMock();
- 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())
- }
- }, renderer.Object);
- AddEnteredExitedHandlers(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, nameof(InputElement.PointerEntered), lastClientPosition),
- (root, nameof(InputElement.PointerEntered), lastClientPosition),
- (canvas, nameof(InputElement.PointerExited), lastClientPosition),
- (root, nameof(InputElement.PointerExited), lastClientPosition),
- },
- result);
- }
- [Fact]
- public void Disabled_Element_Should_Set_PointerOver_On_Visual_Parent()
- {
- using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock<IHitTester>();
- var deviceMock = CreatePointerDeviceMock();
- var impl = CreateTopLevelImplMock();
- var disabledChild = new Border
- {
- Background = Brushes.Red,
- Width = 100,
- Height = 100,
- IsEnabled = false
- };
- var visualParent = new Border
- {
- Background = Brushes.Black,
- Width = 100,
- Height = 100,
- Child = disabledChild
- };
- var logicalParent = new Border
- {
- Background = Brushes.Blue,
- Width = 100,
- Height = 100
- };
- // Change the logical parent and check that we're correctly hit testing on the visual tree.
- // This scenario is made up because it's easy to test.
- // In the real world, this happens with nested Popups from MenuItems (but that's very cumbersome to test).
- ((ISetLogicalParent) disabledChild).SetParent(null);
- ((ISetLogicalParent) disabledChild).SetParent(logicalParent);
- var root = CreateInputRoot(
- impl.Object,
- new Panel
- {
- Children = { visualParent }
- },
- renderer.Object);
- Assert.False(visualParent.IsPointerOver);
- SetHit(renderer, disabledChild);
- impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, new Point(50, 50)));
- Assert.True(visualParent.IsPointerOver);
- Assert.False(logicalParent.IsPointerOver);
- }
- private static void AddEnteredExitedHandlers(
- EventHandler<PointerEventArgs> handler,
- params IInputElement[] controls)
- {
- foreach (var c in controls)
- {
- c.PointerEntered += handler;
- c.PointerExited += handler;
- c.PointerMoved += handler;
- }
- }
- }
- }
|