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.Linq;
5using NUnit.Framework;
6using osu.Framework.Graphics;
7using osu.Framework.Graphics.Containers;
8using osu.Framework.Graphics.Shapes;
9using osu.Framework.Graphics.Sprites;
10using osu.Framework.Graphics.UserInterface;
11using osu.Framework.Input;
12using osu.Framework.Testing;
13using osuTK;
14using osuTK.Graphics;
15using osuTK.Input;
16
17namespace osu.Framework.Tests.Visual.UserInterface
18{
19 public class TestSceneTextBox : ManualInputManagerTestScene
20 {
21 private FillFlowContainer textBoxes;
22
23 [SetUp]
24 public new void SetUp() => Schedule(() =>
25 {
26 Schedule(() =>
27 {
28 Child = textBoxes = new FillFlowContainer
29 {
30 Direction = FillDirection.Vertical,
31 Spacing = new Vector2(0, 50),
32 Padding = new MarginPadding
33 {
34 Top = 50,
35 },
36 Anchor = Anchor.TopCentre,
37 Origin = Anchor.TopCentre,
38 RelativeSizeAxes = Axes.Both,
39 Size = new Vector2(0.9f, 1)
40 };
41 });
42 });
43
44 [Test]
45 public void VariousTextBoxes()
46 {
47 AddStep("add textboxes", () =>
48 {
49 textBoxes.Add(new BasicTextBox
50 {
51 Size = new Vector2(100, 16),
52 TabbableContentContainer = textBoxes
53 });
54
55 textBoxes.Add(new BasicTextBox
56 {
57 Text = @"Limited length",
58 Size = new Vector2(200, 20),
59 LengthLimit = 20,
60 TabbableContentContainer = textBoxes
61 });
62
63 textBoxes.Add(new BasicTextBox
64 {
65 Text = @"Box with some more text",
66 Size = new Vector2(500, 30),
67 TabbableContentContainer = textBoxes
68 });
69
70 textBoxes.Add(new BasicTextBox
71 {
72 PlaceholderText = @"Placeholder text",
73 Size = new Vector2(500, 30),
74 TabbableContentContainer = textBoxes
75 });
76
77 textBoxes.Add(new BasicTextBox
78 {
79 Text = @"prefilled placeholder",
80 PlaceholderText = @"Placeholder text",
81 Size = new Vector2(500, 30),
82 TabbableContentContainer = textBoxes
83 });
84
85 textBoxes.Add(new BasicTextBox
86 {
87 Text = "Readonly textbox",
88 Size = new Vector2(500, 30),
89 ReadOnly = true,
90 TabbableContentContainer = textBoxes
91 });
92
93 textBoxes.Add(new CustomTextBox
94 {
95 Text = @"Custom textbox",
96 Size = new Vector2(500, 30),
97 TabbableContentContainer = textBoxes
98 });
99
100 FillFlowContainer otherTextBoxes = new FillFlowContainer
101 {
102 Direction = FillDirection.Vertical,
103 Spacing = new Vector2(0, 50),
104 Padding = new MarginPadding
105 {
106 Top = 50,
107 Left = 500
108 },
109 Anchor = Anchor.TopCentre,
110 Origin = Anchor.TopCentre,
111 RelativeSizeAxes = Axes.Both,
112 Size = new Vector2(0.8f, 1)
113 };
114
115 otherTextBoxes.Add(new BasicTextBox
116 {
117 PlaceholderText = @"Textbox in separate container",
118 Size = new Vector2(500, 30),
119 TabbableContentContainer = otherTextBoxes
120 });
121
122 otherTextBoxes.Add(new BasicPasswordTextBox
123 {
124 PlaceholderText = @"Password textbox",
125 Text = "Secret ;)",
126 Size = new Vector2(500, 30),
127 TabbableContentContainer = otherTextBoxes
128 });
129
130 FillFlowContainer nestedTextBoxes = new FillFlowContainer
131 {
132 Direction = FillDirection.Vertical,
133 Spacing = new Vector2(0, 50),
134 Margin = new MarginPadding { Left = 50 },
135 RelativeSizeAxes = Axes.Both,
136 Size = new Vector2(0.8f, 1)
137 };
138
139 nestedTextBoxes.Add(new BasicTextBox
140 {
141 PlaceholderText = @"Nested textbox 1",
142 Size = new Vector2(457, 30),
143 TabbableContentContainer = otherTextBoxes
144 });
145
146 nestedTextBoxes.Add(new BasicTextBox
147 {
148 PlaceholderText = @"Nested textbox 2",
149 Size = new Vector2(457, 30),
150 TabbableContentContainer = otherTextBoxes
151 });
152
153 nestedTextBoxes.Add(new BasicTextBox
154 {
155 PlaceholderText = @"Nested textbox 3",
156 Size = new Vector2(457, 30),
157 TabbableContentContainer = otherTextBoxes
158 });
159
160 otherTextBoxes.Add(nestedTextBoxes);
161
162 Add(otherTextBoxes);
163 });
164 }
165
166 [Test]
167 public void TestNumbersOnly()
168 {
169 NumberTextBox numbers = null;
170
171 AddStep("add number textbox", () =>
172 {
173 textBoxes.Add(numbers = new NumberTextBox
174 {
175 PlaceholderText = @"Only numbers",
176 Size = new Vector2(500, 30),
177 TabbableContentContainer = textBoxes
178 });
179 });
180
181 AddStep(@"set number text", () => numbers.Text = @"1h2e3l4l5o6");
182 AddAssert(@"number text only numbers", () => numbers.Text == @"123456");
183 }
184
185 [TestCase(true, true)]
186 [TestCase(true, false)]
187 [TestCase(false, false)]
188 public void CommitOnFocusLost(bool commitOnFocusLost, bool changeText)
189 {
190 InsertableTextBox textBox = null;
191
192 bool wasNewText = false;
193 int commitCount = 0;
194
195 AddStep("add commit on unfocus textbox", () =>
196 {
197 wasNewText = false;
198 commitCount = 0;
199
200 textBoxes.Add(textBox = new InsertableTextBox
201 {
202 Text = "Default Text",
203 CommitOnFocusLost = commitOnFocusLost,
204 Size = new Vector2(500, 30),
205 });
206
207 textBox.OnCommit += (_, newText) =>
208 {
209 commitCount++;
210 wasNewText = newText;
211 };
212 });
213
214 AddAssert("ensure no commits", () => commitCount == 0);
215
216 AddStep("click on textbox", () =>
217 {
218 InputManager.MoveMouseTo(textBox);
219 InputManager.Click(MouseButton.Left);
220 });
221
222 if (changeText)
223 AddStep("insert more text", () => textBox.InsertString(" Plus More"));
224
225 AddStep("click away", () =>
226 {
227 InputManager.MoveMouseTo(Vector2.One);
228 InputManager.Click(MouseButton.Left);
229 });
230
231 if (commitOnFocusLost)
232 {
233 AddAssert("ensure one commit", () => commitCount == 1);
234 AddAssert("ensure new text", () => wasNewText == changeText);
235 }
236 else
237 AddAssert("ensure no commits", () => commitCount == 0);
238
239 AddStep("click on textbox", () =>
240 {
241 InputManager.MoveMouseTo(textBox);
242 InputManager.Click(MouseButton.Left);
243 });
244
245 if (changeText)
246 AddStep("insert more text", () => textBox.InsertString(" Plus More"));
247
248 AddStep("commit via enter", () =>
249 {
250 InputManager.PressKey(Key.Enter);
251 InputManager.ReleaseKey(Key.Enter);
252 });
253
254 int expectedCount = 1 + (commitOnFocusLost ? 1 : 0);
255
256 AddAssert($"ensure {expectedCount} commit(s)", () => commitCount == expectedCount);
257 AddAssert("ensure new text", () => wasNewText == changeText);
258 }
259
260 [Test]
261 public void TestBackspaceWhileShifted()
262 {
263 InsertableTextBox textBox = null;
264
265 AddStep("add textbox", () =>
266 {
267 textBoxes.Add(textBox = new InsertableTextBox
268 {
269 Size = new Vector2(200, 40),
270 });
271 });
272
273 AddStep("click on textbox", () =>
274 {
275 InputManager.MoveMouseTo(textBox);
276 InputManager.Click(MouseButton.Left);
277 });
278
279 AddStep("type character", () =>
280 {
281 // tests don't actually send consumable text, but this important part is that we fire the key event to begin consuming.
282 InputManager.Key(Key.A);
283 textBox.Text += "a";
284 });
285
286 AddStep("backspace character", () => InputManager.Key(Key.BackSpace));
287 AddAssert("character removed", () => textBox.Text == string.Empty);
288
289 AddStep("shift down", () => InputManager.PressKey(Key.ShiftLeft));
290
291 AddStep("type character", () =>
292 {
293 InputManager.Key(Key.A);
294 textBox.Text += "A";
295 });
296
297 AddStep("backspace character", () => InputManager.Key(Key.BackSpace));
298 AddAssert("character removed", () => textBox.Text == string.Empty);
299
300 AddStep("shift up", () => InputManager.ReleaseKey(Key.ShiftLeft));
301 }
302
303 [Test]
304 public void TestPreviousWordDeletion()
305 {
306 InsertableTextBox textBox = null;
307
308 AddStep("add textbox", () =>
309 {
310 textBoxes.Add(textBox = new InsertableTextBox
311 {
312 Size = new Vector2(200, 40),
313 });
314 });
315
316 AddStep("click on textbox", () =>
317 {
318 InputManager.MoveMouseTo(textBox);
319 InputManager.Click(MouseButton.Left);
320 });
321
322 AddStep("insert three words", () => textBox.InsertString("some long text"));
323 AddStep("delete last word", () => InputManager.Keys(PlatformAction.DeleteBackwardWord));
324 AddAssert("two words remain", () => textBox.Text == "some long ");
325 AddStep("delete last word", () => InputManager.Keys(PlatformAction.DeleteBackwardWord));
326 AddAssert("one word remains", () => textBox.Text == "some ");
327 AddStep("delete last word", () => InputManager.Keys(PlatformAction.DeleteBackwardWord));
328 AddAssert("text is empty", () => textBox.Text.Length == 0);
329 AddStep("delete last word", () => InputManager.Keys(PlatformAction.DeleteBackwardWord));
330 AddAssert("text is empty", () => textBox.Text.Length == 0);
331 }
332
333 [Test]
334 public void TestPreviousWordDeletionWithShortWords()
335 {
336 InsertableTextBox textBox = null;
337
338 AddStep("add textbox", () =>
339 {
340 textBoxes.Add(textBox = new InsertableTextBox
341 {
342 Size = new Vector2(200, 40),
343 });
344 });
345
346 AddStep("click on textbox", () =>
347 {
348 InputManager.MoveMouseTo(textBox);
349 InputManager.Click(MouseButton.Left);
350 });
351
352 AddStep("insert three words", () => textBox.InsertString("a b c"));
353 AddStep("delete last word", () => InputManager.Keys(PlatformAction.DeleteBackwardWord));
354 AddAssert("two words remain", () => textBox.Text == "a b ");
355 AddStep("delete last word", () => InputManager.Keys(PlatformAction.DeleteBackwardWord));
356 AddAssert("one word remains", () => textBox.Text == "a ");
357 AddStep("delete last word", () => InputManager.Keys(PlatformAction.DeleteBackwardWord));
358 AddAssert("text is empty", () => textBox.Text.Length == 0);
359 AddStep("delete last word", () => InputManager.Keys(PlatformAction.DeleteBackwardWord));
360 AddAssert("text is empty", () => textBox.Text.Length == 0);
361 }
362
363 [Test]
364 public void TestNextWordDeletion()
365 {
366 InsertableTextBox textBox = null;
367
368 AddStep("add textbox", () =>
369 {
370 textBoxes.Add(textBox = new InsertableTextBox
371 {
372 Size = new Vector2(200, 40)
373 });
374 });
375
376 AddStep("click on textbox", () =>
377 {
378 InputManager.MoveMouseTo(textBox);
379 InputManager.Click(MouseButton.Left);
380 });
381
382 AddStep("insert three words", () => textBox.InsertString("some long text"));
383 AddStep("move caret to start", () => InputManager.Keys(PlatformAction.MoveBackwardLine));
384 AddStep("delete first word", () => InputManager.Keys(PlatformAction.DeleteForwardWord));
385 AddAssert("two words remain", () => textBox.Text == " long text");
386 AddStep("delete first word", () => InputManager.Keys(PlatformAction.DeleteForwardWord));
387 AddAssert("one word remains", () => textBox.Text == " text");
388 AddStep("delete first word", () => InputManager.Keys(PlatformAction.DeleteForwardWord));
389 AddAssert("text is empty", () => textBox.Text.Length == 0);
390 AddStep("delete first word", () => InputManager.Keys(PlatformAction.DeleteForwardWord));
391 AddAssert("text is empty", () => textBox.Text.Length == 0);
392 }
393
394 [Test]
395 public void TestNextWordDeletionWithShortWords()
396 {
397 InsertableTextBox textBox = null;
398
399 AddStep("add textbox", () =>
400 {
401 textBoxes.Add(textBox = new InsertableTextBox
402 {
403 Size = new Vector2(200, 40)
404 });
405 });
406
407 AddStep("click on textbox", () =>
408 {
409 InputManager.MoveMouseTo(textBox);
410 InputManager.Click(MouseButton.Left);
411 });
412
413 AddStep("insert three words", () => textBox.InsertString("a b c"));
414 AddStep("move caret to start", () => InputManager.Keys(PlatformAction.MoveBackwardLine));
415 AddStep("delete first word", () => InputManager.Keys(PlatformAction.DeleteForwardWord));
416 AddAssert("two words remain", () => textBox.Text == " b c");
417 AddStep("delete first word", () => InputManager.Keys(PlatformAction.DeleteForwardWord));
418 AddAssert("one word remains", () => textBox.Text == " c");
419 AddStep("delete first word", () => InputManager.Keys(PlatformAction.DeleteForwardWord));
420 AddAssert("text is empty", () => textBox.Text.Length == 0);
421 AddStep("delete first word", () => InputManager.Keys(PlatformAction.DeleteForwardWord));
422 AddAssert("text is empty", () => textBox.Text.Length == 0);
423 }
424
425 /// <summary>
426 /// Removes first 2 characters and append them, this tests layout positioning of the characters in the text box.
427 /// </summary>
428 [Test]
429 public void TestRemoveAndAppend()
430 {
431 InsertableTextBox textBox = null;
432
433 AddStep("add textbox", () =>
434 {
435 textBoxes.Add(textBox = new InsertableTextBox
436 {
437 Size = new Vector2(200, 40),
438 });
439 });
440
441 AddStep("focus textbox", () =>
442 {
443 InputManager.MoveMouseTo(textBox);
444 InputManager.Click(MouseButton.Left);
445 });
446
447 AddStep("insert word", () => textBox.InsertString("eventext"));
448 AddStep("remove 2 letters", () => removeFirstCharacters(2));
449 AddStep("append string", () => appendString(textBox, "ev"));
450 AddStep("remove 2 letters", () => removeFirstCharacters(2));
451 AddStep("append string", () => appendString(textBox, "en"));
452 AddStep("remove 2 letters", () => removeFirstCharacters(2));
453 AddStep("append string", () => appendString(textBox, "te"));
454 AddStep("remove 2 letters", () => removeFirstCharacters(2));
455 AddStep("append string", () => appendString(textBox, "xt"));
456 AddAssert("is correct displayed text", () => textBox.FlowingText == "eventext" && textBox.FlowingText == textBox.Text);
457 }
458
459 /// <summary>
460 /// Removes last 2 characters and prepend them, this tests layout positioning of the characters in the text box.
461 /// </summary>
462 [Test]
463 public void TestRemoveAndPrepend()
464 {
465 InsertableTextBox textBox = null;
466
467 AddStep("add textbox", () =>
468 {
469 textBoxes.Add(textBox = new InsertableTextBox
470 {
471 Size = new Vector2(200, 40),
472 });
473 });
474
475 AddStep("focus textbox", () =>
476 {
477 InputManager.MoveMouseTo(textBox);
478 InputManager.Click(MouseButton.Left);
479 });
480
481 AddStep("insert word", () => textBox.InsertString("eventext"));
482 AddStep("remove 2 letters", () => removeLastCharacters(2));
483 AddStep("prepend string", () => prependString(textBox, "xt"));
484 AddStep("remove 2 letters", () => removeLastCharacters(2));
485 AddStep("prepend string", () => prependString(textBox, "te"));
486 AddStep("remove 2 letters", () => removeLastCharacters(2));
487 AddStep("prepend string", () => prependString(textBox, "en"));
488 AddStep("remove 2 letters", () => removeLastCharacters(2));
489 AddStep("prepend string", () => prependString(textBox, "ev"));
490 AddAssert("is correct displayed text", () => textBox.FlowingText == "eventext" && textBox.FlowingText == textBox.Text);
491 }
492
493 [Test]
494 public void TestReplaceSelectionWhileLimited()
495 {
496 InsertableTextBox textBox = null;
497
498 AddStep("add limited textbox", () =>
499 {
500 textBoxes.Add(textBox = new InsertableTextBox
501 {
502 Size = new Vector2(200, 40),
503 Text = "some text",
504 });
505
506 textBox.LengthLimit = textBox.Text.Length;
507 });
508
509 AddStep("focus textbox", () =>
510 {
511 InputManager.MoveMouseTo(textBox);
512 InputManager.Click(MouseButton.Left);
513 });
514
515 AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
516 AddStep("insert string", () => textBox.InsertString("another"));
517 AddAssert("text replaced", () => textBox.FlowingText == "another" && textBox.FlowingText == textBox.Text);
518 }
519
520 [Test]
521 public void TestReadOnly()
522 {
523 BasicTextBox firstTextBox = null;
524 BasicTextBox secondTextBox = null;
525
526 AddStep("add textboxes", () => textBoxes.AddRange(new[]
527 {
528 firstTextBox = new BasicTextBox
529 {
530 Text = "Readonly textbox",
531 Size = new Vector2(500, 30),
532 ReadOnly = true,
533 TabbableContentContainer = textBoxes
534 },
535 secondTextBox = new BasicTextBox
536 {
537 Text = "Standard textbox",
538 Size = new Vector2(500, 30),
539 TabbableContentContainer = textBoxes
540 }
541 }));
542
543 AddStep("click first (readonly) textbox", () =>
544 {
545 InputManager.MoveMouseTo(firstTextBox);
546 InputManager.Click(MouseButton.Left);
547 });
548 AddAssert("first textbox has no focus", () => !firstTextBox.HasFocus);
549
550 AddStep("click second (editable) textbox", () =>
551 {
552 InputManager.MoveMouseTo(secondTextBox);
553 InputManager.Click(MouseButton.Left);
554 });
555 AddStep("try to tab backwards", () =>
556 {
557 InputManager.PressKey(Key.ShiftLeft);
558 InputManager.Key(Key.Tab);
559 InputManager.ReleaseKey(Key.ShiftLeft);
560 });
561 AddAssert("first (readonly) has no focus", () => !firstTextBox.HasFocus);
562
563 AddStep("drag on first (readonly) textbox", () =>
564 {
565 InputManager.MoveMouseTo(firstTextBox.ScreenSpaceDrawQuad.Centre);
566 InputManager.PressButton(MouseButton.Left);
567 InputManager.MoveMouseTo(firstTextBox.ScreenSpaceDrawQuad.TopLeft);
568 InputManager.ReleaseButton(MouseButton.Left);
569 });
570 AddAssert("first textbox has no focus", () => !firstTextBox.HasFocus);
571
572 AddStep("make first textbox non-readonly", () => firstTextBox.ReadOnly = false);
573 AddStep("click first textbox", () =>
574 {
575 InputManager.MoveMouseTo(firstTextBox);
576 InputManager.Click(MouseButton.Left);
577 });
578 AddStep("make first textbox readonly again", () => firstTextBox.ReadOnly = true);
579 AddAssert("first textbox yielded focus", () => !firstTextBox.HasFocus);
580 AddStep("delete last character", () => InputManager.Keys(PlatformAction.DeleteBackwardChar));
581 AddAssert("no text removed", () => firstTextBox.Text == "Readonly textbox");
582 }
583
584 private void prependString(InsertableTextBox textBox, string text)
585 {
586 InputManager.Keys(PlatformAction.MoveBackwardLine);
587
588 ScheduleAfterChildren(() => textBox.InsertString(text));
589 }
590
591 private void appendString(InsertableTextBox textBox, string text)
592 {
593 InputManager.Keys(PlatformAction.MoveForwardLine);
594
595 ScheduleAfterChildren(() => textBox.InsertString(text));
596 }
597
598 private void removeFirstCharacters(int count)
599 {
600 InputManager.Keys(PlatformAction.MoveBackwardLine);
601
602 for (int i = 0; i < count; i++)
603 InputManager.Keys(PlatformAction.DeleteForwardChar);
604 }
605
606 private void removeLastCharacters(int count)
607 {
608 InputManager.Keys(PlatformAction.MoveForwardLine);
609
610 for (int i = 0; i < count; i++)
611 InputManager.Keys(PlatformAction.DeleteBackwardChar);
612 }
613
614 public class InsertableTextBox : BasicTextBox
615 {
616 /// <summary>
617 /// Returns the shown-in-screen text.
618 /// </summary>
619 public string FlowingText => string.Concat(TextFlow.FlowingChildren.OfType<FallingDownContainer>().Select(c => c.OfType<SpriteText>().Single().Text.ToString()[0]));
620
621 public new void InsertString(string text) => base.InsertString(text);
622 }
623
624 private class NumberTextBox : BasicTextBox
625 {
626 protected override bool CanAddCharacter(char character) => char.IsNumber(character);
627 }
628
629 private class CustomTextBox : BasicTextBox
630 {
631 protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, CalculatedTextSize);
632
633 private class ScalingText : CompositeDrawable
634 {
635 private readonly SpriteText text;
636
637 public ScalingText(char c, float textSize)
638 {
639 AddInternal(text = new SpriteText
640 {
641 Anchor = Anchor.Centre,
642 Origin = Anchor.Centre,
643 Text = c.ToString(),
644 Font = FrameworkFont.Condensed.With(size: textSize),
645 });
646 }
647
648 protected override void LoadComplete()
649 {
650 base.LoadComplete();
651
652 Size = text.DrawSize;
653 }
654
655 public override void Show()
656 {
657 text.Scale = Vector2.Zero;
658 text.FadeIn(200).ScaleTo(1, 200);
659 }
660
661 public override void Hide()
662 {
663 text.Scale = Vector2.One;
664 text.ScaleTo(0, 200).FadeOut(200);
665 }
666 }
667
668 protected override Caret CreateCaret() => new BorderCaret();
669
670 private class BorderCaret : Caret
671 {
672 private const float caret_width = 2;
673
674 public BorderCaret()
675 {
676 RelativeSizeAxes = Axes.Y;
677
678 Masking = true;
679 BorderColour = Color4.White;
680 BorderThickness = 3;
681
682 InternalChild = new Box
683 {
684 RelativeSizeAxes = Axes.Both,
685 Colour = Color4.Transparent
686 };
687 }
688
689 public override void DisplayAt(Vector2 position, float? selectionWidth)
690 {
691 Position = position - Vector2.UnitX;
692 Width = selectionWidth + 1 ?? caret_width;
693 }
694 }
695 }
696 }
697}