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.Graphics;
8using osu.Framework.Graphics.Containers;
9using osu.Framework.Graphics.Cursor;
10using osu.Framework.Graphics.Shapes;
11using osu.Framework.Graphics.Sprites;
12using osu.Framework.Graphics.UserInterface;
13using osu.Framework.Localisation;
14using osu.Framework.Testing;
15using osuTK;
16using osuTK.Graphics;
17
18namespace osu.Framework.Tests.Visual.UserInterface
19{
20 public class TestSceneTooltip : ManualInputManagerTestScene
21 {
22 private TestTooltipContainer tooltipContainer;
23
24 private TooltipSpriteText tooltipText;
25 private TooltipSpriteText instantTooltipText;
26 private CustomTooltipSpriteText customTooltipTextA;
27 private CustomTooltipSpriteText customTooltipTextB;
28 private CustomTooltipSpriteTextAlt customTooltipTextAlt;
29 private TooltipSpriteText emptyTooltipText;
30 private TooltipSpriteText nullTooltipText;
31 private TooltipTextBox tooltipTextBox;
32
33 [SetUpSteps]
34 public void SetUpSteps()
35 {
36 AddToggleStep("add tooltips (cursor/cursor-less)", generateTest);
37 }
38
39 [Test]
40 public void TestTooltip()
41 {
42 ITooltip originalInstance = null;
43
44 hoverTooltipProvider(() => tooltipText);
45
46 AddStep("get tooltip instance", () => originalInstance = tooltipContainer.CurrentTooltip);
47 assertTooltipText(() => tooltipText.TooltipText);
48
49 hoverTooltipProvider(() => tooltipText);
50
51 AddAssert("tooltip reused", () => tooltipContainer.CurrentTooltip == originalInstance);
52 assertTooltipText(() => tooltipText.TooltipText);
53 }
54
55 [Test]
56 public void TestInstantTooltip()
57 {
58 hoverTooltipProvider(() => instantTooltipText, false);
59 assertTooltipText(() => instantTooltipText.TooltipText);
60 }
61
62 [Test]
63 public void TestCustomTooltip()
64 {
65 ITooltip originalInstance = null;
66
67 hoverTooltipProvider(() => customTooltipTextA);
68
69 AddStep("get tooltip instance", () => originalInstance = tooltipContainer.CurrentTooltip);
70 AddAssert("custom tooltip used", () => originalInstance.GetType() == typeof(CustomTooltip));
71 assertTooltipText(() => ((CustomContent)customTooltipTextA.TooltipContent).Text);
72
73 hoverTooltipProvider(() => customTooltipTextB);
74
75 AddAssert("custom tooltip reused", () => tooltipContainer.CurrentTooltip == originalInstance);
76 assertTooltipText(() => ((CustomContent)customTooltipTextB.TooltipContent).Text);
77 }
78
79 [Test]
80 public void TestDifferentCustomTooltips()
81 {
82 hoverTooltipProvider(() => customTooltipTextA);
83 assertTooltipText(() => ((CustomContent)customTooltipTextA.TooltipContent).Text);
84
85 AddAssert("current tooltip type normal", () => tooltipContainer.CurrentTooltip.GetType() == typeof(CustomTooltip));
86
87 hoverTooltipProvider(() => customTooltipTextAlt);
88 assertTooltipText(() => ((CustomContent)customTooltipTextAlt.TooltipContent).Text);
89
90 AddAssert("current tooltip type alt", () => tooltipContainer.CurrentTooltip.GetType() == typeof(CustomTooltipAlt));
91
92 hoverTooltipProvider(() => customTooltipTextB);
93 assertTooltipText(() => ((CustomContent)customTooltipTextB.TooltipContent).Text);
94
95 AddAssert("current tooltip type normal", () => tooltipContainer.CurrentTooltip.GetType() == typeof(CustomTooltip));
96 }
97
98 [Test]
99 public void TestEmptyTooltip()
100 {
101 AddStep("hover empty tooltip", () => InputManager.MoveMouseTo(emptyTooltipText));
102 AddAssert("tooltip not shown", () => tooltipContainer.CurrentTooltip?.IsPresent != true);
103 }
104
105 [Test]
106 public void TestNullTooltip()
107 {
108 AddStep("hover null tooltip", () => InputManager.MoveMouseTo(nullTooltipText));
109 AddAssert("tooltip not shown", () => tooltipContainer.CurrentTooltip?.IsPresent != true);
110 }
111
112 [Test]
113 public void TestUpdatingTooltip()
114 {
115 hoverTooltipProvider(() => tooltipTextBox);
116 assertTooltipText(() => tooltipTextBox.Text);
117
118 AddStep("update text", () => tooltipTextBox.Text = "updated!");
119
120 assertTooltipText(() => "updated!");
121 }
122
123 private void hoverTooltipProvider(Func<Drawable> getProvider, bool waitForDisplay = true)
124 {
125 AddStep("hover away from tooltips", () => InputManager.MoveMouseTo(Vector2.Zero));
126 AddAssert("tooltip hidden", () => tooltipContainer.CurrentTooltip?.IsPresent != true);
127
128 AddStep("hover tooltip", () => InputManager.MoveMouseTo(getProvider()));
129
130 if (waitForDisplay)
131 AddUntilStep("wait for tooltip", () => tooltipContainer.CurrentTooltip?.IsPresent == true);
132 else
133 AddAssert("tooltip instantly displayed", () => tooltipContainer.CurrentTooltip?.IsPresent == true);
134 }
135
136 private void assertTooltipText(Func<LocalisableString> expected)
137 {
138 AddAssert("tooltip text matching", () =>
139 {
140 var drawableTooltip = (Drawable)tooltipContainer.CurrentTooltip;
141 return drawableTooltip.ChildrenOfType<IHasText>().Count(t => t.Text == expected()) == 1;
142 });
143 }
144
145 private TooltipBox makeBox(Anchor anchor) => new TooltipBox
146 {
147 RelativeSizeAxes = Axes.Both,
148 Size = new Vector2(0.2f),
149 Anchor = anchor,
150 Origin = anchor,
151 Colour = Color4.Blue,
152 TooltipText = $"{anchor}",
153 };
154
155 private void generateTest(bool cursorlessTooltip)
156 {
157 Clear();
158
159 CursorContainer cursor = null;
160
161 if (!cursorlessTooltip)
162 {
163 cursor = new RectangleCursorContainer();
164 Add(cursor);
165 }
166
167 Add(tooltipContainer = new TestTooltipContainer(cursor)
168 {
169 RelativeSizeAxes = Axes.Both,
170 Children = new Drawable[]
171 {
172 new Container
173 {
174 Anchor = Anchor.Centre,
175 Origin = Anchor.Centre,
176 AutoSizeAxes = Axes.Both,
177 Children = new[]
178 {
179 new TooltipBox
180 {
181 TooltipText = "Outer Tooltip",
182 Colour = Color4.CornflowerBlue,
183 Size = new Vector2(300, 300),
184 Anchor = Anchor.Centre,
185 Origin = Anchor.Centre
186 },
187 new TooltipBox
188 {
189 TooltipText = "Inner Tooltip",
190 Size = new Vector2(150, 150),
191 Anchor = Anchor.Centre,
192 Origin = Anchor.Centre
193 },
194 }
195 },
196 new FillFlowContainer
197 {
198 RelativeSizeAxes = Axes.Both,
199 Direction = FillDirection.Vertical,
200 Spacing = new Vector2(0, 10),
201 Children = new Drawable[]
202 {
203 tooltipText = new TooltipSpriteText("this text has a tooltip!"),
204 instantTooltipText = new InstantTooltipSpriteText("this text has an instant tooltip!"),
205 customTooltipTextA = new CustomTooltipSpriteText("this one is custom!"),
206 customTooltipTextB = new CustomTooltipSpriteText("this one is also!"),
207 customTooltipTextAlt = new CustomTooltipSpriteTextAlt("but this one is different."),
208 emptyTooltipText = new InstantTooltipSpriteText("this text has an empty tooltip!", string.Empty),
209 nullTooltipText = new InstantTooltipSpriteText("this text has a nulled tooltip!", null),
210 tooltipTextBox = new TooltipTextBox
211 {
212 Text = "with real time updates!",
213 Size = new Vector2(400, 30),
214 },
215 new TooltipContainer
216 {
217 AutoSizeAxes = Axes.Both,
218 Child = new TooltipSpriteText("Nested tooltip; uses no cursor in all cases!"),
219 },
220 new TooltipTooltipContainer("This tooltip container has a tooltip itself!")
221 {
222 AutoSizeAxes = Axes.Both,
223 Child = new Container
224 {
225 AutoSizeAxes = Axes.Both,
226 Child = new TooltipSpriteText("Nested tooltip; uses no cursor in all cases; parent TooltipContainer has a tooltip"),
227 }
228 },
229 new Container
230 {
231 Child = new FillFlowContainer
232 {
233 Direction = FillDirection.Vertical,
234 Spacing = new Vector2(0, 8),
235 Children = new[]
236 {
237 new Container
238 {
239 Child = new Container
240 {
241 Child = new TooltipSpriteText("Tooltip within containers with zero size; i.e. parent is never hovered."),
242 }
243 },
244 new Container
245 {
246 Child = new TooltipSpriteText("Other tooltip within containers with zero size; different nesting; overlap."),
247 }
248 }
249 }
250 }
251 },
252 }
253 }
254 });
255
256 tooltipContainer.Add(makeBox(Anchor.BottomLeft));
257 tooltipContainer.Add(makeBox(Anchor.TopRight));
258 tooltipContainer.Add(makeBox(Anchor.BottomRight));
259 }
260
261 private class TestTooltipContainer : TooltipContainer
262 {
263 public new ITooltip CurrentTooltip => base.CurrentTooltip;
264
265 public TestTooltipContainer(CursorContainer cursor)
266 : base(cursor)
267 {
268 }
269 }
270
271 private class CustomTooltipSpriteText : Container, IHasCustomTooltip
272 {
273 public object TooltipContent { get; }
274
275 public CustomTooltipSpriteText(string displayedContent, string tooltipContent = null)
276 {
277 TooltipContent = new CustomContent(tooltipContent ?? displayedContent);
278
279 AutoSizeAxes = Axes.Both;
280 Children = new[]
281 {
282 new SpriteText
283 {
284 Text = displayedContent,
285 }
286 };
287 }
288
289 public virtual ITooltip GetCustomTooltip() => new CustomTooltip();
290 }
291
292 private class CustomTooltipSpriteTextAlt : CustomTooltipSpriteText
293 {
294 public CustomTooltipSpriteTextAlt(string displayedContent, string tooltipContent = null)
295 : base(displayedContent, tooltipContent)
296 {
297 }
298
299 public override ITooltip GetCustomTooltip() => new CustomTooltipAlt();
300 }
301
302 private class CustomContent
303 {
304 public readonly LocalisableString Text;
305
306 public CustomContent(string text)
307 {
308 Text = text;
309 }
310 }
311
312 private class CustomTooltip : CompositeDrawable, ITooltip<CustomContent>
313 {
314 private static int i;
315
316 private readonly SpriteText text;
317
318 public CustomTooltip()
319 {
320 AutoSizeAxes = Axes.Both;
321
322 InternalChildren = new Drawable[]
323 {
324 new Box
325 {
326 RelativeSizeAxes = Axes.Both,
327 Colour = FrameworkColour.GreenDark,
328 },
329 text = new SpriteText
330 {
331 Font = FrameworkFont.Regular.With(size: 16),
332 Padding = new MarginPadding(5),
333 },
334 new SpriteText
335 {
336 Anchor = Anchor.BottomLeft,
337 Font = FontUsage.Default.With(size: 12),
338 Colour = Color4.Yellow,
339 Text = $"Custom tooltip instance {i++}"
340 },
341 };
342 }
343
344 public void SetContent(CustomContent content) => text.Text = content.Text;
345
346 public void Move(Vector2 pos) => Position = pos;
347 }
348
349 private class CustomTooltipAlt : CustomTooltip
350 {
351 public CustomTooltipAlt()
352 {
353 AutoSizeAxes = Axes.Both;
354
355 Colour = Color4.Red;
356 }
357 }
358
359 private class TooltipSpriteText : Container, IHasTooltip
360 {
361 public LocalisableString TooltipText { get; }
362
363 public TooltipSpriteText(string displayedContent)
364 : this(displayedContent, displayedContent)
365 {
366 }
367
368 protected TooltipSpriteText(string displayedContent, string tooltipContent)
369 {
370 TooltipText = tooltipContent;
371
372 AutoSizeAxes = Axes.Both;
373 Children = new[]
374 {
375 new SpriteText
376 {
377 Text = displayedContent,
378 }
379 };
380 }
381 }
382
383 private class InstantTooltipSpriteText : TooltipSpriteText, IHasAppearDelay
384 {
385 public InstantTooltipSpriteText(string tooltipContent)
386 : base(tooltipContent, tooltipContent)
387 {
388 }
389
390 public InstantTooltipSpriteText(string displayedContent, string tooltipContent)
391 : base(displayedContent, tooltipContent)
392 {
393 }
394
395 public double AppearDelay => 0;
396 }
397
398 private class TooltipTooltipContainer : TooltipContainer, IHasTooltip
399 {
400 public LocalisableString TooltipText { get; set; }
401
402 public TooltipTooltipContainer(string tooltipText)
403 {
404 TooltipText = tooltipText;
405 }
406 }
407
408 private class TooltipTextBox : BasicTextBox, IHasTooltip
409 {
410 public LocalisableString TooltipText => Text;
411 }
412
413 private class TooltipBox : Box, IHasTooltip
414 {
415 public LocalisableString TooltipText { get; set; }
416 }
417
418 private class RectangleCursorContainer : CursorContainer
419 {
420 protected override Drawable CreateCursor() => new RectangleCursor();
421
422 private class RectangleCursor : Box
423 {
424 public RectangleCursor()
425 {
426 Size = new Vector2(20, 40);
427 }
428 }
429 }
430 }
431}