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