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