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 }