1 /// 2 module fluid.input; 3 4 import std.meta; 5 import std.format; 6 import std.traits; 7 import std.algorithm; 8 9 import fluid.node; 10 import fluid.tree; 11 import fluid.style; 12 import fluid.backend; 13 14 15 @safe: 16 17 18 /// Make a InputAction handler react to every frame as long as the action is being held (mouse button held down, 19 /// key held down, etc.). 20 enum whileDown; 21 22 /// Default input actions one can listen to. 23 @InputAction 24 enum FluidInputAction { 25 26 // Basic 27 press, /// Press the input. Used for example to activate buttons. 28 submit, /// Submit input, eg. finish writing in textInput. 29 cancel, /// Cancel the input. 30 contextMenu, /// Open context menu. 31 32 // Focus 33 focusPrevious, /// Focus previous input. 34 focusNext, /// Focus next input. 35 focusLeft, /// Focus input on the left. 36 focusRight, /// Focus input on the right. 37 focusUp, /// Focus input above. 38 focusDown, /// Focus input below. 39 40 // Text navigation 41 breakLine, /// Start a new text line, place a line feed. 42 previousChar, /// Move to the previous character in text. 43 nextChar, /// Move to the next character in text. 44 previousWord, /// Move to the previous word in text. 45 nextWord, /// Move to the next word in text. 46 previousLine, /// Move to the previous line in text. 47 nextLine, /// Move to the next line in text. 48 toLineStart, /// Move to the beginning of this line; Home key. 49 toLineEnd, /// Move to the end of this line; End key. 50 toStart, /// Move to the beginning. 51 toEnd, /// Move to the end. 52 53 // Editing 54 backspace, /// Erase last character in an input. 55 backspaceWord, /// Erase last a word in an input. 56 deleteChar, /// Delete the next character in an input 57 deleteWord, /// Delete the next word in an input 58 copy, /// Copy selected content. 59 cut, /// Cut (copy and delete) selected content. 60 paste, /// Paste selected content. 61 undo, /// Undo last action. 62 redo, /// Redo last action; Reverse "undo". 63 insertTab, /// Insert a tab into a code editor (tab key) 64 indent, /// Indent current line or selection in a code editor. 65 outdent, /// Outdent current line or selection in a code editor (shift+tab). 66 67 // Selection 68 selectPreviousChar, /// Select previous character in text. 69 selectNextChar, /// Select next character in text. 70 selectPreviousWord, /// Select previous word in text. 71 selectNextWord, /// Select next word in text. 72 selectPreviousLine, /// Select to previous line in text. 73 selectNextLine, /// Select to next line in text. 74 selectAll, /// Select all in text. 75 selectToLineStart, /// Select from here to line beginning. 76 selectToLineEnd, /// Select from here to line end. 77 selectToStart, /// Select from here to beginning. 78 selectToEnd, /// Select from here to end. 79 80 // List navigation 81 entryPrevious, /// Navigate to the previous list entry. 82 entryNext, /// Navigate to the next list entry. 83 entryUp, /// Navigate up in a tree, eg. in the file picker. 84 85 // Scrolling 86 scrollLeft, /// Scroll left a bit. 87 scrollRight, /// Scroll right a bit. 88 scrollUp, /// Scroll up a bit. 89 scrollDown, /// Scroll down a bit 90 pageLeft, /// Scroll left by a page. Unbound by default. 91 pageRight, /// Scroll right by a page. Unbound by default. 92 pageUp, /// Scroll up by a page. 93 pageDown, /// Scroll down by a page. 94 95 } 96 97 /// ID of an input action. 98 immutable struct InputActionID { 99 100 /// Unique ID of the action. 101 size_t id; 102 103 /// Action name. Only emitted when debugging. 104 debug string name; 105 106 /// Get ID of an input action. 107 this(IA : InputAction!actionType, alias actionType)(IA) immutable { 108 109 this.id = cast(size_t) &IA._id; 110 debug this.name = fullyQualifiedName!(IA.type); 111 112 } 113 114 static InputActionID from(alias item)() { 115 116 return InputAction!item.id; 117 118 } 119 120 bool opEqual(InputActionID other) { 121 122 return id == other.id; 123 124 } 125 126 } 127 128 /// Check if the given symbol is an input action type. 129 /// 130 /// The symbol symbol must be a member of an enum marked with `@InputAction`. The enum $(B must not) be a manifest 131 /// constant (eg. `enum foo = 123;`). 132 template isInputActionType(alias actionType) { 133 134 // Require the action type to be an enum 135 static if (is(typeof(actionType) == enum)) { 136 137 // Search through the enum attributes 138 static foreach (attribute; __traits(getAttributes, typeof(actionType))) { 139 140 // Not yet found 141 static if (!is(typeof(isInputActionType) == bool)) { 142 143 // Check if this is the attribute we're looking for 144 static if (__traits(isSame, attribute, InputAction)) { 145 146 enum isInputActionType = true; 147 148 } 149 150 } 151 152 } 153 154 } 155 156 // Not found 157 static if (!is(typeof(isInputActionType) == bool)) { 158 159 // Respond as false 160 enum isInputActionType = false; 161 162 } 163 164 } 165 166 unittest { 167 168 enum MyEnum { 169 foo = 123, 170 } 171 172 @InputAction 173 enum MyAction { 174 foo, 175 } 176 177 static assert(isInputActionType!(FluidInputAction.entryUp)); 178 static assert(isInputActionType!(MyAction.foo)); 179 180 static assert(!isInputActionType!InputNode); 181 static assert(!isInputActionType!InputAction); 182 static assert(!isInputActionType!(InputAction!(FluidInputAction.entryUp))); 183 static assert(!isInputActionType!FluidInputAction); 184 static assert(!isInputActionType!MyEnum); 185 static assert(!isInputActionType!(MyEnum.foo)); 186 static assert(!isInputActionType!MyAction); 187 188 189 } 190 191 /// Represents a key or button input combination. 192 struct InputStroke { 193 194 import std.sumtype; 195 196 alias Item = SumType!(KeyboardKey, MouseButton, GamepadButton); 197 198 Item[] input; 199 200 this(T...)(T items) 201 if (!is(items : Item[])) { 202 203 input.length = items.length; 204 static foreach (i, item; items) { 205 206 input[i] = Item(item); 207 208 } 209 210 } 211 212 this(Item[] items) { 213 214 input = items; 215 216 } 217 218 /// Get number of items in the stroke. 219 size_t length() const => input.length; 220 221 /// Get a copy of the input stroke with the last item removed, if any. 222 /// 223 /// For example, for a `leftShift+w` stroke, this will return `leftShift`. 224 InputStroke modifiers() { 225 226 return input.length 227 ? InputStroke(input[0..$-1]) 228 : InputStroke(); 229 230 } 231 232 /// Check if the last item of this input stroke is done with a mouse 233 bool isMouseStroke() const { 234 235 return isMouseItem(input[$-1]); 236 237 } 238 239 unittest { 240 241 assert(!InputStroke(KeyboardKey.leftControl).isMouseStroke); 242 assert(!InputStroke(KeyboardKey.w).isMouseStroke); 243 assert(!InputStroke(KeyboardKey.leftControl, KeyboardKey.w).isMouseStroke); 244 245 assert(InputStroke(MouseButton.left).isMouseStroke); 246 assert(InputStroke(KeyboardKey.leftControl, MouseButton.left).isMouseStroke); 247 248 assert(!InputStroke(GamepadButton.triangle).isMouseStroke); 249 assert(!InputStroke(KeyboardKey.leftControl, GamepadButton.triangle).isMouseStroke); 250 251 } 252 253 /// Check if the given item is done with a mouse. 254 static bool isMouseItem(Item item) { 255 256 return item.match!( 257 (MouseButton _) => true, 258 (_) => false, 259 ); 260 261 } 262 263 /// Check if all keys or buttons required for the stroke are held down. 264 bool isDown(const FluidBackend backend) const { 265 266 return input.all!(a => isItemDown(backend, a)); 267 268 } 269 270 /// 271 unittest { 272 273 auto stroke = InputStroke(KeyboardKey.leftControl, KeyboardKey.w); 274 auto io = new HeadlessBackend; 275 276 // No keys pressed 277 assert(!stroke.isDown(io)); 278 279 // Control pressed 280 io.press(KeyboardKey.leftControl); 281 assert(!stroke.isDown(io)); 282 283 // Both keys pressed 284 io.press(KeyboardKey.w); 285 assert(stroke.isDown(io)); 286 287 // Still pressed, but not immediately 288 io.nextFrame; 289 assert(stroke.isDown(io)); 290 291 // W pressed 292 io.release(KeyboardKey.leftControl); 293 assert(!stroke.isDown(io)); 294 295 } 296 297 /// Check if the stroke has been triggered during this frame. 298 /// 299 /// If the last item of the action is a mouse button, the action will be triggered on release. If it's a keyboard 300 /// key or gamepad button, it'll be triggered on press. All previous items, if present, have to be held down at the 301 /// time. 302 bool isActive(const FluidBackend backend) const @trusted { 303 304 // For all but the last item, check if it's held down 305 return input[0 .. $-1].all!(a => isItemDown(backend, a)) 306 307 // For the last item, check if it's pressed or released, depending on the type 308 && isItemActive(backend, input[$-1]); 309 310 } 311 312 unittest { 313 314 auto singleKey = InputStroke(KeyboardKey.w); 315 auto stroke = InputStroke(KeyboardKey.leftControl, KeyboardKey.leftShift, KeyboardKey.w); 316 auto io = new HeadlessBackend; 317 318 // No key pressed 319 assert(!singleKey.isActive(io)); 320 assert(!stroke.isActive(io)); 321 322 io.press(KeyboardKey.w); 323 324 // Just pressed the "W" key 325 assert(singleKey.isActive(io)); 326 assert(!stroke.isActive(io)); 327 328 io.nextFrame; 329 330 // The stroke stops being active on the next frame 331 assert(!singleKey.isActive(io)); 332 assert(!stroke.isActive(io)); 333 334 io.press(KeyboardKey.leftControl); 335 io.press(KeyboardKey.leftShift); 336 337 assert(!singleKey.isActive(io)); 338 assert(!stroke.isActive(io)); 339 340 // The last key needs to be pressed during the current frame 341 io.press(KeyboardKey.w); 342 343 assert(singleKey.isActive(io)); 344 assert(stroke.isActive(io)); 345 346 io.release(KeyboardKey.w); 347 348 assert(!singleKey.isActive(io)); 349 assert(!stroke.isActive(io)); 350 351 } 352 353 /// Mouse actions are activated on release 354 unittest { 355 356 auto stroke = InputStroke(KeyboardKey.leftControl, MouseButton.left); 357 auto io = new HeadlessBackend; 358 359 assert(!stroke.isActive(io)); 360 361 io.press(KeyboardKey.leftControl); 362 io.press(MouseButton.left); 363 364 assert(!stroke.isActive(io)); 365 366 io.release(MouseButton.left); 367 368 assert(stroke.isActive(io)); 369 370 // The action won't trigger if previous keys aren't held down 371 io.release(KeyboardKey.leftControl); 372 373 assert(!stroke.isActive(io)); 374 375 } 376 377 /// Check if the given is held down. 378 static bool isItemDown(const FluidBackend backend, Item item) { 379 380 return item.match!( 381 382 // Keyboard 383 (KeyboardKey key) => backend.isDown(key), 384 385 // A released mouse button also counts as down for our purposes, as it might trigger the action 386 (MouseButton button) => backend.isDown(button) || backend.isReleased(button), 387 388 // Gamepad 389 (GamepadButton button) => backend.isDown(button) != 0 390 ); 391 392 } 393 394 /// Check if the given item is triggered. 395 /// 396 /// If the item is a mouse button, it will be triggered on release. If it's a keyboard key or gamepad button, it'll 397 /// be triggered on press. 398 static bool isItemActive(const FluidBackend backend, Item item) { 399 400 return item.match!( 401 (KeyboardKey key) => backend.isPressed(key) || backend.isRepeated(key), 402 (MouseButton button) => backend.isReleased(button), 403 (GamepadButton button) => backend.isPressed(button) || backend.isRepeated(button), 404 ); 405 406 } 407 408 string toString() const { 409 410 return format!"InputStroke(%(%s + %))"(input); 411 412 } 413 414 } 415 416 /// Binding of an input stroke to an input action. 417 struct InputBinding { 418 419 InputActionID action; 420 InputStroke.Item trigger; 421 422 } 423 424 /// A layer groups input bindings by common key modifiers. 425 struct InputLayer { 426 427 InputStroke modifiers; 428 InputBinding[] bindings; 429 430 /// When sorting ascending, the lowest value is given to the InputLayer with greatest number of bindings 431 int opCmp(const InputLayer other) const { 432 433 // You're not going to put 2,147,483,646 modifiers in a single input stroke, are you? 434 return cast(int) (other.modifiers.length - modifiers.length); 435 436 } 437 438 } 439 440 /// This meta-UDA can be attached to an enum, so Fluid would recognize members of said enum as an UDA defining input 441 /// actions. As an UDA, this template should be used without instantiating. 442 /// 443 /// This template also serves to provide unique identifiers for each action type, generated on startup. For example, 444 /// `InputAction!(FluidInputAction.press).id` will have the same value anywhere in the program. 445 /// 446 /// Action types are resolved at compile-time using symbols, so you can supply any `@InputAction`-marked enum defining 447 /// input actions. All built-in enums are defined in `FluidInputAction`. 448 /// 449 /// If the method returns `true`, it is understood that the action has been processed and no more actions will be 450 /// emitted during the frame. If it returns `false`, other actions and keyboardImpl will be tried until any call returns 451 /// `true` or no handlers are left. 452 struct InputAction(alias actionType) 453 if (isInputActionType!actionType) { 454 455 alias type = actionType; 456 457 alias id this; 458 459 /// **The pointer** to `_id` serves as ID of the input actions. 460 /// 461 /// Note: we could be directly getting the address of the ID function itself (`&id`), but it's possible some linkers 462 /// would merge declarations, so we're using `&_id` for safety. Example of such behavior can be achieved using 463 /// `ld.gold` with `--icf=all`. It's possible the linker could be aware we're checking the function address 464 // (`--icf=safe` works correctly), but again, we prefer to play it safe. Alternatively, we could test for this 465 /// behavior when the program starts, but it probably isn't worth it. 466 align(1) 467 private static immutable bool _id; 468 469 static InputActionID id() { 470 471 return InputActionID(typeof(this)()); 472 473 } 474 475 } 476 477 unittest { 478 479 assert(InputAction!(FluidInputAction.press).id == InputAction!(FluidInputAction.press).id); 480 assert(InputAction!(FluidInputAction.press).id != InputAction!(FluidInputAction.entryUp).id); 481 482 // IDs should have the same equality as the enum members, within the same enum 483 // This will not be the case for enum values with explicitly assigned values (but probably should be!) 484 foreach (left; EnumMembers!FluidInputAction) { 485 486 foreach (right; EnumMembers!FluidInputAction) { 487 488 if (left == right) 489 assert(InputAction!left.id == InputAction!right.id); 490 else 491 assert(InputAction!left.id != InputAction!right.id); 492 493 } 494 495 } 496 497 // Enum values don't have to have globally unique 498 @InputAction 499 enum FooActions { 500 action = 0, 501 } 502 503 @InputAction 504 enum BarActions { 505 action = 0, 506 } 507 508 assert(InputAction!(FooActions.action).id == InputAction!(FooActions.action).id); 509 assert(InputAction!(FooActions.action).id != InputAction!(BarActions.action).id); 510 assert(InputAction!(FooActions.action).id != InputAction!(FluidInputAction.press).id); 511 assert(InputAction!(BarActions.action).id != InputAction!(FluidInputAction.press).id); 512 513 } 514 515 @system 516 unittest { 517 518 import std.concurrency; 519 520 // IDs are global across threads 521 auto t0 = InputAction!(FluidInputAction.press).id; 522 523 spawn({ 524 525 ownerTid.send(InputAction!(FluidInputAction.press).id); 526 527 spawn({ 528 529 ownerTid.send(InputAction!(FluidInputAction.press).id); 530 531 }); 532 533 ownerTid.send(receiveOnly!InputActionID); 534 535 ownerTid.send(InputAction!(FluidInputAction.cancel).id); 536 537 }); 538 539 auto t1 = receiveOnly!InputActionID; 540 auto t2 = receiveOnly!InputActionID; 541 542 auto c0 = InputAction!(FluidInputAction.cancel).id; 543 auto c1 = receiveOnly!InputActionID; 544 545 assert(t0 == t1); 546 assert(t1 == t2); 547 548 assert(c0 != t0); 549 assert(c1 != t1); 550 assert(c0 != t1); 551 assert(c1 != t0); 552 553 assert(t0 == t1); 554 555 } 556 557 /// Check if any stroke bound to this action is being held. 558 bool isDown(alias type)(LayoutTree* tree) 559 if (isInputActionType!type) { 560 561 return tree.downActions[].canFind!"a.action == b"(InputActionID.from!type); 562 563 } 564 565 unittest { 566 567 import fluid.space; 568 569 auto io = new HeadlessBackend; 570 auto tree = new LayoutTree(vspace(), io); 571 572 // Nothing pressed, action not activated 573 assert(!tree.isDown!(FluidInputAction.backspaceWord)); 574 575 io.press(KeyboardKey.leftControl); 576 io.press(KeyboardKey.backspace); 577 tree.poll(); 578 579 // The action is now held down with the ctrl+blackspace stroke 580 assert(tree.isDown!(FluidInputAction.backspaceWord)); 581 582 io.release(KeyboardKey.backspace); 583 io.press(KeyboardKey.w); 584 tree.poll(); 585 586 // ctrl+W also activates the stroke 587 assert(tree.isDown!(FluidInputAction.backspaceWord)); 588 589 io.release(KeyboardKey.leftControl); 590 tree.poll(); 591 592 // Control up, won't match any stroke now 593 assert(!tree.isDown!(FluidInputAction.backspaceWord)); 594 595 } 596 597 /// Check if a mouse stroke bound to this action is being held. 598 bool isMouseDown(alias type)(LayoutTree* tree) 599 if (isInputActionType!type) { 600 601 return tree.downActions[].canFind!(a 602 => a.action == InputActionID.from!type 603 && InputStroke.isMouseItem(a.trigger)); 604 605 } 606 607 unittest { 608 609 import fluid.space; 610 611 auto io = new HeadlessBackend; 612 auto tree = new LayoutTree(vspace(), io); 613 614 assert(!tree.isDown!(FluidInputAction.press)); 615 616 io.press(MouseButton.left); 617 tree.poll(); 618 619 // Pressing with a mouse 620 assert(tree.isDown!(FluidInputAction.press)); 621 assert(tree.isMouseDown!(FluidInputAction.press)); 622 623 io.release(MouseButton.left); 624 tree.poll(); 625 626 // Releasing a mouse key still counts as holding it down 627 // This is important — a released mouse is used to trigger the action 628 assert(tree.isDown!(FluidInputAction.press)); 629 assert(tree.isMouseDown!(FluidInputAction.press)); 630 631 // Need to wait a frame 632 io.nextFrame; 633 tree.poll(); 634 635 assert(!tree.isDown!(FluidInputAction.press)); 636 637 io.press(KeyboardKey.enter); 638 tree.poll(); 639 640 // Pressing with a keyboard 641 assert(tree.isDown!(FluidInputAction.press)); 642 assert(!tree.isMouseDown!(FluidInputAction.press)); 643 644 io.release(KeyboardKey.enter); 645 tree.poll(); 646 647 assert(!tree.isDown!(FluidInputAction.press)); 648 649 } 650 651 /// Check if a keyboard or gamepad stroke bound to this action is being held. 652 bool isFocusDown(alias type)(LayoutTree* tree) 653 if (isInputActionType!type) { 654 655 return tree.downActions[].canFind!(a 656 => a.action == InputActionID.from!type 657 && !InputStroke.isMouseItem(a.trigger)); 658 659 } 660 661 unittest { 662 663 import fluid.space; 664 665 auto io = new HeadlessBackend; 666 auto tree = new LayoutTree(vspace(), io); 667 668 assert(!tree.isDown!(FluidInputAction.press)); 669 670 io.press(KeyboardKey.enter); 671 tree.poll(); 672 673 // Pressing with a keyboard 674 assert(tree.isDown!(FluidInputAction.press)); 675 assert(tree.isFocusDown!(FluidInputAction.press)); 676 677 io.release(KeyboardKey.enter); 678 io.press(MouseButton.left); 679 tree.poll(); 680 681 // Pressing with a mouse 682 assert(tree.isDown!(FluidInputAction.press)); 683 assert(!tree.isFocusDown!(FluidInputAction.press)); 684 685 } 686 687 /// Check if any stroke bound to this action is active. 688 bool isActive(alias type)(LayoutTree* tree) 689 if (isInputActionType!type) { 690 691 return tree.activeActions[].canFind!(a 692 => a.action == InputActionID.from!type); 693 694 } 695 696 /// Check if a mouse stroke bound to this action is active 697 bool isMouseActive(alias type)(LayoutTree* tree) 698 if (isInputActionType!type) { 699 700 return tree.activeActions[].canFind!(a 701 => a.action == InputActionID.from!type 702 && InputStroke.isMouseItem(a.trigger)); 703 704 } 705 706 /// Check if a keyboard or gamepad stroke bound to this action is active. 707 bool isFocusActive(alias type)(LayoutTree* tree) 708 if (isInputActionType!type) { 709 710 return tree.activeActions[].canFind!(a 711 => a.action == InputActionID.from!type 712 && !InputStroke.isMouseItem(a.trigger)); 713 714 } 715 716 /// An interface to be implemented by all nodes that can perform actions when hovered (eg. on click) 717 interface FluidHoverable { 718 719 /// Handle mouse input on the node. 720 void mouseImpl(); 721 722 /// Check if the node is disabled. `mixin makeHoverable` to implement. 723 ref inout(bool) isDisabled() inout; 724 725 /// Check if the node is hovered. 726 bool isHovered() const; 727 728 /// Get the underlying node. 729 final inout(Node) asNode() inout { 730 731 return cast(inout Node) this; 732 733 } 734 735 /// Handle input actions. This function is called by `runInputAction` and can be overriden to preprocess input 736 /// actions in some cases. 737 /// 738 /// Example: Override a specific action by running a different input action. 739 /// 740 /// --- 741 /// override bool inputActionImpl(InputActionID id, bool active) { 742 /// 743 /// if (active && id == InputActionID.from!(FluidInputAction.press)) { 744 /// 745 /// return runInputAction!(FluidInputAction.press); 746 /// 747 /// } 748 /// 749 /// return false; 750 /// 751 /// } 752 /// --- 753 /// 754 /// Params: 755 /// id = ID of the action to run. 756 /// active = Actions trigger many times while the corresponding key or button is held down, but usually only one 757 /// of these triggers is interesting — in which case this value will be `true`. This trigger will be the one 758 /// that runs all UDA handler functions. 759 /// Returns: 760 /// * `true` if the handler took care of the action; processing of the action will finish. 761 /// * `false` if the action should be handled by the default input action handler. 762 bool inputActionImpl(InputActionID id, bool active); 763 764 /// Run input actions. 765 /// 766 /// Use `mixin enableInputActions` to implement. 767 /// 768 /// Manual implementation is discouraged; override `inputActionImpl` instead. 769 bool runInputAction(InputActionID action, bool active = true); 770 771 final bool runInputAction(alias action)(bool active = true) { 772 773 return runInputAction(InputActionID.from!action, active); 774 775 } 776 777 /// Run mouse input actions for the node. 778 /// 779 /// Internal. `Node` calls this for the focused node every frame, falling back to `mouseImpl` if this returns 780 /// false. 781 final bool runMouseInputActions() { 782 783 return runInputActionsImpl(true); 784 785 } 786 787 private final bool runInputActionsImpl(bool mouse) { 788 789 auto tree = asNode.tree; 790 bool handled; 791 792 // Run all active actions 793 if (!mouse || isHovered) 794 foreach_reverse (binding; tree.activeActions[]) { 795 796 if (InputStroke.isMouseItem(binding.trigger) != mouse) continue; 797 798 handled = runInputAction(binding.action, true) || handled; 799 800 // Stop once handled 801 if (handled) break; 802 803 } 804 805 // Run all "while down" actions 806 foreach (binding; tree.downActions[]) { 807 808 if (InputStroke.isMouseItem(binding.trigger) != mouse) continue; 809 810 handled = runInputAction(binding.action, false) || handled; 811 812 } 813 814 return handled; 815 816 } 817 818 mixin template makeHoverable() { 819 820 import fluid.node; 821 import std.format; 822 823 static assert(is(typeof(this) : Node), format!"%s : FluidHoverable must inherit from a Node"(typeid(this))); 824 825 override ref inout(bool) isDisabled() inout { 826 827 return super.isDisabled; 828 829 } 830 831 } 832 833 mixin template enableInputActions() { 834 835 import std.string; 836 import std.traits; 837 import fluid.node; 838 import fluid.input; 839 840 static assert(is(typeof(this) : Node), 841 format!"%s : FluidHoverable must inherit from Node"(typeid(this))); 842 843 // For some reason, a simple alias to FluidHoverable.runInputAction doesn't work 844 final bool runInputAction(alias action)(bool active = true) { 845 846 return runInputAction(InputActionID.from!action, active); 847 848 } 849 850 // Provide a default implementation of inputActionImpl 851 static if (!is(typeof(super) : FluidHoverable)) 852 bool inputActionImpl(InputActionID id, bool active) { 853 854 return false; 855 856 } 857 858 override bool runInputAction(InputActionID action, bool active = true) { 859 860 return runInputActionImpl(action, active); 861 862 } 863 864 bool runInputActionImpl(this This)(InputActionID action, bool active = true) { 865 866 import std.meta : Filter; 867 868 // The programmer may override the action 869 if (inputActionImpl(action, active)) return true; 870 871 bool handled; 872 873 // Check each member 874 static foreach (memberName; __traits(allMembers, This)) { 875 876 static if (!__traits(isDeprecated, __traits(getMember, This, memberName))) 877 static foreach (overload; __traits(getOverloads, This, memberName)) {{ 878 879 // Make sure no method is marked `@InputAction`, that's invalid usage 880 alias inputActionUDAs = getUDAs!(overload, InputAction); 881 882 // Check for `@whileDown` 883 enum activateWhileDown = hasUDA!(overload, fluid.input.whileDown); 884 885 static assert(inputActionUDAs.length == 0, 886 format!"Please use @(%s) instead of @InputAction!(%1$s)"(inputActionUDAs[0].type)); 887 888 // Find the matching action 889 static foreach (actionType; __traits(getAttributes, overload)) 890 static if (isInputActionType!actionType) 891 if (InputActionID.from!actionType == action) {{ 892 893 // Run the action if the stroke was performed 894 if (activateWhileDown || active) { 895 896 // Pass the action type if applicable 897 static if (__traits(compiles, overload(actionType))) { 898 899 // Run the action and mark as handled 900 static if (is(typeof(overload(actionType)) == void)) { 901 902 overload(actionType); 903 handled = true; 904 905 } 906 907 else handled = overload(actionType); 908 909 } 910 911 // TODO Support action ID? 912 913 // Run empty 914 else static if (is(typeof(overload()) == void)) { 915 916 overload(); 917 handled = true; 918 919 } 920 921 else handled = overload(); 922 923 } 924 925 }} 926 927 }} 928 929 } 930 931 return handled; 932 933 } 934 935 } 936 937 } 938 939 /// Interface for container nodes that support dropping other nodes inside. 940 interface FluidDroppable { 941 942 /// Returns true if the given node can be dropped into this node. 943 bool canDrop(Node node); 944 945 /// Called every frame an eligible node is hovering the rectangle. Used to provide feedback while drawing the 946 /// container node. 947 /// Params: 948 /// position = Screen cursor position. 949 /// rectangle = Rectangle used by the node, relative to the droppable. 950 void dropHover(Vector2 position, Rectangle rectangle); 951 952 /// Specifies the given node has been dropped inside the container. 953 /// Params: 954 /// position = Screen cursor position. 955 /// rectangle = Rectangle used by the node, relative to the droppable. 956 /// node = Node that has been dropped. 957 void drop(Vector2 position, Rectangle rectangle, Node node); 958 959 } 960 961 /// An interface to be implemented by all nodes that can take focus. 962 /// 963 /// Note: Input nodes often have many things in common. If you want to create an input-taking node, you're likely better 964 /// off extending from `FluidInput`. 965 interface FluidFocusable : FluidHoverable { 966 967 /// Handle input. Called each frame when focused. 968 bool focusImpl(); 969 970 /// Set focus to this node. 971 /// 972 /// Implementation would usually assign `tree.focus` to self for this to take effect. It is legal, however, for this 973 /// method to redirect the focus to another node (by calling its `focus()` method), or ignore the request. 974 void focus(); 975 976 /// Check if this node has focus. Recommended implementation: `return tree.focus is this`. Proxy nodes, such as 977 /// `FluidFilePicker` might choose to return the value of the node they hold. 978 bool isFocused() const; 979 980 /// Run input actions for the node. 981 /// 982 /// Internal. `Node` calls this for the focused node every frame, falling back to `keyboardImpl` if this returns 983 /// false. 984 final bool runFocusInputActions() { 985 986 return runInputActionsImpl(false); 987 988 } 989 990 } 991 992 /// An interface to be implemented by nodes that accept scroll input. 993 interface FluidScrollable { 994 995 /// Returns true if the node can react to given scroll. 996 /// 997 /// Should return false if the given scroll has no effect, either because it scroll on an unsupported axis, or 998 /// because the axis is currently maxed out. 999 bool canScroll(Vector2 value) const; 1000 1001 /// React to scroll wheel input. 1002 void scrollImpl(Vector2 value); 1003 1004 /// Scroll to given child node. 1005 /// Params: 1006 /// child = Child to scroll to. 1007 /// parentBox = Outer box of this node (the scrollable). 1008 /// childBox = Outer box of the child node (the target). 1009 /// Returns: 1010 /// New rectangle for the childBox. 1011 Rectangle shallowScrollTo(const Node child, Rectangle parentBox, Rectangle childBox); 1012 1013 /// Get current scroll value. 1014 float scroll() const; 1015 1016 /// Set scroll value. 1017 float scroll(float value); 1018 1019 } 1020 1021 /// Represents a general input node. 1022 abstract class InputNode(Parent : Node) : Parent, FluidFocusable { 1023 1024 mixin makeHoverable; 1025 mixin enableInputActions; 1026 1027 /// Callback to run when the input value is altered. 1028 void delegate() changed; 1029 1030 /// Callback to run when the input is submitted. 1031 void delegate() submitted; 1032 1033 this(T...)(T sup) { 1034 1035 super(sup); 1036 1037 } 1038 1039 /// Handle mouse input if no input action did. 1040 /// 1041 /// Usually, you'd prefer to define a method marked with an `InputAction` enum. This function is preferred for more 1042 /// advanced usage. 1043 /// 1044 /// Only one node can run its `mouseImpl` callback per frame, specifically, the last one to register its input. 1045 /// This is to prevent parents or overlapping children to take input when another node is drawn on top. 1046 protected override void mouseImpl() { } 1047 1048 protected bool keyboardImpl() { 1049 1050 return false; 1051 1052 } 1053 1054 /// Handle keyboard and gamepad input if no input action did. 1055 /// 1056 /// Usually, you'd prefer to define a method marked with an `InputAction` enum. This function is preferred for more 1057 /// advanced usage. 1058 /// 1059 /// This will be called each frame as long as this node has focus, unless an `InputAction` was triggered first. 1060 /// 1061 /// Returns: True if the input was handled, false if not. 1062 override bool focusImpl() { 1063 1064 return keyboardImpl(); 1065 1066 } 1067 1068 /// Check if the node is being pressed. Performs action lookup. 1069 /// 1070 /// This is a helper for nodes that might do something when pressed, for example, buttons. 1071 protected bool checkIsPressed() { 1072 1073 return (isHovered && tree.isMouseDown!(FluidInputAction.press)) 1074 || (isFocused && tree.isFocusDown!(FluidInputAction.press)); 1075 1076 } 1077 1078 /// Change the focus to this node. 1079 void focus() { 1080 1081 import fluid.actions; 1082 1083 // Ignore if disabled 1084 if (isDisabled) return; 1085 1086 // Switch the scroll 1087 tree.focus = this; 1088 1089 // Ensure this node gets focus 1090 this.scrollIntoView(); 1091 1092 } 1093 1094 @property { 1095 1096 /// Check if the node has focus. 1097 bool isFocused() const { 1098 1099 return tree.focus is this; 1100 1101 } 1102 1103 /// Set or remove focus from this node. 1104 bool isFocused(bool enable) { 1105 1106 if (enable) focus(); 1107 else if (isFocused) tree.focus = null; 1108 1109 return enable; 1110 1111 } 1112 1113 } 1114 1115 } 1116 1117 unittest { 1118 1119 import fluid.label; 1120 1121 // This test checks triggering and running actions bound via UDAs, including reacting to keyboard and mouse input. 1122 1123 int pressCount; 1124 int cancelCount; 1125 1126 auto io = new HeadlessBackend; 1127 auto root = new class InputNode!Label { 1128 1129 @safe: 1130 1131 mixin enableInputActions; 1132 1133 this() { 1134 super(""); 1135 } 1136 1137 override void resizeImpl(Vector2 space) { 1138 1139 minSize = Vector2(10, 10); 1140 1141 } 1142 1143 @(FluidInputAction.press) 1144 void _pressed() { 1145 1146 pressCount++; 1147 1148 } 1149 1150 @(FluidInputAction.cancel) 1151 void _cancelled() { 1152 1153 cancelCount++; 1154 1155 } 1156 1157 }; 1158 1159 root.io = io; 1160 root.theme = nullTheme; 1161 root.focus(); 1162 1163 // Press the node via focus 1164 io.press(KeyboardKey.enter); 1165 1166 root.draw(); 1167 1168 assert(root.tree.isFocusActive!(FluidInputAction.press)); 1169 assert(pressCount == 1); 1170 1171 io.nextFrame; 1172 1173 // Holding shouldn't trigger the callback multiple times 1174 root.draw(); 1175 1176 assert(pressCount == 1); 1177 1178 // Hover the node and press it with the mouse 1179 io.nextFrame; 1180 io.release(KeyboardKey.enter); 1181 io.mousePosition = Vector2(5, 5); 1182 io.press(MouseButton.left); 1183 1184 root.draw(); 1185 root.tree.focus = null; 1186 1187 // This shouldn't be enough to activate the action 1188 assert(pressCount == 1); 1189 1190 // If we now drag away from the button and release... 1191 io.nextFrame; 1192 io.mousePosition = Vector2(15, 15); 1193 io.release(MouseButton.left); 1194 1195 root.draw(); 1196 1197 // ...the action shouldn't trigger 1198 assert(pressCount == 1); 1199 1200 // But if we release the mouse on the button 1201 io.nextFrame; 1202 io.mousePosition = Vector2(5, 5); 1203 io.release(MouseButton.left); 1204 1205 root.draw(); 1206 1207 assert(pressCount == 2); 1208 assert(cancelCount == 0); 1209 1210 // Focus the node again 1211 root.focus(); 1212 1213 // Press escape to cancel 1214 io.nextFrame; 1215 io.press(KeyboardKey.escape); 1216 1217 root.draw(); 1218 1219 assert(pressCount == 2); 1220 assert(cancelCount == 1); 1221 1222 } 1223 1224 unittest { 1225 1226 import fluid.space; 1227 import fluid.button; 1228 1229 // This test checks if "hover slipping" happens; namely, if the user clicks and holds on an object, then hovers on 1230 // something else and releases, the click should be cancelled, and no other object should react to the same click. 1231 1232 class SquareButton : Button { 1233 1234 mixin enableInputActions; 1235 1236 this(T...)(T t) { 1237 super(t); 1238 } 1239 1240 override void resizeImpl(Vector2) { 1241 minSize = Vector2(10, 10); 1242 } 1243 1244 } 1245 1246 int[2] pressCount; 1247 SquareButton[2] buttons; 1248 1249 auto io = new HeadlessBackend; 1250 auto root = hspace( 1251 .nullTheme, 1252 buttons[0] = new SquareButton("", delegate { pressCount[0]++; }), 1253 buttons[1] = new SquareButton("", delegate { pressCount[1]++; }), 1254 ); 1255 1256 root.io = io; 1257 1258 // Press the left button 1259 io.mousePosition = Vector2(5, 5); 1260 io.press(MouseButton.left); 1261 1262 root.draw(); 1263 1264 // Release it 1265 io.release(MouseButton.left); 1266 1267 root.draw(); 1268 1269 assert(root.tree.hover is buttons[0]); 1270 assert(pressCount == [1, 0], "Left button should trigger"); 1271 1272 // Press the right button 1273 io.nextFrame; 1274 io.mousePosition = Vector2(15, 5); 1275 io.press(MouseButton.left); 1276 1277 root.draw(); 1278 1279 // Release it 1280 io.release(MouseButton.left); 1281 1282 root.draw(); 1283 1284 assert(pressCount == [1, 1], "Right button should trigger"); 1285 1286 // Press the left button, but don't release 1287 io.nextFrame; 1288 io.mousePosition = Vector2(5, 5); 1289 io.press(MouseButton.left); 1290 1291 root.draw(); 1292 1293 assert( buttons[0].isPressed); 1294 assert(!buttons[1].isPressed); 1295 1296 // Move the cursor over the right button 1297 io.nextFrame; 1298 io.mousePosition = Vector2(15, 5); 1299 1300 root.draw(); 1301 1302 // Left button should have tree-scope hover, but isHovered status is undefined. At the time of writing, only the 1303 // right button will be isHovered and neither will be isPressed. 1304 // 1305 // TODO It might be a good idea to make neither isHovered. Consider new condition: 1306 // 1307 // (_isHovered && tree.hover is this && !_isDisabled && !tree.isBranchDisabled) 1308 // 1309 // This should also fix having two nodes visually hovered in case they overlap. 1310 // 1311 // Other frameworks might retain isPressed status on the left button, but it might good idea to keep current 1312 // behavior as a visual clue it wouldn't trigger. 1313 assert(root.tree.hover is buttons[0]); 1314 1315 // Release the button on the next frame 1316 io.nextFrame; 1317 io.release(MouseButton.left); 1318 1319 root.draw(); 1320 1321 assert(pressCount == [1, 1], "Neither button should trigger on lost hover"); 1322 1323 // Things should go to normal next frame 1324 io.nextFrame; 1325 io.press(MouseButton.left); 1326 1327 root.draw(); 1328 1329 // So we can expect the right button to trigger now 1330 io.nextFrame; 1331 io.release(MouseButton.left); 1332 1333 root.draw(); 1334 1335 assert(root.tree.hover is buttons[1]); 1336 assert(pressCount == [1, 2]); 1337 1338 }