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