PointerOverTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using Avalonia.Controls;
  5. using Avalonia.Input;
  6. using Avalonia.Input.Raw;
  7. using Avalonia.Rendering;
  8. using Avalonia.UnitTests;
  9. using Moq;
  10. using Xunit;
  11. namespace Avalonia.Base.UnitTests.Input
  12. {
  13. public class PointerOverTests : PointerTestsBase
  14. {
  15. // https://github.com/AvaloniaUI/Avalonia/issues/2821
  16. [Fact]
  17. public void Close_Should_Remove_PointerOver()
  18. {
  19. using var app = UnitTestApplication.Start(new TestServices(
  20. inputManager: new InputManager(),
  21. focusManager: new FocusManager()));
  22. var renderer = new Mock<IHitTester>();
  23. var device = CreatePointerDeviceMock().Object;
  24. var impl = CreateTopLevelImplMock();
  25. Canvas canvas;
  26. var root = CreateInputRoot(impl.Object, new Panel
  27. {
  28. Children =
  29. {
  30. (canvas = new Canvas())
  31. }
  32. }, renderer.Object);
  33. SetHit(renderer, canvas);
  34. impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
  35. Assert.True(canvas.IsPointerOver);
  36. impl.Object.Closed!();
  37. Assert.False(canvas.IsPointerOver);
  38. }
  39. [Fact]
  40. public void MouseMove_Should_Update_IsPointerOver()
  41. {
  42. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  43. var renderer = new Mock<IHitTester>();
  44. var device = CreatePointerDeviceMock().Object;
  45. var impl = CreateTopLevelImplMock();
  46. Canvas canvas;
  47. Border border;
  48. Decorator decorator;
  49. var root = CreateInputRoot(impl.Object, new Panel
  50. {
  51. Children =
  52. {
  53. (canvas = new Canvas()),
  54. (border = new Border
  55. {
  56. Child = decorator = new Decorator(),
  57. })
  58. }
  59. }, renderer.Object);
  60. SetHit(renderer, decorator);
  61. impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
  62. Assert.True(decorator.IsPointerOver);
  63. Assert.True(border.IsPointerOver);
  64. Assert.False(canvas.IsPointerOver);
  65. Assert.True(root.IsPointerOver);
  66. SetHit(renderer, canvas);
  67. impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
  68. Assert.False(decorator.IsPointerOver);
  69. Assert.False(border.IsPointerOver);
  70. Assert.True(canvas.IsPointerOver);
  71. Assert.True(root.IsPointerOver);
  72. }
  73. [Fact]
  74. public void TouchMove_Should_Not_Set_IsPointerOver()
  75. {
  76. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  77. var renderer = new Mock<IHitTester>();
  78. var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object;
  79. var impl = CreateTopLevelImplMock();
  80. Canvas canvas;
  81. var root = CreateInputRoot(impl.Object, new Panel
  82. {
  83. Children =
  84. {
  85. (canvas = new Canvas())
  86. }
  87. }, renderer.Object);
  88. SetHit(renderer, canvas);
  89. impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
  90. Assert.False(canvas.IsPointerOver);
  91. Assert.False(root.IsPointerOver);
  92. }
  93. [Fact]
  94. public void HitTest_Should_Be_Ignored_If_Element_Captured()
  95. {
  96. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  97. var renderer = new Mock<IHitTester>();
  98. var pointer = new Mock<IPointer>();
  99. var device = CreatePointerDeviceMock(pointer.Object).Object;
  100. var impl = CreateTopLevelImplMock();
  101. Canvas canvas;
  102. Border border;
  103. Decorator decorator;
  104. var root = CreateInputRoot(impl.Object, new Panel
  105. {
  106. Children =
  107. {
  108. (canvas = new Canvas()),
  109. (border = new Border
  110. {
  111. Child = decorator = new Decorator(),
  112. })
  113. }
  114. }, renderer.Object);
  115. SetHit(renderer, canvas);
  116. pointer.SetupGet(p => p.Captured).Returns(decorator);
  117. impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
  118. Assert.True(decorator.IsPointerOver);
  119. Assert.True(border.IsPointerOver);
  120. Assert.False(canvas.IsPointerOver);
  121. Assert.True(root.IsPointerOver);
  122. }
  123. [Fact]
  124. public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True()
  125. {
  126. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  127. var renderer = new Mock<IHitTester>();
  128. var device = CreatePointerDeviceMock().Object;
  129. var impl = CreateTopLevelImplMock();
  130. Canvas canvas;
  131. Border border;
  132. Decorator decorator;
  133. var root = CreateInputRoot(impl.Object, new Panel
  134. {
  135. Children =
  136. {
  137. (canvas = new Canvas()),
  138. (border = new Border
  139. {
  140. Child = decorator = new Decorator(),
  141. })
  142. }
  143. }, renderer.Object);
  144. SetHit(renderer, canvas);
  145. impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
  146. Assert.False(decorator.IsPointerOver);
  147. Assert.False(border.IsPointerOver);
  148. Assert.True(canvas.IsPointerOver);
  149. Assert.True(root.IsPointerOver);
  150. // Ensure that e.Handled is reset between controls.
  151. root.PointerMoved += (s, e) => e.Handled = true;
  152. decorator.PointerEntered += (s, e) => e.Handled = true;
  153. SetHit(renderer, decorator);
  154. impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
  155. Assert.True(decorator.IsPointerOver);
  156. Assert.True(border.IsPointerOver);
  157. Assert.False(canvas.IsPointerOver);
  158. Assert.True(root.IsPointerOver);
  159. }
  160. [Fact]
  161. public void Pointer_Enter_Move_Leave_Should_Be_Followed()
  162. {
  163. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  164. var renderer = new Mock<IHitTester>();
  165. var deviceMock = CreatePointerDeviceMock();
  166. var impl = CreateTopLevelImplMock();
  167. var result = new List<(object?, string)>();
  168. void HandleEvent(object? sender, PointerEventArgs e)
  169. {
  170. result.Add((sender, e.RoutedEvent!.Name));
  171. }
  172. Canvas canvas;
  173. Border border;
  174. Decorator decorator;
  175. var root = CreateInputRoot(impl.Object, new Panel
  176. {
  177. Children =
  178. {
  179. (canvas = new Canvas()),
  180. (border = new Border
  181. {
  182. Child = decorator = new Decorator(),
  183. })
  184. }
  185. }, renderer.Object);
  186. AddEnteredExitedHandlers(HandleEvent, canvas, decorator);
  187. // Enter decorator
  188. SetHit(renderer, decorator);
  189. SetMove(deviceMock, root, decorator);
  190. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
  191. // Leave decorator
  192. SetHit(renderer, canvas);
  193. SetMove(deviceMock, root, canvas);
  194. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
  195. Assert.Equal(
  196. new[]
  197. {
  198. ((object?)decorator, nameof(InputElement.PointerEntered)),
  199. (decorator, nameof(InputElement.PointerMoved)),
  200. (decorator, nameof(InputElement.PointerExited)),
  201. (canvas, nameof(InputElement.PointerEntered)),
  202. (canvas, nameof(InputElement.PointerMoved))
  203. },
  204. result);
  205. }
  206. [Fact]
  207. public void PointerEntered_Exited_Should_Be_Raised_In_Correct_Order()
  208. {
  209. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  210. var renderer = new Mock<IHitTester>();
  211. var deviceMock = CreatePointerDeviceMock();
  212. var impl = CreateTopLevelImplMock();
  213. var result = new List<(object?, string)>();
  214. void HandleEvent(object? sender, PointerEventArgs e)
  215. {
  216. result.Add((sender, e.RoutedEvent!.Name));
  217. }
  218. Canvas canvas;
  219. Border border;
  220. Decorator decorator;
  221. var root = CreateInputRoot(impl.Object, new Panel
  222. {
  223. Children =
  224. {
  225. (canvas = new Canvas()),
  226. (border = new Border
  227. {
  228. Child = decorator = new Decorator(),
  229. })
  230. }
  231. }, renderer.Object);
  232. SetHit(renderer, canvas);
  233. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
  234. AddEnteredExitedHandlers(HandleEvent, root, canvas, border, decorator);
  235. SetHit(renderer, decorator);
  236. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
  237. Assert.Equal(
  238. new[]
  239. {
  240. ((object?)canvas, nameof(InputElement.PointerExited)),
  241. (decorator, nameof(InputElement.PointerEntered)),
  242. (border, nameof(InputElement.PointerEntered)),
  243. },
  244. result);
  245. }
  246. // https://github.com/AvaloniaUI/Avalonia/issues/7896
  247. [Fact]
  248. public void PointerEntered_Exited_Should_Set_Correct_Position()
  249. {
  250. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  251. var expectedPosition = new Point(15, 15);
  252. var renderer = new Mock<IHitTester>();
  253. var deviceMock = CreatePointerDeviceMock();
  254. var impl = CreateTopLevelImplMock();
  255. var result = new List<(object?, string, Point)>();
  256. void HandleEvent(object? sender, PointerEventArgs e)
  257. {
  258. result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null)));
  259. }
  260. Canvas canvas;
  261. var root = CreateInputRoot(impl.Object, new Panel
  262. {
  263. Children =
  264. {
  265. (canvas = new Canvas())
  266. }
  267. }, renderer.Object);
  268. AddEnteredExitedHandlers(HandleEvent, root, canvas);
  269. SetHit(renderer, canvas);
  270. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition));
  271. SetHit(renderer, null);
  272. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition));
  273. Assert.Equal(
  274. new[]
  275. {
  276. ((object?)canvas, nameof(InputElement.PointerEntered), expectedPosition),
  277. (root, nameof(InputElement.PointerEntered), expectedPosition),
  278. (canvas, nameof(InputElement.PointerExited), expectedPosition),
  279. (root, nameof(InputElement.PointerExited), expectedPosition)
  280. },
  281. result);
  282. }
  283. void RaiseSceneInvalidated(TopLevel tl) =>
  284. tl.Renderer.TriggerSceneInvalidatedForUnitTests(new Rect(0, 0, 10000, 10000));
  285. [Fact]
  286. public void Render_Invalidation_Should_Affect_PointerOver()
  287. {
  288. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  289. var renderer = new Mock<IHitTester>();
  290. var deviceMock = CreatePointerDeviceMock();
  291. var impl = CreateTopLevelImplMock();
  292. var invalidateRect = new Rect(0, 0, 15, 15);
  293. var lastClientPosition = new Point(1, 5);
  294. var result = new List<(object?, string, Point)>();
  295. void HandleEvent(object? sender, PointerEventArgs e)
  296. {
  297. result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null)));
  298. }
  299. Canvas canvas;
  300. var root = (Window)CreateInputRoot(impl.Object, new Panel
  301. {
  302. Children =
  303. {
  304. (canvas = new Canvas())
  305. }
  306. }, renderer.Object);
  307. AddEnteredExitedHandlers(HandleEvent, root, canvas);
  308. // Let input know about latest device.
  309. SetHit(renderer, canvas);
  310. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition));
  311. Assert.True(canvas.IsPointerOver);
  312. SetHit(renderer, canvas);
  313. RaiseSceneInvalidated(root);
  314. Assert.True(canvas.IsPointerOver);
  315. // Raise SceneInvalidated again, but now hide element from the hittest.
  316. SetHit(renderer, null);
  317. RaiseSceneInvalidated(root);
  318. Assert.False(canvas.IsPointerOver);
  319. Assert.Equal(
  320. new[]
  321. {
  322. ((object?)canvas, nameof(InputElement.PointerEntered), lastClientPosition),
  323. (root, nameof(InputElement.PointerEntered), lastClientPosition),
  324. (canvas, nameof(InputElement.PointerExited), lastClientPosition),
  325. (root, nameof(InputElement.PointerExited), lastClientPosition),
  326. },
  327. result);
  328. }
  329. [Fact]
  330. public void PointerOver_Invalidation_Should_Use_Previously_Captured_Element()
  331. {
  332. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  333. var renderer = new Mock<IHitTester>();
  334. var deviceMock = CreatePointerDeviceMock();
  335. var impl = CreateTopLevelImplMock();
  336. var invalidateRect = new Rect(0, 0, 15, 15);
  337. Canvas canvas1, canvas2;
  338. var root = CreateInputRoot(impl.Object, new Panel
  339. {
  340. Children =
  341. {
  342. (canvas1 = new Canvas()),
  343. (canvas2 = new Canvas())
  344. }
  345. }, renderer.Object);
  346. canvas1.PointerMoved += (s, a) => a.Pointer.Capture(canvas1);
  347. // Let input know about latest device.
  348. SetHit(renderer, canvas1);
  349. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
  350. Assert.True(canvas1.IsPointerOver);
  351. Assert.False(canvas2.IsPointerOver);
  352. SetHit(renderer, canvas2);
  353. RaiseSceneInvalidated(root);
  354. Assert.False(canvas1.IsPointerOver);
  355. Assert.True(canvas2.IsPointerOver);
  356. }
  357. // https://github.com/AvaloniaUI/Avalonia/issues/7748
  358. [Fact]
  359. public void LeaveWindow_Should_Reset_PointerOver()
  360. {
  361. using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
  362. var renderer = new Mock<IHitTester>();
  363. var deviceMock = CreatePointerDeviceMock();
  364. var impl = CreateTopLevelImplMock();
  365. var lastClientPosition = new Point(1, 5);
  366. var invalidateRect = new Rect(0, 0, 15, 15);
  367. var result = new List<(object?, string, Point)>();
  368. void HandleEvent(object? sender, PointerEventArgs e)
  369. {
  370. result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null)));
  371. }
  372. Canvas canvas;
  373. var root = CreateInputRoot(impl.Object, new Panel
  374. {
  375. Children =
  376. {
  377. (canvas = new Canvas())
  378. }
  379. }, renderer.Object);
  380. AddEnteredExitedHandlers(HandleEvent, root, canvas);
  381. // Init pointer over.
  382. SetHit(renderer, canvas);
  383. impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition));
  384. Assert.True(canvas.IsPointerOver);
  385. // Send LeaveWindow.
  386. impl.Object.Input!(new RawPointerEventArgs(deviceMock.Object, 0, root, RawPointerEventType.LeaveWindow, new Point(), default));
  387. Assert.False(canvas.IsPointerOver);
  388. Assert.Equal(
  389. new[]
  390. {
  391. ((object?)canvas, nameof(InputElement.PointerEntered), lastClientPosition),
  392. (root, nameof(InputElement.PointerEntered), lastClientPosition),
  393. (canvas, nameof(InputElement.PointerExited), lastClientPosition),
  394. (root, nameof(InputElement.PointerExited), lastClientPosition),
  395. },
  396. result);
  397. }
  398. private static void AddEnteredExitedHandlers(
  399. EventHandler<PointerEventArgs> handler,
  400. params IInputElement[] controls)
  401. {
  402. foreach (var c in controls)
  403. {
  404. c.PointerEntered += handler;
  405. c.PointerExited += handler;
  406. c.PointerMoved += handler;
  407. }
  408. }
  409. }
  410. }