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