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