1 module nodes.hover_chain; 2 3 import std.array; 4 import fluid; 5 6 @safe: 7 8 alias myHover = nodeBuilder!MyHover; 9 10 class MyHover : Node, MouseIO { 11 12 HoverIO hoverIO; 13 HoverPointer[] pointers; 14 15 inout(HoverPointer) makePointer(int number, Vector2 position, bool isDisabled = false) inout { 16 return inout HoverPointer(this, number, position, Vector2(), isDisabled); 17 } 18 19 void emit(int number, InputEvent event) { 20 21 foreach (pointer; pointers) { 22 if (pointer.number != number) continue; 23 hoverIO.emitEvent(pointer, event); 24 return; 25 } 26 27 assert(false); 28 29 } 30 31 override void resizeImpl(Vector2 space) { 32 require(hoverIO); 33 loadPointers(); 34 minSize = Vector2(); 35 } 36 37 override void drawImpl(Rectangle, Rectangle) { 38 loadPointers(); 39 } 40 41 void loadPointers() { 42 43 foreach (ref pointer; pointers) { 44 load(hoverIO, pointer); 45 } 46 47 } 48 49 } 50 51 alias hoverTracker = nodeBuilder!HoverTracker; 52 53 class HoverTracker : Node, Hoverable { 54 55 mixin enableInputActions; 56 57 HoverIO hoverIO; 58 59 int hoverImplCount; 60 int pressHeldCount; 61 int pressCount; 62 63 HoverPointer lastPointer; 64 Appender!(HoverPointer[]) pointers; 65 66 override void resizeImpl(Vector2) { 67 require(hoverIO); 68 minSize = Vector2(); 69 } 70 71 override void drawImpl(Rectangle, Rectangle) { 72 pointers.clear(); 73 foreach (HoverPointer pointer; hoverIO) { 74 pointers ~= pointer; 75 } 76 } 77 78 override bool blocksInput() const { 79 return isDisabled || isDisabledInherited; 80 } 81 82 override bool hoverImpl(HoverPointer) { 83 assert(!blocksInput); 84 hoverImplCount++; 85 return false; 86 } 87 88 override bool isHovered() const { 89 return hoverIO.isHovered(this); 90 } 91 92 alias opEquals = typeof(super).opEquals; 93 94 override bool opEquals(const Object other) const { 95 return super.opEquals(other); 96 } 97 98 @(FluidInputAction.press, WhileHeld) 99 void pressHeld(HoverPointer pointer) { 100 assert(!blocksInput); 101 pressHeldCount++; 102 lastPointer = pointer; 103 } 104 105 @(FluidInputAction.press) 106 void press(HoverPointer pointer) { 107 assert(!blocksInput); 108 pressCount++; 109 lastPointer = pointer; 110 } 111 112 } 113 114 alias scrollTracker = nodeBuilder!ScrollTracker; 115 116 class ScrollTracker : Frame, HoverScrollable { 117 118 bool disableScroll; 119 Vector2 totalScroll; 120 Vector2 lastScroll; 121 122 this(Node[] nodes...) { 123 super(nodes); 124 } 125 126 alias opEquals = Frame.opEquals; 127 128 override bool opEquals(const Object other) const { 129 return super.opEquals(other); 130 } 131 132 override bool canScroll(const HoverPointer) const { 133 return !disableScroll; 134 } 135 136 override Rectangle shallowScrollTo(const Node, Rectangle, Rectangle childBox) { 137 return childBox; 138 } 139 140 override bool scrollImpl(HoverPointer pointer) { 141 totalScroll += pointer.scroll; 142 lastScroll = pointer.scroll; 143 return true; 144 } 145 146 } 147 148 @("HoverChain assigns unique IDs for each pointer number") 149 unittest { 150 151 MyHover device; 152 153 auto root = hoverChain( 154 device = myHover(), 155 ); 156 157 device.pointers = [ 158 device.makePointer(0, Vector2(1, 1)), 159 device.makePointer(1, Vector2(1, 1)), 160 ]; 161 root.draw(); 162 163 assert(device.pointers[0].id != device.pointers[1].id); 164 165 } 166 167 @("HoverChain assigns unique IDs for different devices") 168 unittest { 169 170 MyHover firstDevice, secondDevice; 171 172 auto root = hoverChain( 173 vspace( 174 firstDevice = myHover(), 175 secondDevice = myHover() 176 ), 177 ); 178 179 firstDevice.pointers = [ 180 firstDevice.makePointer(0, Vector2(1, 1)), 181 ]; 182 secondDevice.pointers = [ 183 secondDevice.makePointer(0, Vector2(1, 1)), 184 ]; 185 root.draw(); 186 187 assert(firstDevice.pointers[0].id != secondDevice.pointers[0].id); 188 189 } 190 191 @("HoverChain can list hovered nodes") 192 unittest { 193 194 MyHover device; 195 Button one, two; 196 197 auto root = hoverChain( 198 .nullTheme, 199 sizeLock!vspace( 200 .sizeLimit(300, 300), 201 device = myHover(), 202 one = button(.layout!(1, "fill"), "One", delegate { }), 203 vframe(.layout!(1, "fill")), 204 two = button(.layout!(1, "fill"), "Two", delegate { }), 205 ), 206 ); 207 208 root.draw(); 209 assert(!root.hovers); 210 assert(!one.isHovered); 211 assert(!two.isHovered); 212 213 device.pointers = [ 214 device.makePointer(0, Vector2(0, 0)), 215 ]; 216 root.draw(); 217 root.draw(); 218 assert(root.hovers(one)); 219 assert(root.hoversOnly([one])); 220 assert(one.isHovered); 221 assert(!two.isHovered); 222 223 device.pointers = [ 224 device.makePointer(0, Vector2(0, 120)), 225 ]; 226 root.draw(); 227 root.draw(); 228 assert(!root.hovers()); 229 230 device.pointers = [ 231 device.makePointer(0, Vector2(0, 220)), 232 ]; 233 root.draw(); 234 root.draw(); 235 assert(root.hovers()); 236 assert(root.hoversOnly([two])); 237 assert(!one.isHovered); 238 assert(two.isHovered); 239 240 } 241 242 @("Opaque nodes block hover") 243 unittest { 244 245 MyHover device; 246 Button btn; 247 Frame frame; 248 249 auto root = hoverChain( 250 vspace( 251 device = myHover(), 252 onionFrame( 253 .layout!"fill", 254 btn = button("One", delegate { }), 255 frame = vframe(.layout!"fill"), 256 ), 257 ), 258 ); 259 260 device.pointers = [ 261 device.makePointer(0, Vector2(20, 20)), 262 ]; 263 root.draw(); 264 root.draw(); 265 assert(!root.hovers); 266 267 frame.hide(); 268 root.draw(); 269 root.draw(); 270 assert(root.hovers(btn)); 271 assert(btn.isHovered); 272 273 frame.show(); 274 root.draw(); 275 root.draw(); 276 assert(!root.hovers); 277 assert(!btn.isHovered); 278 279 } 280 281 @("HoverChain triggers input actions") 282 unittest { 283 284 MyHover device; 285 Button btn; 286 HoverChain hover; 287 int pressCount; 288 289 auto map = InputMapping(); 290 map.bindNew!(FluidInputAction.press)(MouseIO.codes.left); 291 292 auto root = inputMapChain( 293 .nullTheme, 294 map, 295 hover = hoverChain( 296 vspace( 297 device = myHover(), 298 btn = button("One", delegate { pressCount++; }), 299 ), 300 ), 301 ); 302 303 device.pointers = [ 304 device.makePointer(0, Vector2(10, 10)), 305 ]; 306 root.draw(); 307 root.draw(); 308 309 assert(hover.hovers(btn)); 310 assert(pressCount == 0); 311 312 device.emit(0, MouseIO.release.left); 313 314 assert(pressCount == 0); 315 316 root.draw(); 317 assert(pressCount == 1); 318 319 root.draw(); 320 assert(pressCount == 1); 321 322 } 323 324 @("HoverChain actions won't apply if hover changes") 325 unittest { 326 327 MyHover device; 328 HoverChain hover; 329 330 int onePressed; 331 int twoPressed; 332 333 auto map = InputMapping(); 334 map.bindNew!(FluidInputAction.press)(MouseIO.codes.left); 335 336 auto root = inputMapChain( 337 map, 338 hover = hoverChain( 339 .nullTheme, 340 sizeLock!vspace( 341 .sizeLimit(400, 400), 342 device = myHover(), 343 button(.layout!(1, "fill"), "One", delegate { onePressed++; }), 344 button(.layout!(1, "fill"), "Two", delegate { twoPressed++; }), 345 ), 346 ) 347 ); 348 349 device.pointers = [ 350 device.makePointer(0, Vector2(100, 100)), 351 device.makePointer(1, Vector2(300, 300)), 352 ]; 353 root.draw(); 354 355 // Hold both — no input action is necessary 356 device.emit(0, MouseIO.hold.left); 357 device.emit(1, MouseIO.hold.left); 358 root.draw(); 359 360 // Move them 361 device.pointers = [ 362 device.makePointer(0, Vector2(100, 300)), 363 device.makePointer(1, Vector2(300, 100)), 364 ]; 365 device.emit(0, MouseIO.hold.left); 366 device.emit(1, MouseIO.hold.left); 367 root.draw(); 368 369 assert(onePressed == 0); 370 assert(twoPressed == 0); 371 372 // Press them 373 device.emit(0, MouseIO.release.left); 374 device.emit(1, MouseIO.release.left); 375 root.draw(); 376 hover.runInputAction!(FluidInputAction.press)(device.pointers[0]); 377 hover.runInputAction!(FluidInputAction.press)(device.pointers[1]); 378 379 assert(onePressed == 0); 380 assert(twoPressed == 0); 381 382 // Move outside of the canvas 383 device.pointers = [ 384 device.makePointer(0, Vector2(500, 500)), 385 ]; 386 device.emit(0, MouseIO.hold.left); 387 root.draw(); 388 device.emit(0, MouseIO.hold.left); 389 root.draw(); 390 device.emit(0, MouseIO.release.left); 391 root.draw(); 392 assert(onePressed == 0); 393 394 } 395 396 @("HoverChain triggers hover events, even if moved") 397 unittest { 398 399 MyHover device; 400 HoverChain hover; 401 HoverTracker tracker1, tracker2; 402 403 auto map = InputMapping(); 404 map.bindNew!(FluidInputAction.press)(MouseIO.codes.left); 405 406 auto root = inputMapChain( 407 map, 408 hover = hoverChain( 409 .nullTheme, 410 sizeLock!vspace( 411 .sizeLimit(400, 400), 412 device = myHover(), 413 tracker1 = hoverTracker(.layout!(1, "fill")), 414 tracker2 = hoverTracker(.layout!(1, "fill")), 415 ), 416 ) 417 ); 418 419 device.pointers = [ 420 device.makePointer(0, Vector2(100, 100)), 421 ]; 422 root.draw(); 423 assert(tracker1.hoverImplCount == 1); 424 assert(tracker1.pressHeldCount == 0); 425 assert(tracker1.pressCount == 0); 426 assert(tracker2.hoverImplCount == 0); 427 428 // Hover 429 root.draw(); 430 assert(tracker1.hoverImplCount == 2); 431 assert(tracker1.pressHeldCount == 0); 432 433 // Hold 434 device.emit(0, MouseIO.hold.left); 435 root.draw(); 436 assert(tracker1.hoverImplCount == 2); // pressHeld overrides this call 437 assert(tracker1.pressHeldCount == 1); 438 439 device.emit(0, MouseIO.hold.left); 440 root.draw(); 441 assert(tracker1.hoverImplCount == 2); 442 assert(tracker1.pressHeldCount == 2); 443 assert(tracker1.pressCount == 0); 444 assert(tracker2.hoverImplCount == 0); 445 446 // Press 447 device.emit(0, MouseIO.press.left); 448 root.draw(); 449 assert(tracker1.hoverImplCount == 2); 450 assert(tracker1.pressHeldCount == 3); 451 assert(tracker1.pressCount == 1); 452 assert(tracker2.hoverImplCount == 0); 453 454 // Move & press 455 device.pointers = [ 456 device.makePointer(0, Vector2(100, 300)), 457 ]; 458 device.emit(0, MouseIO.hold.left); 459 root.draw(); 460 assert(tracker1.hoverImplCount == 2); 461 assert(tracker1.pressHeldCount == 4); 462 assert(tracker2.hoverImplCount == 0); 463 assert(tracker2.pressHeldCount == 0); 464 465 device.emit(0, MouseIO.hold.left); 466 root.draw(); 467 assert(tracker1.hoverImplCount == 2); // Hover still calls the old tracker 468 assert(tracker1.pressHeldCount == 5); 469 470 device.emit(0, MouseIO.press.left); 471 root.draw(); 472 assert(tracker1.hoverImplCount == 3); 473 assert(tracker1.pressHeldCount == 5); 474 assert(tracker1.pressCount == 1); 475 assert(tracker2.hoverImplCount == 0); 476 assert(tracker2.pressHeldCount == 0); 477 assert(tracker2.pressCount == 0); // The press isn't registered. 478 479 root.draw(); 480 assert(tracker2.hoverImplCount == 1); // Hover only registers after release 481 482 // Unrelated input actions cannot trigger press 483 hover.runInputAction!(FluidInputAction.press)(device.pointers[0]); 484 assert(tracker2.pressCount == 1); 485 hover.runInputAction!(FluidInputAction.contextMenu)(device.pointers[0]); 486 assert(tracker2.pressCount == 1); 487 assert(tracker2.hoverImplCount == 1); 488 489 } 490 491 @("HoverChain runs hoverImpl if ActionIO is absent") 492 unittest { 493 494 MyHover device; 495 HoverChain hover; 496 HoverTracker tracker; 497 498 auto root = hover = hoverChain( 499 .nullTheme, 500 sizeLock!vspace( 501 .sizeLimit(400, 400), 502 device = myHover(), 503 tracker = hoverTracker(.layout!(1, "fill")), 504 ), 505 ); 506 507 device.pointers = [ 508 device.makePointer(0, Vector2(10, 10)), 509 ]; 510 root.draw(); 511 assert(tracker.hoverImplCount == 1); 512 513 } 514 515 @("HoverChain doesn't call input handlers on disabled nodes") 516 unittest { 517 518 MyHover device; 519 HoverTracker inaccessibleTracker, mainTracker; 520 521 auto root = hoverChain( 522 .layout!(1, "fill"), 523 vspace( 524 .layout!(1, "fill"), 525 device = myHover(), 526 onionFrame( 527 .layout!(1, "fill"), 528 inaccessibleTracker = hoverTracker(), 529 mainTracker = hoverTracker(.layout!"fill") 530 ), 531 ), 532 ); 533 534 device.pointers = [ 535 device.makePointer(0, Vector2(100, 100)), 536 ]; 537 root.draw(); 538 assert(mainTracker.isHovered); 539 assert(mainTracker.hoverImplCount == 1); 540 assert(inaccessibleTracker.hoverImplCount == 0); 541 542 // Disable the tracker; it shouldn't take hoverImpl, but it should continue to block 543 mainTracker.disable(); 544 root.draw(); 545 assert(mainTracker.hoverImplCount == 1); 546 assert(inaccessibleTracker.hoverImplCount == 0); 547 548 // Press it 549 root.emitEvent(device.pointers[0], MouseIO.release.left); 550 root.draw(); 551 assert(mainTracker.hoverImplCount == 1); 552 assert(mainTracker.pressCount == 0); 553 assert(inaccessibleTracker.hoverImplCount == 0); 554 assert(inaccessibleTracker.pressCount == 0); 555 556 } 557 558 @("HoverChain won't call handlers if disability status changes") 559 unittest { 560 561 MyHover device; 562 HoverTracker tracker; 563 564 auto root = hoverChain( 565 .layout!"fill", 566 vspace( 567 .layout!"fill", 568 device = myHover(), 569 tracker = hoverTracker(.layout!(1, "fill")) 570 ), 571 ); 572 573 device.pointers = [ 574 device.makePointer(0, Vector2(100, 100)), 575 ]; 576 root.draw(); 577 578 // Hold the left button now 579 root.runInputAction!(FluidInputAction.press)(device.pointers[0], false); 580 root.draw(); 581 assert(tracker.isHovered); 582 assert(tracker.pressHeldCount == 1); 583 584 // Block the button and press it 585 tracker.disable(); 586 root.runInputAction!(FluidInputAction.press)(device.pointers[0], true); 587 root.draw(); 588 assert(tracker.isHovered); 589 assert(tracker.pressHeldCount == 1); 590 assert(tracker.pressCount == 0); 591 592 } 593 594 @("HoverChain fires scroll events for scrollable nodes") 595 unittest { 596 597 ScrollTracker tracker; 598 Button btn; 599 600 auto hover = hoverChain( 601 .layout!"fill", 602 tracker = scrollTracker( 603 .layout!(1, "fill"), 604 btn = button( 605 .layout!(1, "fill"), 606 "Do not press me", 607 delegate { 608 assert(false); 609 } 610 ), 611 ), 612 ); 613 auto root = hover; 614 615 hover.point(50, 50).scroll(0, 10) 616 .then((a) { 617 assert(a.currentHover && a.currentHover.opEquals(btn)); 618 assert(a.currentScroll && a.currentScroll.opEquals(tracker)); 619 assert(tracker.totalScroll == Vector2(0, 10)); 620 assert(tracker.lastScroll == Vector2(0, 10)); 621 622 return a.scroll(5, 20); 623 }) 624 .then((a) { 625 assert(a.currentHover && a.currentHover.opEquals(btn)); 626 assert(a.currentScroll && a.currentScroll.opEquals(tracker)); 627 assert(tracker.lastScroll == Vector2(5, 20)); 628 assert(tracker.totalScroll == Vector2(5, 30)); 629 }) 630 .runWhileDrawing(root, 3); 631 632 } 633 634 @("HoverChain supports touchscreen scrolling") 635 unittest { 636 637 // Try the same motion without holding the scroll (like a mouse) 638 // and then while holding (like a touchscreen) 639 foreach (testHold; [false, true]) { 640 641 ScrollTracker targetTracker, dummyTracker; 642 643 auto hover = hoverChain( 644 .layout!"fill", 645 sizeLock!vspace( 646 .sizeLimit(100, 100), 647 targetTracker = scrollTracker( 648 .layout!(2, "fill"), 649 ), 650 dummyTracker = scrollTracker( 651 .layout!(1, "fill"), 652 ), 653 ), 654 ); 655 auto root = hover; 656 657 hover.point(50, 25) 658 .then((a) { 659 660 assert(a.currentScroll.opEquals(targetTracker)); 661 662 // Hold the left mouse button 663 a.press(false); 664 return a.move(50, 75).holdScroll(0, -25, testHold); 665 666 }) 667 .then((a) { 668 669 if (testHold) 670 assert(a.currentScroll.opEquals(targetTracker)); 671 else 672 assert(a.currentScroll.opEquals(dummyTracker)); 673 674 // Release the mouse button 675 a.press; 676 return a.holdScroll(0, -25, testHold); 677 678 }) 679 .runWhileDrawing(root); 680 681 if (testHold) { 682 assert(targetTracker.totalScroll.y == -50); 683 assert(dummyTracker.totalScroll.y == 0); 684 } 685 else { 686 assert(targetTracker.totalScroll.y == 0); 687 assert(dummyTracker.totalScroll.y == -50); 688 } 689 690 } 691 692 } 693 694 @("Held scroll counts as a single motion for canScroll() in HoverChain") 695 unittest { 696 697 // Compare differences between held and hovered scroll 698 foreach (testHold; [false, true]) { 699 700 ScrollTracker outerTracker, innerTracker; 701 702 auto hover = hoverChain( 703 sizeLock!vspace( 704 .sizeLimit(100, 100), 705 outerTracker = scrollTracker( 706 .layout!(1, "fill"), 707 innerTracker = scrollTracker( 708 .layout!(1, "fill"), 709 ), 710 ), 711 ), 712 ); 713 auto root = hover; 714 715 hover.point(50, 50) 716 .then((a) { 717 assert(a.currentScroll.opEquals(innerTracker)); 718 return a.holdScroll(0, -20, testHold); 719 }) 720 // Pretend innerTracker has reached its limit for scrolling 721 .then((a) { 722 assert(a.currentScroll.opEquals(innerTracker)); 723 innerTracker.disableScroll = true; 724 return a.holdScroll(0, -10, testHold); 725 }) 726 .then((a) { 727 if (testHold) { 728 assert(a.currentScroll.opEquals(innerTracker)); 729 } 730 else { 731 assert(a.currentScroll.opEquals(outerTracker)); 732 } 733 }) 734 .runWhileDrawing(root, 3); 735 736 // A held scroll counts as a single motion, so it shouldn't test canScroll during the second frame 737 if (testHold) { 738 assert(innerTracker.totalScroll == Vector2(0, -30)); 739 assert(outerTracker.totalScroll == Vector2(0, 0)); 740 } 741 742 // If not held, the motions are separate, so outerTracker should get the second part 743 else { 744 assert(innerTracker.totalScroll == Vector2(0, -20)); 745 assert(outerTracker.totalScroll == Vector2(0, -10)); 746 } 747 748 } 749 750 } 751 752 @("Multiple pointers can be created in HoverChain at once") 753 unittest { 754 755 auto hover = hoverChain(); 756 auto root = hover; 757 758 // Resize first 759 root.draw(); 760 761 HoverPointer pointer1; 762 pointer1.number = 1; 763 pointer1.position = Vector2(10, 10); 764 hover.loadTo(pointer1); 765 766 HoverPointer pointer2; 767 pointer2.number = 2; 768 pointer2.position = Vector2(20, 20); 769 hover.loadTo(pointer2); 770 root.draw(); 771 772 assert(hover.fetch(pointer1.id).position == Vector2(10, 10)); 773 assert(hover.fetch(pointer2.id).position == Vector2(20, 20)); 774 775 } 776 777 @("Pressing a node switches focus") 778 unittest { 779 780 Button button1, button2; 781 782 auto focus = focusChain(); 783 auto hover = hoverChain(); 784 auto root = chain( 785 focus, 786 hover, 787 sizeLock!vspace( 788 .sizeLimit(100, 100), 789 .nullTheme, 790 button1 = button(.layout!(1, "fill"), "One", delegate { }), 791 button2 = button(.layout!(1, "fill"), "Two", delegate { }), 792 ), 793 ); 794 795 root.draw(); 796 797 // Hover the first button; focus should stay the same 798 hover.point(50, 25) 799 .then((a) { 800 assert(a.isHovered(button1)); 801 assert(focus.currentFocus is null); 802 803 // Press to change focus (inactive) 804 a.press(false); 805 return a.stayIdle; 806 }) 807 .then((a) { 808 assert(a.isHovered(button1)); 809 assert(focus.isFocused(button1)); 810 811 // Move onto the other button, focus should stay the same 812 a.press(false); 813 return a.move(50, 75); 814 }) 815 .then((a) { 816 assert(a.isHovered(button1)); 817 assert(focus.isFocused(button1)); 818 819 a.press(); 820 821 }) 822 .runWhileDrawing(root); 823 824 assert(focus.isFocused(button1)); 825 826 } 827 828 @("Pressing a non-focusable node clears focus") 829 unittest { 830 831 Button targetButton; 832 Frame filler; 833 834 auto focus = focusChain(); 835 auto hover = hoverChain(); 836 auto root = chain( 837 focus, 838 hover, 839 sizeLock!vspace( 840 .sizeLimit(100, 100), 841 .nullTheme, 842 filler = vframe(.layout!(1, "fill")), 843 targetButton = button(.layout!(1, "fill"), "Target", delegate { }), 844 ), 845 ); 846 847 root.draw(); 848 focus.currentFocus = targetButton; 849 850 // Hover the space; focus should stay the same 851 hover.point(50, 25) 852 .then((a) { 853 assert(!a.isHovered(targetButton)); 854 assert(focus.currentFocus.opEquals(targetButton)); 855 856 // Press to change focus (inactive) 857 a.press(false); 858 return a.stayIdle; 859 }) 860 .then((a) { 861 assert(!a.isHovered(targetButton)); 862 assert(focus.currentFocus is null); 863 864 // Move onto the button, focus should remain empty 865 a.press(false); 866 return a.move(50, 75); 867 }) 868 .then((a) { 869 assert(!a.isHovered(targetButton)); 870 assert(focus.currentFocus is null); 871 872 a.press(); 873 874 }) 875 .runWhileDrawing(root); 876 877 assert(focus.currentFocus is null); 878 879 } 880 881 @("Input event handlers receive negative pointer IDs from HoverChain") 882 unittest { 883 884 auto tracker = sizeLock!hoverTracker( 885 .sizeLimit(100, 100), 886 ); 887 auto hover = hoverChain(tracker); 888 auto root = chain( 889 inputMapChain(), 890 hover, 891 ); 892 893 // Try two pointers: 894 // First pointer should be ID 0, armed ID -1, 895 // Second pointer should be ID 1, armed ID -2 896 foreach (int id, armedID; [-1, -2]) { 897 898 // Setup the pointer and click 899 auto action = hover.point(50, 50); 900 action.runWhileDrawing(root); 901 hover.emitEvent(action.pointer, MouseIO.press.left); 902 root.draw(); 903 904 assert(action.pointer.id == id); 905 assert(tracker.pressCount == 1); 906 assert(tracker.lastPointer.id == armedID); 907 908 // Try upadting the data 909 action.move(20, 20); 910 assert(hover.fetch(id).position == Vector2(20, 20)); 911 assert(hover.fetch(armedID).position == Vector2(50, 50)); 912 913 tracker.pressCount = 0; 914 915 } 916 917 } 918 919 @("HoverChain correctly receives scroll data from node inside") 920 unittest { 921 922 static class IncrementalScroller : Node, MouseIO { 923 924 HoverIO hoverIO; 925 HoverPointer pointer; 926 int value; 927 928 override void resizeImpl(Vector2) { 929 require(hoverIO); 930 pointer.device = this; 931 minSize = Vector2(0, 0); 932 } 933 934 override void drawImpl(Rectangle, Rectangle) { 935 pointer.position = Vector2(0, 0); 936 pointer.scroll = Vector2(0, ++value); 937 load(hoverIO, pointer); 938 } 939 940 } 941 942 alias incrementalScroller = nodeBuilder!IncrementalScroller; 943 944 auto automaton = incrementalScroller(); 945 auto tracker = sizeLock!scrollTracker( 946 .sizeLimit(10, 10), 947 ); 948 auto hover = hoverChain( 949 vspace(tracker, automaton), 950 ); 951 auto root = testSpace(hover); 952 953 // One frame to find the node 954 root.draw(); 955 assert(tracker.lastScroll.y == 0); 956 assert(tracker.totalScroll.y == 0); 957 958 root.draw(); 959 assert(tracker.lastScroll.y == 1); 960 assert(tracker.totalScroll.y == 1); 961 962 root.draw(); 963 assert(tracker.lastScroll.y == 2); 964 assert(tracker.totalScroll.y == 3); 965 966 } 967 968 @("HoverChain exposes all active pointers") 969 unittest { 970 971 auto hover = hoverChain(); 972 auto action1 = hover.point(10, 20); 973 auto action2 = hover.point(20, 10); 974 975 size_t index; 976 foreach (HoverPointer pointer; hover) { 977 if (index++ == 0) { 978 assert(pointer == action1.pointer); 979 assert(pointer != action2.pointer); 980 } 981 else { 982 assert(pointer != action1.pointer); 983 assert(pointer == action2.pointer); 984 } 985 } 986 987 } 988 989 @("HoverChain's HoverPointer iterator uses armed pointers when used while drawing") 990 unittest { 991 992 auto tracker = hoverTracker(); 993 auto hover = hoverChain(tracker); 994 auto action1 = hover.point(10, 20); 995 auto action2 = hover.point(20, 10); 996 997 hover.draw(); 998 999 assert(tracker.pointers[][0].id == hover.armedPointerID(action1.pointer.id)); 1000 assert(tracker.pointers[][1].id == hover.armedPointerID(action2.pointer.id)); 1001 1002 foreach (HoverPointer pointer; hover) { 1003 assert(pointer.id == hover.normalizedPointerID(pointer.id)); 1004 assert(pointer.id != hover.armedPointerID(pointer.id)); 1005 } 1006 1007 }