LayoutableTests_EffectiveViewportChanged.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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 : ScopedTestBase
  14. {
  15. [Fact]
  16. public async Task EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree_And_Layout_Pass_Has_Not_Run()
  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_When_Control_Added_To_Tree_And_Layout_Pass_Has_Run()
  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. Assert.Equal(0, raised);
  47. await ExecuteInitialLayoutPass(root);
  48. Assert.Equal(1, raised);
  49. });
  50. }
  51. [Fact]
  52. public async Task EffectiveViewportChanged_Raised_When_Root_LayedOut_And_Then_Control_Added_To_Tree_And_Layout_Pass_Runs()
  53. {
  54. await RunOnUIThread.Execute(async () =>
  55. {
  56. var root = CreateRoot();
  57. var target = new Canvas();
  58. var raised = 0;
  59. target.EffectiveViewportChanged += (s, e) =>
  60. {
  61. ++raised;
  62. };
  63. await ExecuteInitialLayoutPass(root);
  64. root.Child = target;
  65. Assert.Equal(0, raised);
  66. await ExecuteInitialLayoutPass(root);
  67. Assert.Equal(1, raised);
  68. });
  69. }
  70. [Fact]
  71. public async Task EffectiveViewportChanged_Raised_Before_LayoutUpdated()
  72. {
  73. await RunOnUIThread.Execute(async () =>
  74. {
  75. var root = CreateRoot();
  76. var target = new Canvas();
  77. var raised = 0;
  78. target.EffectiveViewportChanged += (s, e) =>
  79. {
  80. ++raised;
  81. };
  82. root.Child = target;
  83. await ExecuteInitialLayoutPass(root);
  84. Assert.Equal(1, raised);
  85. });
  86. }
  87. [Fact]
  88. public async Task EffectiveViewportChanged_Should_Not_Be_Raised_Twice_If_Subcribed_In_AttachedToVisualTree()
  89. {
  90. await RunOnUIThread.Execute(async () =>
  91. {
  92. var root = CreateRoot();
  93. var target = new Canvas();
  94. var raised = 0;
  95. target.AttachedToVisualTree += (_, _) =>
  96. {
  97. target.EffectiveViewportChanged += (_, _) => ++raised;
  98. };
  99. root.Child = target;
  100. await ExecuteInitialLayoutPass(root);
  101. Assert.Equal(1, raised);
  102. });
  103. }
  104. [Fact]
  105. public async Task Parent_Affects_EffectiveViewport()
  106. {
  107. await RunOnUIThread.Execute(async () =>
  108. {
  109. var root = CreateRoot();
  110. var target = new Canvas { Width = 100, Height = 100 };
  111. var parent = new Border { Width = 200, Height = 200, Child = target };
  112. var raised = 0;
  113. root.Child = parent;
  114. target.EffectiveViewportChanged += (s, e) =>
  115. {
  116. Assert.Equal(new Rect(-550, -400, 1200, 900), e.EffectiveViewport);
  117. ++raised;
  118. };
  119. await ExecuteInitialLayoutPass(root);
  120. });
  121. }
  122. [Fact]
  123. public async Task Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated_Raised()
  124. {
  125. await RunOnUIThread.Execute(async () =>
  126. {
  127. var root = CreateRoot();
  128. var target = new TestCanvas();
  129. var raised = 0;
  130. var layoutUpdatedRaised = 0;
  131. root.LayoutUpdated += (s, e) =>
  132. {
  133. Assert.Equal(2, target.MeasureCount);
  134. Assert.Equal(2, target.ArrangeCount);
  135. ++layoutUpdatedRaised;
  136. };
  137. target.EffectiveViewportChanged += (s, e) =>
  138. {
  139. target.InvalidateMeasure();
  140. ++raised;
  141. };
  142. root.Child = target;
  143. await ExecuteInitialLayoutPass(root);
  144. Assert.Equal(1, raised);
  145. Assert.Equal(1, layoutUpdatedRaised);
  146. });
  147. }
  148. [Fact]
  149. public async Task Viewport_Extends_Beyond_Centered_Control()
  150. {
  151. await RunOnUIThread.Execute(async () =>
  152. {
  153. var root = CreateRoot();
  154. var target = new Canvas { Width = 52, Height = 52, };
  155. var raised = 0;
  156. target.EffectiveViewportChanged += (s, e) =>
  157. {
  158. Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport);
  159. ++raised;
  160. };
  161. root.Child = target;
  162. await ExecuteInitialLayoutPass(root);
  163. Assert.Equal(1, raised);
  164. });
  165. }
  166. [Fact]
  167. public async Task Viewport_Extends_Beyond_Nested_Centered_Control()
  168. {
  169. await RunOnUIThread.Execute(async () =>
  170. {
  171. var root = CreateRoot();
  172. var target = new Canvas { Width = 52, Height = 52 };
  173. var parent = new Border { Width = 100, Height = 100, Child = target };
  174. var raised = 0;
  175. target.EffectiveViewportChanged += (s, e) =>
  176. {
  177. Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport);
  178. ++raised;
  179. };
  180. root.Child = parent;
  181. await ExecuteInitialLayoutPass(root);
  182. Assert.Equal(1, raised);
  183. });
  184. }
  185. [Fact]
  186. public async Task ScrollViewer_Determines_EffectiveViewport()
  187. {
  188. await RunOnUIThread.Execute(async () =>
  189. {
  190. var root = CreateRoot();
  191. var target = new Canvas { Width = 200, Height = 200 };
  192. var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden };
  193. var raised = 0;
  194. target.EffectiveViewportChanged += (s, e) =>
  195. {
  196. Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport);
  197. ++raised;
  198. };
  199. root.Child = scroller;
  200. await ExecuteInitialLayoutPass(root);
  201. Assert.Equal(1, raised);
  202. });
  203. }
  204. [Fact]
  205. public async Task Scrolled_ScrollViewer_Determines_EffectiveViewport()
  206. {
  207. using var scope = AvaloniaLocator.EnterScope();
  208. await RunOnUIThread.Execute(async () =>
  209. {
  210. var root = CreateRoot();
  211. var target = new Canvas { Width = 200, Height = 200 };
  212. var scroller = new ScrollViewer { Width = 100, Height = 100, Content = target, Template = ScrollViewerTemplate(), HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden };
  213. var raised = 0;
  214. root.Child = scroller;
  215. await ExecuteInitialLayoutPass(root);
  216. scroller.Offset = new Vector(0, 10);
  217. await ExecuteScrollerLayoutPass(root, scroller, target, (s, e) =>
  218. {
  219. Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport);
  220. ++raised;
  221. });
  222. Assert.Equal(1, raised);
  223. });
  224. }
  225. [Fact]
  226. public async Task Moving_Parent_Updates_EffectiveViewport()
  227. {
  228. using var scope = AvaloniaLocator.EnterScope();
  229. await RunOnUIThread.Execute(async () =>
  230. {
  231. var root = CreateRoot();
  232. var target = new Canvas { Width = 100, Height = 100 };
  233. var parent = new Border { Width = 200, Height = 200, Child = target };
  234. var raised = 0;
  235. root.Child = parent;
  236. await ExecuteInitialLayoutPass(root);
  237. target.EffectiveViewportChanged += (s, e) =>
  238. {
  239. Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport);
  240. ++raised;
  241. };
  242. parent.Margin = new Thickness(8, 0, 0, 0);
  243. await ExecuteLayoutPass(root);
  244. Assert.Equal(1, raised);
  245. });
  246. }
  247. [Fact]
  248. public async Task Translate_Transform_Doesnt_Affect_EffectiveViewport()
  249. {
  250. using var scope = AvaloniaLocator.EnterScope();
  251. await RunOnUIThread.Execute(async () =>
  252. {
  253. var root = CreateRoot();
  254. var target = new Canvas { Width = 100, Height = 100 };
  255. var parent = new Border { Width = 200, Height = 200, Child = target };
  256. var raised = 0;
  257. root.Child = parent;
  258. target.EffectiveViewportChanged += (s, e) => ++raised;
  259. await ExecuteInitialLayoutPass(root);
  260. raised = 0; // The initial layout pass is expected to raise.
  261. target.RenderTransform = new TranslateTransform { X = 8 };
  262. target.InvalidateMeasure();
  263. await ExecuteLayoutPass(root);
  264. Assert.Equal(0, raised);
  265. });
  266. }
  267. [Fact]
  268. public async Task Translate_Transform_On_Parent_Affects_EffectiveViewport()
  269. {
  270. using var scope = AvaloniaLocator.EnterScope();
  271. await RunOnUIThread.Execute(async () =>
  272. {
  273. var root = CreateRoot();
  274. var target = new Canvas { Width = 100, Height = 100 };
  275. var parent = new Border { Width = 200, Height = 200, Child = target };
  276. var raised = 0;
  277. root.Child = parent;
  278. await ExecuteInitialLayoutPass(root);
  279. target.EffectiveViewportChanged += (s, e) =>
  280. {
  281. Assert.Equal(new Rect(-558, -400, 1200, 900), e.EffectiveViewport);
  282. ++raised;
  283. };
  284. // Change the parent render transform to move it. A layout is then needed before
  285. // EffectiveViewportChanged is raised.
  286. parent.RenderTransform = new TranslateTransform { X = 8 };
  287. parent.InvalidateMeasure();
  288. await ExecuteLayoutPass(root);
  289. Assert.Equal(1, raised);
  290. });
  291. }
  292. [Fact]
  293. public async Task Rotate_Transform_On_Parent_Affects_EffectiveViewport()
  294. {
  295. using var scope = AvaloniaLocator.EnterScope();
  296. await RunOnUIThread.Execute(async () =>
  297. {
  298. var root = CreateRoot();
  299. var target = new Canvas { Width = 100, Height = 100 };
  300. var parent = new Border { Width = 200, Height = 200, Child = target };
  301. var raised = 0;
  302. root.Child = parent;
  303. await ExecuteInitialLayoutPass(root);
  304. target.EffectiveViewportChanged += (s, e) =>
  305. {
  306. AssertArePixelEqual(new Rect(-651, -792, 1484, 1484), e.EffectiveViewport);
  307. ++raised;
  308. };
  309. parent.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute);
  310. parent.RenderTransform = new RotateTransform { Angle = 45 };
  311. parent.InvalidateMeasure();
  312. await ExecuteLayoutPass(root);
  313. Assert.Equal(1, raised);
  314. });
  315. }
  316. [Fact]
  317. public async Task Event_Unsubscribed_While_Inside_Callback()
  318. {
  319. await RunOnUIThread.Execute(async () =>
  320. {
  321. var root = CreateRoot();
  322. var target = new Canvas();
  323. var raised = 0;
  324. void OnTargetOnEffectiveViewportChanged(object s, EffectiveViewportChangedEventArgs e)
  325. {
  326. target.EffectiveViewportChanged -= OnTargetOnEffectiveViewportChanged;
  327. ++raised;
  328. }
  329. target.EffectiveViewportChanged += OnTargetOnEffectiveViewportChanged;
  330. root.Child = target;
  331. await ExecuteInitialLayoutPass(root);
  332. Assert.Equal(1, raised);
  333. });
  334. }
  335. // https://github.com/AvaloniaUI/Avalonia/issues/12452
  336. [Fact]
  337. public async Task Zero_ScaleTransform_Sets_Empty_EffectiveViewport()
  338. {
  339. await RunOnUIThread.Execute(async () =>
  340. {
  341. var effectiveViewport = new Rect(Size.Infinity);
  342. var root = CreateRoot();
  343. var target = new Canvas { Width = 100, Height = 100 };
  344. var parent = new Border { Width = 100, Height = 100, Child = target };
  345. target.EffectiveViewportChanged += (_, e) => effectiveViewport = e.EffectiveViewport;
  346. root.Child = parent;
  347. await ExecuteInitialLayoutPass(root);
  348. parent.RenderTransform = new ScaleTransform(0, 0);
  349. await ExecuteLayoutPass(root);
  350. Assert.Equal(new Rect(0, 0, 0, 0), effectiveViewport);
  351. });
  352. }
  353. private static TestRoot CreateRoot() => new TestRoot { Width = 1200, Height = 900 };
  354. private static Task ExecuteInitialLayoutPass(TestRoot root)
  355. {
  356. root.LayoutManager.ExecuteInitialLayoutPass();
  357. return Task.CompletedTask;
  358. }
  359. private static Task ExecuteLayoutPass(TestRoot root)
  360. {
  361. root.LayoutManager.ExecuteLayoutPass();
  362. return Task.CompletedTask;
  363. }
  364. private static Task ExecuteScrollerLayoutPass(
  365. TestRoot root,
  366. ScrollViewer scroller,
  367. Control target,
  368. Action<object, EffectiveViewportChangedEventArgs> handler)
  369. {
  370. void ViewportChanged(object sender, EffectiveViewportChangedEventArgs e)
  371. {
  372. handler(sender, e);
  373. }
  374. target.EffectiveViewportChanged += ViewportChanged;
  375. root.LayoutManager.ExecuteLayoutPass();
  376. return Task.CompletedTask;
  377. }
  378. private static IControlTemplate ScrollViewerTemplate()
  379. {
  380. return new FuncControlTemplate<ScrollViewer>((control, scope) => new Grid
  381. {
  382. ColumnDefinitions = new ColumnDefinitions
  383. {
  384. new ColumnDefinition(1, GridUnitType.Star),
  385. new ColumnDefinition(GridLength.Auto),
  386. },
  387. RowDefinitions = new RowDefinitions
  388. {
  389. new RowDefinition(1, GridUnitType.Star),
  390. new RowDefinition(GridLength.Auto),
  391. },
  392. Children =
  393. {
  394. new ScrollContentPresenter
  395. {
  396. Name = "PART_ContentPresenter",
  397. }.RegisterInNameScope(scope),
  398. new ScrollBar
  399. {
  400. Name = "horizontalScrollBar",
  401. Orientation = Orientation.Horizontal,
  402. [Grid.RowProperty] = 1,
  403. }.RegisterInNameScope(scope),
  404. new ScrollBar
  405. {
  406. Name = "verticalScrollBar",
  407. Orientation = Orientation.Vertical,
  408. [Grid.ColumnProperty] = 1,
  409. }.RegisterInNameScope(scope),
  410. },
  411. });
  412. }
  413. private static void AssertArePixelEqual(Rect expected, Rect actual)
  414. {
  415. var expectedRounded = new Rect((int)expected.X, (int)expected.Y, (int)expected.Width, (int)expected.Height);
  416. var actualRounded = new Rect((int)actual.X, (int)actual.Y, (int)actual.Width, (int)actual.Height);
  417. Assert.Equal(expectedRounded, actualRounded);
  418. }
  419. private class TestCanvas : Canvas
  420. {
  421. public int MeasureCount { get; private set; }
  422. public int ArrangeCount { get; private set; }
  423. protected override Size MeasureOverride(Size availableSize)
  424. {
  425. ++MeasureCount;
  426. return base.MeasureOverride(availableSize);
  427. }
  428. protected override Size ArrangeOverride(Size finalSize)
  429. {
  430. ++ArrangeCount;
  431. return base.ArrangeOverride(finalSize);
  432. }
  433. }
  434. private static class RunOnUIThread
  435. {
  436. public static async Task Execute(Func<Task> func)
  437. {
  438. await func();
  439. }
  440. }
  441. }
  442. }