LayoutableTests_EffectiveViewportChanged.cs 15 KB

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