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