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 System.Linq;
6using NUnit.Framework;
7using osu.Framework.Extensions;
8using osu.Framework.Graphics;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Cursor;
11using osu.Framework.Graphics.Shapes;
12using osu.Framework.Graphics.Sprites;
13using osu.Framework.Graphics.UserInterface;
14using osu.Framework.Input.Events;
15using osu.Framework.Testing;
16using osuTK;
17using osuTK.Graphics;
18using osuTK.Input;
19
20namespace osu.Framework.Tests.Visual.UserInterface
21{
22 public class TestScenePopoverContainer : ManualInputManagerTestScene
23 {
24 private Container[,] cells;
25 private Container popoverWrapper;
26 private PopoverContainer popoverContainer;
27 private GridContainer gridContainer;
28
29 [SetUpSteps]
30 public void SetUpSteps()
31 {
32 AddStep("create popover container", () =>
33 {
34 Child = popoverWrapper = new Container
35 {
36 RelativeSizeAxes = Axes.Both,
37 Anchor = Anchor.Centre,
38 Origin = Anchor.Centre,
39 Masking = true,
40 BorderThickness = 5,
41 BorderColour = Colour4.White,
42 Children = new Drawable[]
43 {
44 new Box
45 {
46 RelativeSizeAxes = Axes.Both,
47 AlwaysPresent = true,
48 Colour = Colour4.Transparent
49 },
50 popoverContainer = new PopoverContainer
51 {
52 RelativeSizeAxes = Axes.Both,
53 Padding = new MarginPadding(5),
54 Children = new Drawable[]
55 {
56 new ClickableContainer
57 {
58 RelativeSizeAxes = Axes.Both,
59 Size = new Vector2(0.5f),
60 Children = new Drawable[]
61 {
62 new Box
63 {
64 Colour = Color4.Blue,
65 RelativeSizeAxes = Axes.Both,
66 },
67 new TextFlowContainer
68 {
69 AutoSizeAxes = Axes.X,
70 TextAnchor = Anchor.TopCentre,
71 Anchor = Anchor.Centre,
72 Origin = Anchor.Centre,
73 Text = "click blocking container between\nPopover creator and PopoverContainer"
74 }
75 }
76 },
77 gridContainer = new GridContainer
78 {
79 RelativeSizeAxes = Axes.Both
80 }
81 }
82 }
83 }
84 };
85
86 cells = new Container[3, 3];
87
88 for (int r = 0; r < 3; r++)
89 {
90 for (int c = 0; c < 3; c++)
91 cells[r, c] = new Container { RelativeSizeAxes = Axes.Both };
92 }
93
94 gridContainer.Content = cells.ToJagged();
95 });
96 }
97
98 [Test]
99 public void TestShowHide()
100 {
101 createContent(button => new BasicPopover
102 {
103 Child = new SpriteText
104 {
105 Text = $"{button.Anchor} popover"
106 }
107 });
108
109 AddStep("click button", () =>
110 {
111 InputManager.MoveMouseTo(this.ChildrenOfType<DrawableWithPopover>().First());
112 InputManager.Click(MouseButton.Left);
113 });
114 AddAssert("popover shown", () => this.ChildrenOfType<Popover>().Any(popover => popover.State.Value == Visibility.Visible));
115
116 AddStep("click popover", () =>
117 {
118 InputManager.MoveMouseTo(this.ChildrenOfType<Popover>().Single().Body);
119 InputManager.Click(MouseButton.Left);
120 });
121 AddAssert("popover still visible", () => this.ChildrenOfType<Popover>().Single().State.Value == Visibility.Visible);
122
123 AddStep("click away", () =>
124 {
125 InputManager.MoveMouseTo(this.ChildrenOfType<DrawableWithPopover>().First().ScreenSpaceDrawQuad.BottomRight + new Vector2(10));
126 InputManager.Click(MouseButton.Left);
127 });
128 AddAssert("all hidden", () => this.ChildrenOfType<Popover>().All(popover => popover.State.Value != Visibility.Visible));
129 }
130
131 [Test]
132 public void TestHideViaKeyboard()
133 {
134 createContent(button => new BasicPopover
135 {
136 Child = new SpriteText
137 {
138 Text = $"{button.Anchor} popover"
139 }
140 });
141
142 AddStep("click button", () =>
143 {
144 InputManager.MoveMouseTo(this.ChildrenOfType<DrawableWithPopover>().First());
145 InputManager.Click(MouseButton.Left);
146 });
147 AddAssert("popover shown", () => this.ChildrenOfType<Popover>().Any(popover => popover.State.Value == Visibility.Visible));
148
149 AddStep("press Escape", () => InputManager.Key(Key.Escape));
150 AddAssert("all hidden", () => this.ChildrenOfType<Popover>().All(popover => popover.State.Value != Visibility.Visible));
151 }
152
153 [Test]
154 public void TestShowHideViaExtensionMethod()
155 {
156 createContent(button => new BasicPopover
157 {
158 Child = new SpriteText
159 {
160 Text = $"{button.Anchor} popover"
161 }
162 });
163
164 AddStep("show popover manually", () => this.ChildrenOfType<DrawableWithPopover>().First().ShowPopover());
165 AddAssert("popover shown", () => this.ChildrenOfType<Popover>().Any(popover => popover.State.Value == Visibility.Visible));
166
167 AddStep("hide popover manually", () => popoverContainer.HidePopover());
168 AddAssert("all hidden", () => this.ChildrenOfType<Popover>().All(popover => popover.State.Value != Visibility.Visible));
169 }
170
171 [Test]
172 public void TestClickBetweenMultiple()
173 {
174 createContent(button => new BasicPopover
175 {
176 Name = button.Anchor.ToString(),
177 Child = new SpriteText
178 {
179 Text = $"{button.Anchor} popover"
180 }
181 });
182
183 AddStep("click button", () =>
184 {
185 InputManager.MoveMouseTo(this.ChildrenOfType<DrawableWithPopover>().First());
186 InputManager.Click(MouseButton.Left);
187 });
188
189 AddAssert("first shown", () => this.ChildrenOfType<Popover>().Single().Name == Anchor.TopLeft.ToString());
190
191 AddStep("click last button", () =>
192 {
193 InputManager.MoveMouseTo(this.ChildrenOfType<DrawableWithPopover>().Last());
194 InputManager.Click(MouseButton.Left);
195 });
196
197 AddAssert("last shown", () => this.ChildrenOfType<Popover>().Single().Name == Anchor.BottomRight.ToString());
198 }
199
200 [Test]
201 public void TestDragAwayDoesntHide()
202 {
203 createContent(button => new BasicPopover
204 {
205 Child = new SpriteText
206 {
207 Text = $"{button.Anchor} popover"
208 }
209 });
210
211 AddStep("click button", () =>
212 {
213 InputManager.MoveMouseTo(this.ChildrenOfType<DrawableWithPopover>().First());
214 InputManager.Click(MouseButton.Left);
215 });
216
217 AddAssert("popover shown", () => this.ChildrenOfType<Popover>().Any(popover => popover.State.Value == Visibility.Visible));
218
219 AddStep("mousedown popover", () =>
220 {
221 InputManager.MoveMouseTo(this.ChildrenOfType<Popover>().Single().Body);
222 InputManager.PressButton(MouseButton.Left);
223 });
224 AddAssert("popover still visible", () => this.ChildrenOfType<Popover>().Single().State.Value == Visibility.Visible);
225
226 AddStep("move away", () => InputManager.MoveMouseTo(this.ChildrenOfType<DrawableWithPopover>().Last()));
227
228 AddStep("release button", () => InputManager.ReleaseButton(MouseButton.Left));
229
230 AddAssert("popover remains", () => this.ChildrenOfType<Popover>().Any(popover => popover.State.Value == Visibility.Visible));
231 }
232
233 [Test]
234 public void TestInteractiveContent()
235 {
236 createContent(button =>
237 {
238 TextBox textBox;
239
240 return new AnimatedPopover
241 {
242 Child = new FillFlowContainer
243 {
244 Direction = FillDirection.Vertical,
245 Width = 200,
246 AutoSizeAxes = Axes.Y,
247 Spacing = new Vector2(5),
248 Children = new Drawable[]
249 {
250 textBox = new BasicTextBox
251 {
252 PlaceholderText = $"{button.Anchor} text box",
253 Height = 30,
254 RelativeSizeAxes = Axes.X
255 },
256 new BasicButton
257 {
258 RelativeSizeAxes = Axes.X,
259 Height = 30,
260 Text = "Clear",
261 Action = () => textBox.Text = string.Empty
262 }
263 }
264 }
265 };
266 });
267
268 AddStep("click button", () =>
269 {
270 InputManager.MoveMouseTo(this.ChildrenOfType<DrawableWithPopover>().First());
271 InputManager.Click(MouseButton.Left);
272 });
273
274 AddAssert("popover shown", () => this.ChildrenOfType<Popover>().Any(popover => popover.State.Value == Visibility.Visible));
275
276 AddStep("click textbox", () =>
277 {
278 InputManager.MoveMouseTo(this.ChildrenOfType<TextBox>().First());
279 InputManager.Click(MouseButton.Left);
280 });
281
282 AddAssert("textbox is focused", () => InputManager.FocusedDrawable is TextBox);
283 AddAssert("popover still shown", () => this.ChildrenOfType<Popover>().Any(popover => popover.State.Value == Visibility.Visible));
284 AddStep("click in popover", () =>
285 {
286 InputManager.MoveMouseTo(this.ChildrenOfType<Popover>().First().Body.ScreenSpaceDrawQuad.TopLeft + Vector2.One);
287 InputManager.Click(MouseButton.Left);
288 });
289
290 AddAssert("popover is focused", () => InputManager.FocusedDrawable is Popover);
291 AddAssert("popover still shown", () => this.ChildrenOfType<Popover>().Any(popover => popover.State.Value == Visibility.Visible));
292 }
293
294 [Test]
295 public void TestAutomaticLayouting()
296 {
297 DrawableWithPopover target = null;
298
299 AddStep("add button", () => popoverContainer.Child = target = new DrawableWithPopover
300 {
301 Width = 200,
302 Height = 30,
303 RelativePositionAxes = Axes.Both,
304 Text = "open",
305 CreateContent = _ => new BasicPopover
306 {
307 Child = new SpriteText
308 {
309 Text = "This popover follows its associated UI component",
310 Size = new Vector2(400)
311 }
312 }
313 });
314
315 AddSliderStep("move X", 0f, 1, 0, x =>
316 {
317 if (target != null)
318 target.X = x;
319 });
320
321 AddSliderStep("move Y", 0f, 1, 0, y =>
322 {
323 if (target != null)
324 target.Y = y;
325 });
326
327 AddSliderStep("container width", 0f, 1, 1, width =>
328 {
329 if (popoverWrapper != null)
330 popoverWrapper.Width = width;
331 });
332
333 AddSliderStep("container height", 0f, 1, 1, height =>
334 {
335 if (popoverWrapper != null)
336 popoverWrapper.Height = height;
337 });
338 }
339
340 [Test]
341 public void TestAutoSize()
342 {
343 AddStep("create content", () =>
344 {
345 popoverWrapper.RelativeSizeAxes = popoverContainer.RelativeSizeAxes = Axes.X;
346 popoverWrapper.AutoSizeAxes = popoverContainer.AutoSizeAxes = Axes.Y;
347
348 popoverContainer.Child = new Container
349 {
350 RelativeSizeAxes = Axes.X,
351 Height = 200,
352 Child = new DrawableWithPopover
353 {
354 Width = 200,
355 Height = 30,
356 Text = "open",
357 CreateContent = _ => new BasicPopover
358 {
359 Child = new SpriteText
360 {
361 Text = "I'm in an auto-sized container!"
362 }
363 }
364 }
365 };
366 });
367
368 AddSliderStep("change content height", 100, 500, 200, height =>
369 {
370 if (popoverContainer?.Children.Count == 1)
371 popoverContainer.Child.Height = height;
372 });
373 }
374
375 [Test]
376 public void TestExternalPopoverControl()
377 {
378 TextBoxWithPopover target = null;
379
380 AddStep("create content", () =>
381 {
382 popoverContainer.Child = target = new TextBoxWithPopover
383 {
384 Width = 200,
385 Height = 30,
386 PlaceholderText = "focus to show popover"
387 };
388 });
389
390 AddStep("click text box", () =>
391 {
392 InputManager.MoveMouseTo(target);
393 InputManager.Click(MouseButton.Left);
394 });
395 AddAssert("popover shown", () => this.ChildrenOfType<Popover>().Any());
396
397 AddStep("take away text box focus", () => InputManager.ChangeFocus(null));
398 AddAssert("popover hidden", () => !this.ChildrenOfType<Popover>().Any());
399 }
400
401 [Test]
402 public void TestPopoverCleanupOnTargetDisposal()
403 {
404 DrawableWithPopover target = null;
405
406 AddStep("add button", () => popoverContainer.Child = target = new DrawableWithPopover
407 {
408 Width = 200,
409 Height = 30,
410 Anchor = Anchor.Centre,
411 Origin = Anchor.Centre,
412 Text = "open",
413 CreateContent = _ => new BasicPopover
414 {
415 Child = new SpriteText
416 {
417 Text = "This popover should be cleaned up when its button is removed",
418 }
419 }
420 });
421
422 AddStep("click button", () =>
423 {
424 InputManager.MoveMouseTo(target);
425 InputManager.Click(MouseButton.Left);
426 });
427 AddAssert("popover created", () => this.ChildrenOfType<Popover>().Any());
428
429 AddStep("dispose of button", () => popoverContainer.Clear());
430 AddUntilStep("no popover present", () => !this.ChildrenOfType<Popover>().Any());
431 }
432
433 [Test]
434 public void TestPopoverCleanupOnTargetHide()
435 {
436 DrawableWithPopover target = null;
437
438 AddStep("add button", () => popoverContainer.Child = target = new DrawableWithPopover
439 {
440 Width = 200,
441 Height = 30,
442 Anchor = Anchor.Centre,
443 Origin = Anchor.Centre,
444 Text = "open",
445 CreateContent = _ => new BasicPopover
446 {
447 Child = new SpriteText
448 {
449 Text = "This popover should be cleaned up when its button is hidden",
450 }
451 }
452 });
453
454 AddStep("click button", () =>
455 {
456 InputManager.MoveMouseTo(target);
457 InputManager.Click(MouseButton.Left);
458 });
459 AddAssert("popover created", () => this.ChildrenOfType<Popover>().Any());
460
461 AddStep("hide button", () => target.Hide());
462 AddUntilStep("no popover present", () => !this.ChildrenOfType<Popover>().Any());
463 }
464
465 [Test]
466 public void TestPopoverEventHandling()
467 {
468 EventHandlingContainer eventHandlingContainer = null;
469 DrawableWithPopover target = null;
470
471 AddStep("add button", () => popoverContainer.Child = eventHandlingContainer = new EventHandlingContainer
472 {
473 RelativeSizeAxes = Axes.Both,
474 Child = target = new DrawableWithPopover
475 {
476 Width = 200,
477 Height = 30,
478 Anchor = Anchor.Centre,
479 Origin = Anchor.Centre,
480 Text = "open",
481 CreateContent = _ => new BasicPopover
482 {
483 Child = new SpriteText
484 {
485 Text = "This popover should be handle hover and click events",
486 }
487 }
488 }
489 });
490
491 AddStep("click button", () =>
492 {
493 InputManager.MoveMouseTo(target);
494 InputManager.Click(MouseButton.Left);
495 });
496
497 AddAssert("container received hover", () => eventHandlingContainer.HoverReceived);
498
499 AddAssert("popover created", () => this.ChildrenOfType<Popover>().Any());
500
501 AddStep("mouse over popover", () =>
502 {
503 eventHandlingContainer.Reset();
504 InputManager.MoveMouseTo(this.ChildrenOfType<Popover>().Single().Body);
505 });
506
507 AddAssert("container did not receive hover", () => !eventHandlingContainer.HoverReceived);
508
509 AddStep("click on popover", () => InputManager.Click(MouseButton.Left));
510 AddAssert("container did not receive click", () => !eventHandlingContainer.ClickReceived);
511
512 AddStep("dismiss popover", () =>
513 {
514 InputManager.MoveMouseTo(eventHandlingContainer.ScreenSpaceDrawQuad.TopLeft + new Vector2(10));
515 InputManager.Click(MouseButton.Left);
516 });
517
518 AddAssert("container received hover", () => eventHandlingContainer.HoverReceived);
519 AddStep("click again", () => InputManager.Click(MouseButton.Left));
520 AddAssert("container received click", () => eventHandlingContainer.ClickReceived);
521 }
522
523 private void createContent(Func<DrawableWithPopover, Popover> creationFunc)
524 => AddStep("create content", () =>
525 {
526 for (int i = 0; i < 3; ++i)
527 {
528 for (int j = 0; j < 3; ++j)
529 {
530 Anchor popoverAnchor = 0;
531 popoverAnchor |= (Anchor)((int)Anchor.x0 << i);
532 popoverAnchor |= (Anchor)((int)Anchor.y0 << j);
533
534 cells[j, i].Child = new DrawableWithPopover
535 {
536 Width = 200,
537 Height = 30,
538 Text = $"open {popoverAnchor}",
539 Anchor = popoverAnchor,
540 Origin = popoverAnchor,
541 CreateContent = creationFunc
542 };
543 }
544 }
545 });
546
547 private class AnimatedPopover : BasicPopover
548 {
549 protected override void PopIn() => this.FadeIn(300, Easing.OutQuint);
550 protected override void PopOut() => this.FadeOut(300, Easing.OutQuint);
551 }
552
553 private class DrawableWithPopover : CircularContainer, IHasPopover
554 {
555 public Func<DrawableWithPopover, Popover> CreateContent { get; set; }
556
557 public string Text
558 {
559 set => spriteText.Text = value;
560 }
561
562 private readonly SpriteText spriteText;
563
564 public DrawableWithPopover()
565 {
566 Masking = true;
567 BorderThickness = 4;
568 BorderColour = FrameworkColour.YellowGreenDark;
569
570 Children = new Drawable[]
571 {
572 new Box
573 {
574 RelativeSizeAxes = Axes.Both,
575 Colour = FrameworkColour.GreenDark
576 },
577 spriteText = new SpriteText
578 {
579 Anchor = Anchor.Centre,
580 Origin = Anchor.Centre,
581 Font = FontUsage.Default.With(italics: true)
582 }
583 };
584 }
585
586 public Popover GetPopover() => CreateContent.Invoke(this);
587
588 protected override bool OnClick(ClickEvent e)
589 {
590 this.ShowPopover();
591 return true;
592 }
593 }
594
595 private class TextBoxWithPopover : BasicTextBox, IHasPopover
596 {
597 protected override void OnFocus(FocusEvent e)
598 {
599 base.OnFocus(e);
600 this.ShowPopover();
601 }
602
603 protected override void OnFocusLost(FocusLostEvent e)
604 {
605 base.OnFocusLost(e);
606 this.HidePopover();
607 }
608
609 public Popover GetPopover() => new BasicPopover
610 {
611 Child = new SpriteText
612 {
613 Text = "the text box has focus now!"
614 }
615 };
616 }
617
618 private class EventHandlingContainer : Container
619 {
620 private readonly Box colourBox;
621
622 public bool ClickReceived { get; private set; }
623 public bool HoverReceived { get; private set; }
624
625 protected override Container<Drawable> Content { get; }
626
627 public EventHandlingContainer()
628 {
629 AddInternal(new Container
630 {
631 RelativeSizeAxes = Axes.Both,
632 Children = new Drawable[]
633 {
634 colourBox = new Box
635 {
636 Colour = Color4.Black,
637 RelativeSizeAxes = Axes.Both,
638 },
639 Content = new Container { RelativeSizeAxes = Axes.Both },
640 }
641 });
642 }
643
644 public void Reset()
645 {
646 ClickReceived = HoverReceived = false;
647 colourBox.FadeColour(Color4.Black);
648 }
649
650 protected override bool OnClick(ClickEvent e)
651 {
652 ClickReceived = true;
653 colourBox.FlashColour(Color4.White, 200);
654 return true;
655 }
656
657 protected override bool OnHover(HoverEvent e)
658 {
659 HoverReceived = true;
660 colourBox.FadeColour(Color4.DarkSlateBlue, 200);
661 return true;
662 }
663
664 protected override void OnHoverLost(HoverLostEvent e)
665 {
666 colourBox.FadeColour(Color4.Black, 200);
667 base.OnHoverLost(e);
668 }
669 }
670 }
671}