LayoutableTests_EffectiveViewportChanged.cs 15 KB

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