LayoutableTests_EffectiveViewportChanged.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. using System;
  2. using System.Threading.Tasks;
  3. using Avalonia.Controls;
  4. using Avalonia.Controls.Presenters;
  5. using Avalonia.Controls.Primitives;
  6. using Avalonia.Controls.Templates;
  7. using Avalonia.Layout;
  8. using Avalonia.Media;
  9. using Avalonia.UnitTests;
  10. using Xunit;
  11. namespace Avalonia.Base.UnitTests.Layout
  12. {
  13. public class LayoutableTests_EffectiveViewportChanged
  14. {
  15. [Fact]
  16. public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree()
  17. {
  18. #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
  19. await RunOnUIThread.Execute(async () =>
  20. #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
  21. {
  22. var root = CreateRoot();
  23. var target = new Canvas();
  24. var raised = 0;
  25. target.EffectiveViewportChanged += (s, e) =>
  26. {
  27. ++raised;
  28. };
  29. root.Child = target;
  30. Assert.Equal(0, raised);
  31. });
  32. }
  33. [Fact]
  34. public async Task EffectiveViewportChanged_Raised_Before_LayoutUpdated()
  35. {
  36. await RunOnUIThread.Execute(async () =>
  37. {
  38. var root = CreateRoot();
  39. var target = new Canvas();
  40. var raised = 0;
  41. target.EffectiveViewportChanged += (s, e) =>
  42. {
  43. ++raised;
  44. };
  45. root.Child = target;
  46. await ExecuteInitialLayoutPass(root);
  47. Assert.Equal(1, raised);
  48. });
  49. }
  50. [Fact]
  51. public async Task Parent_Affects_EffectiveViewport()
  52. {
  53. await RunOnUIThread.Execute(async () =>
  54. {
  55. var root = CreateRoot();
  56. var target = new Canvas { Width = 100, Height = 100 };
  57. var parent = new Border { Width = 200, Height = 200, Child = target };
  58. var raised = 0;
  59. root.Child = parent;
  60. target.EffectiveViewportChanged += (s, e) =>
  61. {
  62. Assert.Equal(new Rect(-550, -400, 1200, 900), e.EffectiveViewport);
  63. ++raised;
  64. };
  65. await ExecuteInitialLayoutPass(root);
  66. });
  67. }
  68. [Fact]
  69. public async Task Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated_Raised()
  70. {
  71. await RunOnUIThread.Execute(async () =>
  72. {
  73. var root = CreateRoot();
  74. var target = new TestCanvas();
  75. var raised = 0;
  76. var layoutUpdatedRaised = 0;
  77. root.LayoutUpdated += (s, e) =>
  78. {
  79. Assert.Equal(2, target.MeasureCount);
  80. Assert.Equal(2, target.ArrangeCount);
  81. ++layoutUpdatedRaised;
  82. };
  83. target.EffectiveViewportChanged += (s, e) =>
  84. {
  85. target.InvalidateMeasure();
  86. ++raised;
  87. };
  88. root.Child = target;
  89. await ExecuteInitialLayoutPass(root);
  90. Assert.Equal(1, raised);
  91. Assert.Equal(1, layoutUpdatedRaised);
  92. });
  93. }
  94. [Fact]
  95. public async Task Viewport_Extends_Beyond_Centered_Control()
  96. {
  97. await RunOnUIThread.Execute(async () =>
  98. {
  99. var root = CreateRoot();
  100. var target = new Canvas { Width = 52, Height = 52, };
  101. var raised = 0;
  102. target.EffectiveViewportChanged += (s, e) =>
  103. {
  104. Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport);
  105. ++raised;
  106. };
  107. root.Child = target;
  108. await ExecuteInitialLayoutPass(root);
  109. Assert.Equal(1, raised);
  110. });
  111. }
  112. [Fact]
  113. public async Task Viewport_Extends_Beyond_Nested_Centered_Control()
  114. {
  115. await RunOnUIThread.Execute(async () =>
  116. {
  117. var root = CreateRoot();
  118. var target = new Canvas { Width = 52, Height = 52 };
  119. var parent = new Border { Width = 100, Height = 100, Child = target };
  120. var raised = 0;
  121. target.EffectiveViewportChanged += (s, e) =>
  122. {
  123. Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport);
  124. ++raised;
  125. };
  126. root.Child = parent;
  127. await ExecuteInitialLayoutPass(root);
  128. Assert.Equal(1, raised);
  129. });
  130. }
  131. [Fact]
  132. public async Task ScrollViewer_Determines_EffectiveViewport()
  133. {
  134. await RunOnUIThread.Execute(async () =>
  135. {
  136. var root = CreateRoot();
  137. var target = new Canvas { Width = 200, Height = 200 };
  138. var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden };
  139. var raised = 0;
  140. target.EffectiveViewportChanged += (s, e) =>
  141. {
  142. Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport);
  143. ++raised;
  144. };
  145. root.Child = scroller;
  146. await ExecuteInitialLayoutPass(root);
  147. Assert.Equal(1, raised);
  148. });
  149. }
  150. [Fact]
  151. public async Task Scrolled_ScrollViewer_Determines_EffectiveViewport()
  152. {
  153. await RunOnUIThread.Execute(async () =>
  154. {
  155. var root = CreateRoot();
  156. var target = new Canvas { Width = 200, Height = 200 };
  157. var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden };
  158. var raised = 0;
  159. root.Child = scroller;
  160. await ExecuteInitialLayoutPass(root);
  161. scroller.Offset = new Vector(0, 10);
  162. await ExecuteScrollerLayoutPass(root, scroller, target, (s, e) =>
  163. {
  164. Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport);
  165. ++raised;
  166. });
  167. Assert.Equal(1, raised);
  168. });
  169. }
  170. [Fact]
  171. public async Task Moving_Parent_Updates_EffectiveViewport()
  172. {
  173. await RunOnUIThread.Execute(async () =>
  174. {
  175. var root = CreateRoot();
  176. var target = new Canvas { Width = 100, Height = 100 };
  177. var parent = new Border { Width = 200, Height = 200, Child = target };
  178. var raised = 0;
  179. root.Child = parent;
  180. await ExecuteInitialLayoutPass(root);
  181. target.EffectiveViewportChanged += (s, e) =>
  182. {
  183. Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport);
  184. ++raised;
  185. };
  186. parent.Margin = new Thickness(8, 0, 0, 0);
  187. await ExecuteLayoutPass(root);
  188. Assert.Equal(1, raised);
  189. });
  190. }
  191. [Fact]
  192. public async Task Translate_Transform_Doesnt_Affect_EffectiveViewport()
  193. {
  194. await RunOnUIThread.Execute(async () =>
  195. {
  196. var root = CreateRoot();
  197. var target = new Canvas { Width = 100, Height = 100 };
  198. var parent = new Border { Width = 200, Height = 200, Child = target };
  199. var raised = 0;
  200. root.Child = parent;
  201. await ExecuteInitialLayoutPass(root);
  202. target.EffectiveViewportChanged += (s, e) => ++raised;
  203. target.RenderTransform = new TranslateTransform { X = 8 };
  204. target.InvalidateMeasure();
  205. await ExecuteLayoutPass(root);
  206. Assert.Equal(0, raised);
  207. });
  208. }
  209. [Fact]
  210. public async Task Translate_Transform_On_Parent_Affects_EffectiveViewport()
  211. {
  212. await RunOnUIThread.Execute(async () =>
  213. {
  214. var root = CreateRoot();
  215. var target = new Canvas { Width = 100, Height = 100 };
  216. var parent = new Border { Width = 200, Height = 200, Child = target };
  217. var raised = 0;
  218. root.Child = parent;
  219. await ExecuteInitialLayoutPass(root);
  220. target.EffectiveViewportChanged += (s, e) =>
  221. {
  222. Assert.Equal(new Rect(-558, -400, 1200, 900), e.EffectiveViewport);
  223. ++raised;
  224. };
  225. // Change the parent render transform to move it. A layout is then needed before
  226. // EffectiveViewportChanged is raised.
  227. parent.RenderTransform = new TranslateTransform { X = 8 };
  228. parent.InvalidateMeasure();
  229. await ExecuteLayoutPass(root);
  230. Assert.Equal(1, raised);
  231. });
  232. }
  233. [Fact]
  234. public async Task Rotate_Transform_On_Parent_Affects_EffectiveViewport()
  235. {
  236. await RunOnUIThread.Execute(async () =>
  237. {
  238. var root = CreateRoot();
  239. var target = new Canvas { Width = 100, Height = 100 };
  240. var parent = new Border { Width = 200, Height = 200, Child = target };
  241. var raised = 0;
  242. root.Child = parent;
  243. await ExecuteInitialLayoutPass(root);
  244. target.EffectiveViewportChanged += (s, e) =>
  245. {
  246. AssertArePixelEqual(new Rect(-651, -792, 1484, 1484), e.EffectiveViewport);
  247. ++raised;
  248. };
  249. parent.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
  250. parent.RenderTransform = new RotateTransform { Angle = 45 };
  251. parent.InvalidateMeasure();
  252. await ExecuteLayoutPass(root);
  253. Assert.Equal(1, raised);
  254. });
  255. }
  256. [Fact]
  257. public async Task Event_Unsubscribed_While_Inside_Callback()
  258. {
  259. await RunOnUIThread.Execute(async () =>
  260. {
  261. var root = CreateRoot();
  262. var target = new Canvas();
  263. var raised = 0;
  264. void OnTargetOnEffectiveViewportChanged(object s, EffectiveViewportChangedEventArgs e)
  265. {
  266. target.EffectiveViewportChanged -= OnTargetOnEffectiveViewportChanged;
  267. ++raised;
  268. }
  269. target.EffectiveViewportChanged += OnTargetOnEffectiveViewportChanged;
  270. root.Child = target;
  271. await ExecuteInitialLayoutPass(root);
  272. Assert.Equal(1, raised);
  273. });
  274. }
  275. private static TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 };
  276. private static Task ExecuteInitialLayoutPass(TestRoot root)
  277. {
  278. root.LayoutManager.ExecuteInitialLayoutPass();
  279. return Task.CompletedTask;
  280. }
  281. private static Task ExecuteLayoutPass(TestRoot root)
  282. {
  283. root.LayoutManager.ExecuteLayoutPass();
  284. return Task.CompletedTask;
  285. }
  286. private static Task ExecuteScrollerLayoutPass(
  287. TestRoot root,
  288. ScrollViewer scroller,
  289. Control target,
  290. Action<object, EffectiveViewportChangedEventArgs> handler)
  291. {
  292. void ViewportChanged(object sender, EffectiveViewportChangedEventArgs e)
  293. {
  294. handler(sender, e);
  295. }
  296. target.EffectiveViewportChanged += ViewportChanged;
  297. root.LayoutManager.ExecuteLayoutPass();
  298. return Task.CompletedTask;
  299. }
  300. private static IControlTemplate ScrollViewerTemplate()
  301. {
  302. return new FuncControlTemplate<ScrollViewer>((control, scope) => new Grid
  303. {
  304. ColumnDefinitions = new ColumnDefinitions
  305. {
  306. new ColumnDefinition(1, GridUnitType.Star),
  307. new ColumnDefinition(GridLength.Auto),
  308. },
  309. RowDefinitions = new RowDefinitions
  310. {
  311. new RowDefinition(1, GridUnitType.Star),
  312. new RowDefinition(GridLength.Auto),
  313. },
  314. Children =
  315. {
  316. new ScrollContentPresenter
  317. {
  318. Name = "PART_ContentPresenter",
  319. [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty],
  320. [~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty],
  321. [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty],
  322. [~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty],
  323. [~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty],
  324. [~ScrollContentPresenter.CanVerticallyScrollProperty] = control[~ScrollViewer.CanVerticallyScrollProperty],
  325. }.RegisterInNameScope(scope),
  326. new ScrollBar
  327. {
  328. Name = "horizontalScrollBar",
  329. Orientation = Orientation.Horizontal,
  330. [~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty],
  331. [~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty],
  332. [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty],
  333. [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty],
  334. [Grid.RowProperty] = 1,
  335. }.RegisterInNameScope(scope),
  336. new ScrollBar
  337. {
  338. Name = "verticalScrollBar",
  339. Orientation = Orientation.Vertical,
  340. [~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty],
  341. [~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty],
  342. [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty],
  343. [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty],
  344. [Grid.ColumnProperty] = 1,
  345. }.RegisterInNameScope(scope),
  346. },
  347. });
  348. }
  349. private static void AssertArePixelEqual(Rect expected, Rect actual)
  350. {
  351. var expectedRounded = new Rect((int)expected.X, (int)expected.Y, (int)expected.Width, (int)expected.Height);
  352. var actualRounded = new Rect((int)actual.X, (int)actual.Y, (int)actual.Width, (int)actual.Height);
  353. Assert.Equal(expectedRounded, actualRounded);
  354. }
  355. private class TestCanvas : Canvas
  356. {
  357. public int MeasureCount { get; private set; }
  358. public int ArrangeCount { get; private set; }
  359. protected override Size MeasureOverride(Size availableSize)
  360. {
  361. ++MeasureCount;
  362. return base.MeasureOverride(availableSize);
  363. }
  364. protected override Size ArrangeOverride(Size finalSize)
  365. {
  366. ++ArrangeCount;
  367. return base.ArrangeOverride(finalSize);
  368. }
  369. }
  370. private static class RunOnUIThread
  371. {
  372. public static async Task Execute(Func<Task> func)
  373. {
  374. await func();
  375. }
  376. }
  377. }
  378. }