A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using NUnit.Framework;
6using osu.Framework.Graphics;
7using osu.Framework.Graphics.Containers;
8using osu.Framework.Graphics.Shapes;
9using osu.Framework.Layout;
10using osu.Framework.Testing;
11using osu.Framework.Tests.Visual;
12using osu.Framework.Utils;
13using osuTK;
14
15namespace osu.Framework.Tests.Layout
16{
17 [HeadlessTest]
18 public class TestSceneContainerLayout : FrameworkTestScene
19 {
20 /// <summary>
21 /// Tests that auto-size is updated when a child becomes alive.
22 /// </summary>
23 [Test]
24 public void TestContainerAutoSizeUpdatesWhenChildBecomesAlive()
25 {
26 Box box = null;
27 Container parent = null;
28
29 AddStep("create test", () =>
30 {
31 Child = parent = new Container
32 {
33 RemoveCompletedTransforms = false,
34 AutoSizeAxes = Axes.Both,
35 Child = box = new Box
36 {
37 Size = new Vector2(200),
38 LifetimeStart = double.MaxValue
39 }
40 };
41 });
42
43 AddStep("make child alive", () => box.LifetimeStart = double.MinValue);
44
45 AddAssert("parent has size 200", () => Precision.AlmostEquals(new Vector2(200), parent.DrawSize));
46 }
47
48 /// <summary>
49 /// Tests that auto-size is updated when a child is removed through death.
50 /// </summary>
51 [Test]
52 public void TestContainerAutoSizeUpdatesWhenChildBecomesDead()
53 {
54 Box box = null;
55 Container parent = null;
56
57 AddStep("create test", () =>
58 {
59 Child = parent = new Container
60 {
61 AutoSizeAxes = Axes.Both,
62 Child = box = new Box { Size = new Vector2(200) }
63 };
64 });
65
66 AddStep("make child dead", () => box.Expire());
67
68 AddAssert("parent has size 0", () => Precision.AlmostEquals(Vector2.Zero, parent.DrawSize));
69 }
70
71 /// <summary>
72 /// Tests that auto-size is updated when a child becomes dead and doesn't get removed.
73 /// </summary>
74 [Test]
75 public void TestContainerAutoSizeUpdatesWhenChildBecomesDeadWithoutRemoval()
76 {
77 Box box = null;
78 Container parent = null;
79
80 AddStep("create test", () =>
81 {
82 Child = parent = new Container
83 {
84 RemoveCompletedTransforms = false,
85 AutoSizeAxes = Axes.Both,
86 Child = box = new TestBox1 { Size = new Vector2(200) }
87 };
88 });
89
90 AddStep("make child dead", () => box.Expire());
91
92 AddAssert("parent has size 0", () => Precision.AlmostEquals(Vector2.Zero, parent.DrawSize));
93 }
94
95 /// <summary>
96 /// Tests that auto-size properly captures a child's presence change.
97 /// </summary>
98 [Test]
99 public void TestAddHiddenChildAndFadeIn()
100 {
101 Container content = null;
102 Box child = null;
103
104 AddStep("create test", () =>
105 {
106 Child = content = new Container
107 {
108 RelativeSizeAxes = Axes.X,
109 AutoSizeAxes = Axes.Y,
110 };
111 });
112
113 AddStep("add child", () => LoadComponentAsync(child = new Box
114 {
115 RelativeSizeAxes = Axes.X,
116 Height = 500,
117 }, d =>
118 {
119 content.Add(d);
120 d.FadeInFromZero(50000);
121 }));
122
123 AddUntilStep("wait for child load", () => child.IsLoaded);
124
125 AddUntilStep("content height matches box height", () => Precision.AlmostEquals(content.DrawHeight, child.DrawHeight));
126 }
127
128 /// <summary>
129 /// Tests that a parent container is not re-auto-sized when a child's size changes along the bypassed axes.
130 /// </summary>
131 /// <param name="axes">The bypassed axes that are bypassed.</param>
132 [TestCase(Axes.X)]
133 [TestCase(Axes.Y)]
134 [TestCase(Axes.Both)]
135 public void TestParentNotInvalidatedByBypassedSize(Axes axes)
136 {
137 Box child = null;
138
139 bool autoSized = false;
140
141 AddStep("create test", () =>
142 {
143 Child = new Container
144 {
145 AutoSizeAxes = Axes.Both,
146 Child = child = new Box { BypassAutoSizeAxes = axes }
147 }.With(c => c.OnAutoSize += () => autoSized = true);
148 });
149
150 AddUntilStep("wait for autosize", () => autoSized);
151
152 AddStep("adjust child size", () =>
153 {
154 autoSized = false;
155
156 if (axes == Axes.Both)
157 child.Size = new Vector2(50);
158 else if (axes == Axes.X)
159 child.Width = 50;
160 else if (axes == Axes.Y)
161 child.Height = 50;
162 });
163
164 AddWaitStep("wait for autosize", 1);
165
166 AddAssert("not autosized", () => !autoSized);
167 }
168
169 /// <summary>
170 /// Tests that a parent container is not re-auto-sized when a child's position changes along the bypassed axes.
171 /// </summary>
172 /// <param name="axes">The bypassed axes that are bypassed.</param>
173 [TestCase(Axes.X)]
174 [TestCase(Axes.Y)]
175 [TestCase(Axes.Both)]
176 public void TestParentNotInvalidatedByBypassedPosition(Axes axes)
177 {
178 Box child = null;
179
180 bool autoSized = false;
181
182 AddStep("create test", () =>
183 {
184 Child = new Container
185 {
186 AutoSizeAxes = Axes.Both,
187 Child = child = new Box { BypassAutoSizeAxes = axes }
188 }.With(c => c.OnAutoSize += () => autoSized = true);
189 });
190
191 AddUntilStep("wait for autosize", () => autoSized);
192
193 AddStep("adjust child size", () =>
194 {
195 autoSized = false;
196
197 if (axes == Axes.Both)
198 child.Position = new Vector2(50);
199 else if (axes == Axes.X)
200 child.X = 50;
201 else if (axes == Axes.Y)
202 child.Y = 50;
203 });
204
205 AddWaitStep("wait for autosize", 1);
206
207 AddAssert("not autosized", () => !autoSized);
208 }
209
210 [TestCase(Axes.X)]
211 [TestCase(Axes.Y)]
212 [TestCase(Axes.Both)]
213 public void TestParentSizeNotInvalidatedWhenChildGeometryInvalidated(Axes axes)
214 {
215 Drawable child = null;
216
217 Invalidation invalidation = Invalidation.None;
218
219 AddStep("create test", () =>
220 {
221 Child = new TestContainer1
222 {
223 Child = child = new Box { Size = new Vector2(200) }
224 }.With(c => c.Invalidated += i => invalidation = i);
225 });
226
227 AddStep("move child", () =>
228 {
229 invalidation = Invalidation.None;
230
231 if (axes == Axes.Both)
232 child.Position = new Vector2(10);
233 else if (axes == Axes.X)
234 child.X = 10;
235 else if (axes == Axes.Y)
236 child.Y = 10;
237 });
238
239 AddAssert("parent only invalidated with geometry", () => invalidation == Invalidation.MiscGeometry);
240 }
241
242 [TestCase(Axes.X)]
243 [TestCase(Axes.Y)]
244 [TestCase(Axes.Both)]
245 public void TestParentGeometryNotInvalidatedWhenChildSizeInvalidated(Axes axes)
246 {
247 Drawable child = null;
248
249 Invalidation invalidation = Invalidation.None;
250
251 AddStep("create test", () =>
252 {
253 Child = new TestContainer1
254 {
255 Child = child = new Box { Size = new Vector2(200) }
256 }.With(c => c.Invalidated += i => invalidation = i);
257 });
258
259 AddStep("move child", () =>
260 {
261 invalidation = Invalidation.None;
262
263 if (axes == Axes.Both)
264 child.Size = new Vector2(10);
265 else if (axes == Axes.X)
266 child.Width = 10;
267 else if (axes == Axes.Y)
268 child.Height = 10;
269 });
270
271 AddAssert("parent only invalidated with size", () => invalidation == Invalidation.DrawSize);
272 }
273
274 /// <summary>
275 /// Tests that a child is not invalidated by its parent when not alive.
276 /// </summary>
277 [Test]
278 public void TestChildNotInvalidatedWhenNotAlive()
279 {
280 Container parent = null;
281 bool invalidated = false;
282
283 AddStep("create test", () =>
284 {
285 Drawable child;
286
287 Child = parent = new Container
288 {
289 Size = new Vector2(200),
290 Child = child = new Box
291 {
292 RelativeSizeAxes = Axes.Both,
293 LifetimeStart = double.MaxValue
294 }
295 };
296
297 // Trigger a validation of draw size.
298 Assert.That(child.DrawSize, Is.EqualTo(new Vector2(200)));
299
300 child.Invalidated += _ => invalidated = true;
301 });
302
303 AddStep("resize parent", () => parent.Size = new Vector2(400));
304 AddAssert("child not invalidated", () => !invalidated);
305 }
306
307 /// <summary>
308 /// Tests that a loaded child is invalidated when it becomes alive.
309 /// </summary>
310 [Test]
311 public void TestChildInvalidatedWhenMadeAlive()
312 {
313 Container parent = null;
314 Drawable child = null;
315 bool invalidated = false;
316
317 AddStep("create test", () =>
318 {
319 Child = parent = new Container
320 {
321 Size = new Vector2(200),
322 Child = child = new Box { RelativeSizeAxes = Axes.Both }
323 };
324 });
325
326 AddStep("make child dead", () =>
327 {
328 child.LifetimeStart = double.MaxValue;
329 child.Invalidated += _ => invalidated = true;
330 });
331
332 // See above: won't cause an invalidation
333 AddStep("resize parent", () => parent.Size = new Vector2(400));
334
335 AddStep("make child alive", () => child.LifetimeStart = double.MinValue);
336 AddAssert("child invalidated", () => invalidated);
337
338 // Final check to make sure that the correct invalidation occurred
339 AddAssert("child size matches parent", () => child.DrawSize == parent.Size);
340 }
341
342 /// <summary>
343 /// Tests that non-alive children always receive Parent invalidations.
344 /// </summary>
345 [Test]
346 public void TestNonAliveChildReceivesParentInvalidations()
347 {
348 Container parent = null;
349 bool invalidated = false;
350
351 AddStep("create test", () =>
352 {
353 Drawable child;
354
355 Child = parent = new Container
356 {
357 Size = new Vector2(200),
358 Child = child = new Box
359 {
360 RelativeSizeAxes = Axes.Both,
361 LifetimeStart = double.MaxValue
362 }
363 };
364
365 child.Invalidated += _ => invalidated = true;
366 });
367
368 AddStep("invalidate parent", () =>
369 {
370 invalidated = false;
371 parent.Invalidate(Invalidation.Parent);
372 });
373
374 AddAssert("child invalidated", () => invalidated);
375 }
376
377 /// <summary>
378 /// Tests that DrawNode invalidations never propagate.
379 /// </summary>
380 [Test]
381 public void TestDrawNodeInvalidationsNeverPropagate()
382 {
383 Container parent = null;
384 bool invalidated = false;
385
386 AddStep("create test", () =>
387 {
388 Drawable child;
389
390 Child = parent = new Container
391 {
392 Size = new Vector2(200),
393 Child = child = new Box { RelativeSizeAxes = Axes.Both }
394 };
395
396 child.Invalidated += _ => invalidated = true;
397 });
398
399 AddStep("invalidate parent", () =>
400 {
401 invalidated = false;
402 parent.Invalidate(Invalidation.DrawNode);
403 });
404
405 AddAssert("child not invalidated", () => !invalidated);
406 }
407
408 private class TestBox1 : Box
409 {
410 public override bool RemoveWhenNotAlive => false;
411 }
412
413 private class TestContainer1 : Container
414 {
415 public new Action<Invalidation> Invalidated;
416
417 protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
418 {
419 Invalidated?.Invoke(invalidation);
420 return base.OnInvalidate(invalidation, source);
421 }
422 }
423 }
424}