InputElement_HitTesting.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using Avalonia.Controls;
  4. using Avalonia.Controls.Presenters;
  5. using Avalonia.Layout;
  6. using Avalonia.Media;
  7. using Avalonia.Platform;
  8. using Avalonia.Rendering;
  9. using Avalonia.UnitTests;
  10. using Moq;
  11. using System;
  12. using System.Collections.Generic;
  13. using System.IO;
  14. using Xunit;
  15. namespace Avalonia.Input.UnitTests
  16. {
  17. public class InputElement_HitTesting
  18. {
  19. [Fact]
  20. public void InputHitTest_Should_Find_Control_At_Point()
  21. {
  22. using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
  23. {
  24. var container = new Decorator
  25. {
  26. Width = 200,
  27. Height = 200,
  28. Child = new Border
  29. {
  30. Width = 100,
  31. Height = 100,
  32. HorizontalAlignment = HorizontalAlignment.Center,
  33. VerticalAlignment = VerticalAlignment.Center
  34. }
  35. };
  36. container.Measure(Size.Infinity);
  37. container.Arrange(new Rect(container.DesiredSize));
  38. var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
  39. context.Render(container);
  40. var result = container.InputHitTest(new Point(100, 100));
  41. Assert.Equal(container.Child, result);
  42. }
  43. }
  44. [Fact]
  45. public void InputHitTest_Should_Not_Find_Control_Outside_Point()
  46. {
  47. using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
  48. {
  49. var container = new Decorator
  50. {
  51. Width = 200,
  52. Height = 200,
  53. Child = new Border
  54. {
  55. Width = 100,
  56. Height = 100,
  57. HorizontalAlignment = HorizontalAlignment.Center,
  58. VerticalAlignment = VerticalAlignment.Center
  59. }
  60. };
  61. container.Measure(Size.Infinity);
  62. container.Arrange(new Rect(container.DesiredSize));
  63. var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
  64. context.Render(container);
  65. var result = container.InputHitTest(new Point(10, 10));
  66. Assert.Equal(container, result);
  67. }
  68. }
  69. [Fact]
  70. public void InputHitTest_Should_Find_Top_Control_At_Point()
  71. {
  72. using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
  73. {
  74. var container = new Panel
  75. {
  76. Width = 200,
  77. Height = 200,
  78. Children = new Controls.Controls
  79. {
  80. new Border
  81. {
  82. Width = 100,
  83. Height = 100,
  84. HorizontalAlignment = HorizontalAlignment.Center,
  85. VerticalAlignment = VerticalAlignment.Center
  86. },
  87. new Border
  88. {
  89. Width = 50,
  90. Height = 50,
  91. HorizontalAlignment = HorizontalAlignment.Center,
  92. VerticalAlignment = VerticalAlignment.Center
  93. }
  94. }
  95. };
  96. container.Measure(Size.Infinity);
  97. container.Arrange(new Rect(container.DesiredSize));
  98. var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
  99. context.Render(container);
  100. var result = container.InputHitTest(new Point(100, 100));
  101. Assert.Equal(container.Children[1], result);
  102. }
  103. }
  104. [Fact]
  105. public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder()
  106. {
  107. using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
  108. {
  109. var container = new Panel
  110. {
  111. Width = 200,
  112. Height = 200,
  113. Children = new Controls.Controls
  114. {
  115. new Border
  116. {
  117. Width = 100,
  118. Height = 100,
  119. ZIndex = 1,
  120. HorizontalAlignment = HorizontalAlignment.Center,
  121. VerticalAlignment = VerticalAlignment.Center
  122. },
  123. new Border
  124. {
  125. Width = 50,
  126. Height = 50,
  127. HorizontalAlignment = HorizontalAlignment.Center,
  128. VerticalAlignment = VerticalAlignment.Center
  129. }
  130. }
  131. };
  132. container.Measure(Size.Infinity);
  133. container.Arrange(new Rect(container.DesiredSize));
  134. var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
  135. context.Render(container);
  136. var result = container.InputHitTest(new Point(100, 100));
  137. Assert.Equal(container.Children[0], result);
  138. }
  139. }
  140. [Fact]
  141. public void InputHitTest_Should_Find_Control_Translated_Outside_Parent_Bounds()
  142. {
  143. using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
  144. {
  145. Border target;
  146. var container = new Panel
  147. {
  148. Width = 200,
  149. Height = 200,
  150. ClipToBounds = false,
  151. Children = new Controls.Controls
  152. {
  153. new Border
  154. {
  155. Width = 100,
  156. Height = 100,
  157. ZIndex = 1,
  158. HorizontalAlignment = HorizontalAlignment.Left,
  159. VerticalAlignment = VerticalAlignment.Top,
  160. Child = target = new Border
  161. {
  162. Width = 50,
  163. Height = 50,
  164. HorizontalAlignment = HorizontalAlignment.Left,
  165. VerticalAlignment = VerticalAlignment.Top,
  166. RenderTransform = new TranslateTransform(110, 110),
  167. }
  168. },
  169. }
  170. };
  171. container.Measure(Size.Infinity);
  172. container.Arrange(new Rect(container.DesiredSize));
  173. var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
  174. context.Render(container);
  175. var result = container.InputHitTest(new Point(120, 120));
  176. Assert.Equal(target, result);
  177. }
  178. }
  179. [Fact]
  180. public void InputHitTest_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped()
  181. {
  182. using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
  183. {
  184. Border target;
  185. var container = new Panel
  186. {
  187. Width = 100,
  188. Height = 200,
  189. Children = new Controls.Controls
  190. {
  191. new Panel()
  192. {
  193. Width = 100,
  194. Height = 100,
  195. Margin = new Thickness(0, 100, 0, 0),
  196. ClipToBounds = true,
  197. Children = new Controls.Controls
  198. {
  199. (target = new Border()
  200. {
  201. Width = 100,
  202. Height = 100,
  203. Margin = new Thickness(0, -100, 0, 0)
  204. })
  205. }
  206. }
  207. }
  208. };
  209. container.Measure(Size.Infinity);
  210. container.Arrange(new Rect(container.DesiredSize));
  211. var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
  212. context.Render(container);
  213. var result = container.InputHitTest(new Point(50, 50));
  214. Assert.NotEqual(target, result);
  215. Assert.Equal(container, result);
  216. }
  217. }
  218. [Fact]
  219. public void InputHitTest_Should_Not_Find_Control_Outside_Scroll_ViewPort()
  220. {
  221. using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
  222. {
  223. Border target;
  224. Border item1;
  225. Border item2;
  226. ScrollContentPresenter scroll;
  227. var container = new Panel
  228. {
  229. Width = 100,
  230. Height = 200,
  231. Children = new Controls.Controls
  232. {
  233. (target = new Border()
  234. {
  235. Width = 100,
  236. Height = 100
  237. }),
  238. new Border()
  239. {
  240. Width = 100,
  241. Height = 100,
  242. Margin = new Thickness(0, 100, 0, 0),
  243. Child = scroll = new ScrollContentPresenter()
  244. {
  245. Content = new StackPanel()
  246. {
  247. Children = new Controls.Controls
  248. {
  249. (item1 = new Border()
  250. {
  251. Width = 100,
  252. Height = 100,
  253. }),
  254. (item2 = new Border()
  255. {
  256. Width = 100,
  257. Height = 100,
  258. }),
  259. }
  260. }
  261. }
  262. }
  263. }
  264. };
  265. scroll.UpdateChild();
  266. container.Measure(Size.Infinity);
  267. container.Arrange(new Rect(container.DesiredSize));
  268. var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
  269. context.Render(container);
  270. var result = container.InputHitTest(new Point(50, 150));
  271. Assert.Equal(item1, result);
  272. result = container.InputHitTest(new Point(50, 50));
  273. Assert.Equal(target, result);
  274. scroll.Offset = new Vector(0, 100);
  275. //we don't have setup LayoutManager so we will make it manually
  276. scroll.Parent.InvalidateArrange();
  277. container.InvalidateArrange();
  278. container.Arrange(new Rect(container.DesiredSize));
  279. context.Render(container);
  280. result = container.InputHitTest(new Point(50, 150));
  281. Assert.Equal(item2, result);
  282. result = container.InputHitTest(new Point(50, 50));
  283. Assert.NotEqual(item1, result);
  284. Assert.Equal(target, result);
  285. }
  286. }
  287. class MockRenderInterface : IPlatformRenderInterface
  288. {
  289. public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList<FormattedTextStyleSpan> spans)
  290. {
  291. throw new NotImplementedException();
  292. }
  293. public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
  294. {
  295. throw new NotImplementedException();
  296. }
  297. public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
  298. {
  299. throw new NotImplementedException();
  300. }
  301. public IStreamGeometryImpl CreateStreamGeometry()
  302. {
  303. return new MockStreamGeometry();
  304. }
  305. public IBitmapImpl LoadBitmap(Stream stream)
  306. {
  307. throw new NotImplementedException();
  308. }
  309. public IBitmapImpl LoadBitmap(string fileName)
  310. {
  311. throw new NotImplementedException();
  312. }
  313. public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, int width, int height, int stride)
  314. {
  315. throw new NotImplementedException();
  316. }
  317. public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt)
  318. {
  319. throw new NotImplementedException();
  320. }
  321. class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl
  322. {
  323. private MockStreamGeometryContext _impl = new MockStreamGeometryContext();
  324. public Rect Bounds
  325. {
  326. get
  327. {
  328. throw new NotImplementedException();
  329. }
  330. }
  331. public Matrix Transform
  332. {
  333. get
  334. {
  335. throw new NotImplementedException();
  336. }
  337. set
  338. {
  339. throw new NotImplementedException();
  340. }
  341. }
  342. public IStreamGeometryImpl Clone()
  343. {
  344. return this;
  345. }
  346. public bool FillContains(Point point)
  347. {
  348. return _impl.FillContains(point);
  349. }
  350. public Rect GetRenderBounds(double strokeThickness)
  351. {
  352. throw new NotImplementedException();
  353. }
  354. public IGeometryImpl Intersect(IGeometryImpl geometry)
  355. {
  356. throw new NotImplementedException();
  357. }
  358. public IStreamGeometryContextImpl Open()
  359. {
  360. return _impl;
  361. }
  362. public bool StrokeContains(Pen pen, Point point)
  363. {
  364. throw new NotImplementedException();
  365. }
  366. public IGeometryImpl WithTransform(Matrix transform)
  367. {
  368. throw new NotImplementedException();
  369. }
  370. class MockStreamGeometryContext : IStreamGeometryContextImpl
  371. {
  372. private List<Point> points = new List<Point>();
  373. public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
  374. {
  375. throw new NotImplementedException();
  376. }
  377. public void BeginFigure(Point startPoint, bool isFilled)
  378. {
  379. points.Add(startPoint);
  380. }
  381. public void CubicBezierTo(Point point1, Point point2, Point point3)
  382. {
  383. throw new NotImplementedException();
  384. }
  385. public void Dispose()
  386. {
  387. }
  388. public void EndFigure(bool isClosed)
  389. {
  390. }
  391. public void LineTo(Point point)
  392. {
  393. points.Add(point);
  394. }
  395. public void QuadraticBezierTo(Point control, Point endPoint)
  396. {
  397. throw new NotImplementedException();
  398. }
  399. public void SetFillRule(FillRule fillRule)
  400. {
  401. }
  402. public bool FillContains(Point point)
  403. {
  404. // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html
  405. // to determine if the point is in the geometry (since it will always be convex in this situation)
  406. for (int i = 0; i < points.Count; i++)
  407. {
  408. var a = points[i];
  409. var b = points[(i + 1) % points.Count];
  410. var c = points[(i + 2) % points.Count];
  411. Vector v0 = c - a;
  412. Vector v1 = b - a;
  413. Vector v2 = point - a;
  414. var dot00 = v0 * v0;
  415. var dot01 = v0 * v1;
  416. var dot02 = v0 * v2;
  417. var dot11 = v1 * v1;
  418. var dot12 = v1 * v2;
  419. var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
  420. var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
  421. var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
  422. if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
  423. }
  424. return false;
  425. }
  426. }
  427. }
  428. }
  429. }
  430. }