PointerOverTests.cs 17 KB

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