A game framework written with osu! in mind.
at master 26 kB view raw
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}