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