1 module fluid.tree;
2 
3 import std.conv;
4 import std.math;
5 import std.container;
6 import std.algorithm;
7 
8 import fluid.node;
9 import fluid.input;
10 import fluid.style;
11 import fluid.backend;
12 
13 
14 @safe:
15 
16 
17 version (OSX)
18     version = Fluid_MacKeyboard;
19 
20 ///
21 struct FocusDirection {
22 
23     struct WithPriority {
24 
25         /// Pick priority based on tree distance from the focused node.
26         int priority;
27 
28         /// Square of the distance between this node and the focused node.
29         float distance2;
30 
31         /// The node.
32         FluidFocusable node;
33 
34         alias node this;
35 
36     }
37 
38     /// Available space box of the focused item after last frame.
39     Rectangle lastFocusBox;
40 
41     /// Nodes that may get focus with tab navigation.
42     FluidFocusable previous, next;
43 
44     /// First and last focusable nodes in the tree.
45     FluidFocusable first, last;
46 
47     /// Focusable nodes, by direction from the focused node.
48     WithPriority[4] positional;
49 
50     /// Focus priority for the currently drawn node.
51     ///
52     /// Increased until the focused node is found, decremented afterwards. As a result, values will be the highest for
53     /// nodes near the focused one. Changes with tree depth rather than individual nodes.
54     int priority;
55 
56     private {
57 
58         /// Value `prioerity` is summed with on each step. `1` before finding the focused node, `-1` after.
59         int priorityDirection = 1;
60 
61         /// Current tree depth.
62         uint depth;
63 
64     }
65 
66     /// Update focus info with the given node. Automatically called when a node is drawn, shouldn't be called manually.
67     ///
68     /// `previous` will be the last focusable node encountered before the focused node, and `next` will be the first one
69     /// after. `first` and `last will be the last focusable nodes in the entire tree.
70     ///
71     /// Params:
72     ///     current = Node to update the focus info with.
73     ///     box     = Box defining node boundaries (focus box)
74     ///     depth   = Current tree depth. Pass in `tree.depth`.
75     void update(Node current, Rectangle box, uint depth)
76     in (current !is null, "Current node must not be null")
77     do {
78 
79         import std.algorithm : either;
80 
81         auto currentFocusable = cast(FluidFocusable) current;
82 
83         // Count focus priority
84         {
85 
86             // Get depth difference since last time
87             const int depthDiff = depth - this.depth;
88 
89             // Count steps in change of depth
90             priority += priorityDirection * abs(depthDiff);
91 
92             // Update depth
93             this.depth = depth;
94 
95         }
96 
97         // Stop if the current node can't take focus
98         if (!currentFocusable) return;
99 
100         // And it DOES have focus
101         if (current.tree.focus is currentFocusable) {
102 
103             // Mark the node preceding it to the last encountered focusable node
104             previous = last;
105 
106             // Clear the next node, so it can be overwritten by a correct value.
107             next = null;
108 
109             // Reverse priority target
110             priorityDirection = -1;
111 
112         }
113 
114         else {
115 
116             // Update positional focus
117             updatePositional(currentFocusable, box);
118 
119             // There's no node to take focus next, set it now
120             if (next is null) next = currentFocusable;
121 
122         }
123 
124 
125         // Set the current node as the first focusable, if true
126         if (first is null) first = currentFocusable;
127 
128         // Replace the last
129         last = currentFocusable;
130 
131     }
132 
133     /// Check the given node's position and update `positional` to match.
134     private void updatePositional(FluidFocusable node, Rectangle box) {
135 
136         // Note: This might give false-positives if the focused node has changed during this frame
137 
138         // Check each direction
139         foreach (i, ref otherNode; positional) {
140 
141             const side = cast(Style.Side) i;
142             const dist = distance2(box, side);
143 
144             // If a node took this spot before
145             if (otherNode !is null) {
146 
147                 // Ignore if the other node has higher priority
148                 if (otherNode.priority > priority) continue;
149 
150                 // If priorities are equal, check if we're closer than the other node
151                 if (otherNode.priority == priority
152                     && otherNode.distance2 < dist) continue;
153 
154             }
155 
156             // Check if this node matches the direction
157             if (checkDirection(box, side)) {
158 
159                 // Replace the node
160                 otherNode = WithPriority(priority, dist, node);
161 
162             }
163 
164         }
165 
166     }
167 
168     /// Check if the given box is located to the given side of the focus box.
169     bool checkDirection(Rectangle box, Style.Side side) {
170 
171         // Distance between box sides facing each other.
172         //
173         // ↓ lastFocusBox  ↓ box
174         // +======+        +------+
175         // |      |        |      |
176         // |      | ~~~~~~ |      |
177         // |      |        |      |
178         // +======+        +------+
179         //   side ↑        ↑ side.reverse
180         const distanceExternal = lastFocusBox.getSide(side) - box.getSide(side.reverse);
181 
182         // Distance between corresponding box sides.
183         //
184         // ↓ lastFocusBox  ↓ box
185         // +======+        +------+
186         // |      |        :      |
187         // |      | ~~~~~~~~~~~~~ |
188         // |      |        :      |
189         // +======+        +------+
190         //   side ↑          side ↑
191         const distanceInternal = lastFocusBox.getSide(side) - box.getSide(side);
192 
193         // The condition for the return value to be true, is for distanceInternal to be greater than distanceExternal.
194         // This is not the case in the opposite situation.
195         //
196         // For example, if we're checking if the box is on the *right* of lastFocusBox:
197         //
198         // trueish scenario:                                 falseish scenario:
199         // Box is to the right of lastFocusBox               Box is the left of lastFocusBox
200         //
201         // ↓ lastFocusBox  ↓ box                             ↓ box           ↓ lastFocusBox
202         // +======+        +------+                          +------+        +======+
203         // |      | ~~~~~~ :      | external                 | ~~~~~~~~~~~~~~~~~~~~ | external
204         // |      |        :      |    <                     |      :        :      |    >
205         // |      | ~~~~~~~~~~~~~ | internal                 |      : ~~~~~~~~~~~~~ | internal
206         // +======+        +------+                          +------+        +======+
207         //   side ↑        ↑ side.reverse                      side ↑          side ↑
208         const condition = abs(distanceInternal) > abs(distanceExternal);
209 
210         // ↓ box                    There is an edgecase though. If one box entirely overlaps the other on one axis, we
211         // +--------------------+   might end up with unwanted behavior, for example, in a ScrollFrame, focus might
212         // |   ↓ lastFocusBox   |   switch to the scrollbar instead of a child, as we would normally expect.
213         // |   +============+   |
214         // |   |            |   |   For this reason, we require both `distanceInternal` and `distanceExternal` to have
215         // +---|            |---+   the same sign, as it normally would, but not here.
216         //     |            |
217         //     +============+       One can still navigate to the `box` using controls for the other axis.
218         return condition
219             && distanceInternal * distanceExternal >= 0;
220 
221     }
222 
223     /// Get the square of the distance between given box and `lastFocusBox`.
224     float distance2(Rectangle box, Style.Side side) {
225 
226         /// Get the center of given rectangle on the axis opposite to the results of getSide.
227         float center(Rectangle rect) {
228 
229             return side == Style.Side.left || side == Style.Side.right
230                 ? rect.y + rect.height
231                 : rect.x + rect.width;
232 
233         }
234 
235         // Distance between box sides facing each other, see `checkDirection`
236         const distanceExternal = lastFocusBox.getSide(side) - box.getSide(side.reverse);
237 
238         /// Distance between centers of the boxes on the other axis
239         const distanceOpposite = center(box) - center(lastFocusBox);
240 
241         return distanceExternal^^2 + distanceOpposite^^2;
242 
243     }
244 
245 }
246 
247 /// A class for iterating over the node tree.
248 abstract class TreeAction {
249 
250     public {
251 
252         /// Node to descend into; `beforeDraw` and `afterDraw` will only be emitted for this node and its children.
253         ///
254         /// May be null to enable iteration over the entire tree.
255         Node startNode;
256 
257         /// If true, this action is complete and no callbacks should be ran.
258         ///
259         /// Overloads of the same callbacks will still be called for the event that prompted stopping.
260         bool toStop;
261 
262     }
263 
264     private {
265 
266         /// Set to true once the action has descended into `startNode`.
267         bool startNodeFound;
268 
269     }
270 
271     /// Stop the action
272     final void stop() {
273 
274         toStop = true;
275 
276     }
277 
278     /// Called before the tree is drawn. Keep in mind this might not be called if the action is started when tree
279     /// iteration has already begun.
280     /// Params:
281     ///     root     = Root of the tree.
282     ///     viewport = Screen space for the node.
283     void beforeTree(Node root, Rectangle viewport) { }
284 
285     /// Called before a node is resized.
286     void beforeResize(Node node, Vector2 viewportSpace) { }
287 
288     /// Called before each `drawImpl` call of any node in the tree, so supplying parent nodes before their children.
289     ///
290     /// This might not be called if the node is offscreen. If you need to find all nodes, try `beforeResize`.
291     ///
292     /// Params:
293     ///     node       = Node that's about to be drawn.
294     ///     space      = Space given for the node.
295     ///     paddingBox = Padding box of the node.
296     ///     contentBox = Content box of teh node.
297     void beforeDraw(Node node, Rectangle space, Rectangle paddingBox, Rectangle contentBox) { }
298 
299     /// ditto
300     void beforeDraw(Node node, Rectangle space) { }
301 
302     /// internal
303     final package void beforeDrawImpl(Node node, Rectangle space, Rectangle paddingBox, Rectangle contentBox) {
304 
305         // There is a start node set
306         if (startNode !is null) {
307 
308             // Check if we're descending into its branch
309             if (node is startNode) startNodeFound = true;
310 
311             // Continue only if it was found
312             else if (!startNodeFound) return;
313 
314         }
315 
316         // Call the hooks
317         beforeDraw(node, space, paddingBox, contentBox);
318         beforeDraw(node, space);
319 
320     }
321 
322     /// Called after each `drawImpl` call of any node in the tree, so supplying children nodes before their parents.
323     ///
324     /// This might not be called if the node is offscreen. If you need to find all nodes, try `beforeResize`.
325     ///
326     /// Params:
327     ///     node       = Node that's about to be drawn.
328     ///     space      = Space given for the node.
329     ///     paddingBox = Padding box of the node.
330     ///     contentBox = Content box of teh node.
331     void afterDraw(Node node, Rectangle space, Rectangle paddingBox, Rectangle contentBox) { }
332 
333     /// ditto
334     void afterDraw(Node node, Rectangle space) { }
335 
336     /// internal
337     final package void afterDrawImpl(Node node, Rectangle space, Rectangle paddingBox, Rectangle contentBox) {
338 
339         // There is a start node set
340         if (startNode !is null) {
341 
342             // Check if we're leaving the node
343             if (node is startNode) startNodeFound = false;
344 
345             // Continue only if it was found
346             else if (!startNodeFound) return;
347             // Note: We still emit afterDraw for that node, hence `else if`
348 
349         }
350 
351         afterDraw(node, space, paddingBox, contentBox);
352         afterDraw(node, space);
353     }
354 
355     /// Called after the tree is drawn. Called before input events, so they can assume actions have completed.
356     ///
357     /// By default, calls `stop()` preventing the action from evaluating during next draw.
358     void afterTree() {
359 
360         stop();
361 
362     }
363 
364     /// Hook that triggers after processing input. Useful if post-processing is necessary to, perhaps, implement
365     /// fallback input.
366     ///
367     /// Warning: This will **not trigger** unless `afterTree` is overrided not to stop the action. If you make use of
368     /// this, make sure to make the action stop in this method.
369     ///
370     /// Params:
371     ///     keyboardHandled = If true, keyboard input was handled. Passed by reference, so if you react to input, change
372     ///         this to true.
373     void afterInput(ref bool keyboardHandled) { }
374 
375 }
376 
377 /// Global data for the layout tree.
378 struct LayoutTree {
379 
380     import fluid.theme : Breadcrumbs;
381 
382     // Nodes
383     public {
384 
385         /// Root node of the tree.
386         Node root;
387 
388         /// Top-most hovered node in the tree.
389         Node hover;
390 
391         /// Currently focused node.
392         ///
393         /// Changing this value directly is discouraged. Some nodes might not want the focus! Be gentle, call
394         /// `FluidFocusable.focus()` instead and let the node set the value on its own.
395         FluidFocusable focus;
396 
397         /// Deepest hovered scrollable node.
398         FluidScrollable scroll;
399 
400     }
401 
402     // Input
403     public {
404 
405         /// Focus direction data.
406         FocusDirection focusDirection;
407 
408         /// Padding box of the currently focused node. Only available after the node has been drawn.
409         ///
410         /// See_also: `focusDirection.lastFocusBox`.
411         Rectangle focusBox;
412 
413         /// Tree actions queued to execute during next draw.
414         DList!TreeAction actions;
415 
416         /// Input strokes bound to emit given action signals.
417         ///
418         /// Input layers have to be sorted.
419         InputLayer[] boundInputs;
420 
421         invariant(boundInputs.isSorted);
422 
423         /// Actions that are currently held down.
424         DList!InputBinding downActions;
425 
426         /// Actions that have just triggered.
427         DList!InputBinding activeActions;
428 
429         /// Access to core input and output facilities.
430         FluidBackend backend;
431         alias io = backend;
432 
433         /// Check if keyboard input was handled; updated after rendering has completed.
434         bool keyboardHandled;
435 
436     }
437 
438     /// Miscelleanous, technical properties.
439     public {
440 
441         /// Current node drawing depth.
442         uint depth;
443 
444         /// Current rectangle drawing is limited to.
445         Rectangle scissors;
446 
447         /// True if the current tree branch is marked as disabled (doesn't take input).
448         bool isBranchDisabled;
449 
450         /// Current breadcrumbs. These are assigned to any node that is resized or drawn at the time.
451         ///
452         /// Any node that introduces its own breadcrumbs will push onto this stack, and pop once finished.
453         Breadcrumbs breadcrumbs;
454 
455     }
456 
457     /// Incremented for every `filterActions` access to prevent nested accesses from breaking previously made ranges.
458     private int _actionAccessCounter;
459 
460     /// Create a new tree with the given node as its root, and using the given backend for I/O.
461     this(Node root, FluidBackend backend) {
462 
463         this.root = root;
464         this.backend = backend;
465         this.restoreDefaultInputBinds();
466 
467     }
468 
469     /// Create a new tree with the given node as its root. Use the default backend, if any is present.
470     this(Node root) {
471 
472         this(root, defaultFluidBackend);
473 
474         assert(backend, "Cannot create LayoutTree; no backend was chosen, and no default is set.");
475 
476     }
477 
478     /// Returns true if this branch requested a resize or is pending a resize.
479     bool resizePending() const {
480 
481         return root.resizePending;
482 
483     }
484 
485     /// Queue an action to perform while iterating the tree.
486     ///
487     /// Avoid using this; most of the time `Node.queueAction` is what you want. `LayoutTree.queueAction` might fire
488     /// too early
489     void queueAction(TreeAction action)
490     in (action, "Invalid action queued")
491     do {
492 
493         actions ~= action;
494 
495     }
496 
497     /// Restore defaults for given actions.
498     void restoreDefaultInputBinds() {
499 
500         /// Get the ID of an input action.
501         auto bind(alias a, T)(T arg) {
502 
503             return InputBinding(InputAction!a.id, InputStroke.Item(arg));
504 
505         }
506 
507         with (FluidInputAction) {
508 
509             // System-independent keys
510             auto universalShift = InputLayer(
511                 InputStroke(KeyboardKey.leftShift),
512                 [
513                     bind!focusPrevious(KeyboardKey.tab),
514                     bind!entryPrevious(KeyboardKey.tab),
515                     bind!outdent(KeyboardKey.tab),
516                     bind!selectPreviousChar(KeyboardKey.left),
517                     bind!selectNextChar(KeyboardKey.right),
518                     bind!selectPreviousLine(KeyboardKey.up),
519                     bind!selectNextLine(KeyboardKey.down),
520                     bind!selectToLineStart(KeyboardKey.home),
521                     bind!selectToLineEnd(KeyboardKey.end),
522                     bind!breakLine(KeyboardKey.enter),
523                     bind!contextMenu(KeyboardKey.f10),
524                 ]
525             );
526             auto universal = InputLayer(
527                 InputStroke(),
528                 [
529                     // Press
530                     bind!press(MouseButton.left),
531                     bind!press(KeyboardKey.enter),
532                     bind!press(GamepadButton.cross),
533 
534                     // Submit
535                     bind!submit(KeyboardKey.enter),
536                     bind!submit(GamepadButton.cross),
537 
538                     // Cancel
539                     bind!cancel(KeyboardKey.escape),
540                     bind!cancel(GamepadButton.circle),
541 
542                     // Menu
543                     bind!contextMenu(MouseButton.right),
544                     bind!contextMenu(KeyboardKey.contextMenu),
545 
546                     // Tabbing; index-focus
547                     bind!focusPrevious(GamepadButton.leftButton),
548                     bind!focusNext(KeyboardKey.tab),
549                     bind!focusNext(GamepadButton.rightButton),
550 
551                     // Directional focus
552                     bind!focusLeft(KeyboardKey.left),
553                     bind!focusLeft(GamepadButton.dpadLeft),
554                     bind!focusRight(KeyboardKey.right),
555                     bind!focusRight(GamepadButton.dpadRight),
556                     bind!focusUp(KeyboardKey.up),
557                     bind!focusUp(GamepadButton.dpadUp),
558                     bind!focusDown(KeyboardKey.down),
559                     bind!focusDown(GamepadButton.dpadDown),
560 
561                     // Text input
562                     bind!backspace(KeyboardKey.backspace),
563                     bind!deleteChar(KeyboardKey.delete_),
564                     bind!breakLine(KeyboardKey.enter),
565                     bind!previousChar(KeyboardKey.left),
566                     bind!nextChar(KeyboardKey.right),
567                     bind!previousLine(KeyboardKey.up),
568                     bind!nextLine(KeyboardKey.down),
569                     bind!entryPrevious(KeyboardKey.up),
570                     bind!entryPrevious(GamepadButton.dpadUp),
571                     bind!entryNext(KeyboardKey.down),
572                     bind!entryNext(KeyboardKey.tab),
573                     bind!entryNext(GamepadButton.dpadDown),
574                     bind!toLineStart(KeyboardKey.home),
575                     bind!toLineEnd(KeyboardKey.end),
576                     bind!insertTab(KeyboardKey.tab),
577 
578                     // Scrolling
579                     bind!scrollLeft(KeyboardKey.left),
580                     bind!scrollLeft(GamepadButton.dpadLeft),
581                     bind!scrollRight(KeyboardKey.right),
582                     bind!scrollRight(GamepadButton.dpadRight),
583                     bind!scrollUp(KeyboardKey.up),
584                     bind!scrollUp(GamepadButton.dpadUp),
585                     bind!scrollDown(KeyboardKey.down),
586                     bind!scrollDown(GamepadButton.dpadDown),
587                     bind!pageUp(KeyboardKey.pageUp),
588                     bind!pageDown(KeyboardKey.pageDown),
589                 ]
590             );
591 
592             // TODO universal left/right key
593             version (Fluid_MacKeyboard)
594                 boundInputs = [
595 
596                     // Shift + Command
597                     InputLayer(
598                         InputStroke(KeyboardKey.leftShift, KeyboardKey.leftSuper),
599                         [
600                             // TODO Command should *expand selection* on macOS instead of current
601                             // toLineStart/toLineEnd behavior
602                             bind!selectToLineStart(KeyboardKey.left),
603                             bind!selectToLineEnd(KeyboardKey.right),
604                             bind!selectToStart(KeyboardKey.up),
605                             bind!selectToEnd(KeyboardKey.down),
606                             bind!redo(KeyboardKey.z),
607                         ]
608                     ),
609 
610                     // Shift + Option
611                     InputLayer(
612                         InputStroke(KeyboardKey.leftShift, KeyboardKey.leftAlt),
613                         [
614                             bind!selectPreviousWord(KeyboardKey.left),
615                             bind!selectNextWord(KeyboardKey.right),
616                         ]
617                     ),
618 
619                     // Command
620                     InputLayer(
621                         InputStroke(KeyboardKey.leftSuper),
622                         [
623                             bind!toLineStart(KeyboardKey.left),
624                             bind!toLineEnd(KeyboardKey.right),
625                             bind!toStart(KeyboardKey.up),
626                             bind!toEnd(KeyboardKey.down),
627                             bind!selectAll(KeyboardKey.a),
628                             bind!copy(KeyboardKey.c),
629                             bind!cut(KeyboardKey.x),
630                             bind!paste(KeyboardKey.v),
631                             bind!undo(KeyboardKey.z),
632                             bind!redo(KeyboardKey.y),
633                             bind!submit(KeyboardKey.enter),
634                         ]
635                     ),
636 
637                     // Option
638                     InputLayer(
639                         InputStroke(KeyboardKey.leftAlt),
640                         [
641                             bind!deleteWord(KeyboardKey.delete_),
642                             bind!backspaceWord(KeyboardKey.backspace),
643                             bind!previousWord(KeyboardKey.left),
644                             bind!nextWord(KeyboardKey.right),
645                         ]
646                     ),
647 
648                     // Control
649                     InputLayer(
650                         InputStroke(KeyboardKey.leftControl),
651                         [
652                             bind!backspaceWord(KeyboardKey.w),  // emacs & vim
653                             bind!entryPrevious(KeyboardKey.k),  // vim
654                             bind!entryPrevious(KeyboardKey.p),  // emacs
655                             bind!entryNext(KeyboardKey.j),  // vim
656                             bind!entryNext(KeyboardKey.n),  // emacs
657                         ]
658                     ),
659 
660                     universalShift,
661                     universal,
662                 ];
663             else
664                 boundInputs = [
665 
666                     InputLayer(
667                         InputStroke(KeyboardKey.leftShift, KeyboardKey.leftControl),
668                         [
669                             bind!selectPreviousWord(KeyboardKey.left),
670                             bind!selectNextWord(KeyboardKey.right),
671                             bind!selectToStart(KeyboardKey.home),
672                             bind!selectToEnd(KeyboardKey.end),
673                             bind!redo(KeyboardKey.z),
674                         ]
675                     ),
676 
677                     InputLayer(
678                         InputStroke(KeyboardKey.leftControl),
679                         [
680                             bind!deleteWord(KeyboardKey.delete_),
681                             bind!backspaceWord(KeyboardKey.backspace),
682                             bind!backspaceWord(KeyboardKey.w),  // emacs & vim
683                             bind!entryPrevious(KeyboardKey.k),  // vim
684                             bind!entryPrevious(KeyboardKey.p),  // emacs
685                             bind!entryNext(KeyboardKey.j),  // vim
686                             bind!entryNext(KeyboardKey.n),  // emacs
687                             bind!previousWord(KeyboardKey.left),
688                             bind!nextWord(KeyboardKey.right),
689                             bind!selectAll(KeyboardKey.a),
690                             bind!copy(KeyboardKey.c),
691                             bind!cut(KeyboardKey.x),
692                             bind!paste(KeyboardKey.v),
693                             bind!undo(KeyboardKey.z),
694                             bind!redo(KeyboardKey.y),
695                             bind!toStart(KeyboardKey.home),
696                             bind!toEnd(KeyboardKey.end),
697 
698                             // Submit with ctrl+enter
699                             bind!submit(KeyboardKey.enter),
700                         ]
701                     ),
702 
703                     InputLayer(
704                         InputStroke(KeyboardKey.leftAlt),
705                         [
706                             bind!entryUp(KeyboardKey.up),
707                         ]
708                     ),
709 
710                     universalShift,
711                     universal,
712 
713                 ];
714 
715         }
716 
717     }
718 
719     /// Remove any inputs bound to given input action.
720     /// Returns: `true` if the action was cleared.
721     bool clearBoundInput(InputActionID action) {
722 
723         import std.array;
724 
725         // TODO test
726 
727         bool found;
728 
729         foreach (ref layer; boundInputs) {
730 
731             const oldLength = layer.bindings.length;
732 
733             layer.bindings = layer.bindings.filter!(a => a.action == action).array;
734 
735             if (layer.bindings.length != oldLength) {
736                 found = true;
737             }
738 
739         }
740 
741         return found;
742 
743     }
744 
745     /// Find a layer for the given input stroke.
746     /// Returns: Layer found for the given input stroke. `null` if none found.
747     inout(InputLayer)* layerForStroke(InputStroke stroke) inout scope return {
748 
749         auto modifiers = stroke.modifiers;
750 
751         foreach (i, layer; boundInputs) {
752 
753             // Found a matching layer
754             if (modifiers == layer.modifiers) {
755 
756                 return &boundInputs[i];
757 
758             }
759 
760             // Stop if other layers are less complex
761             if (modifiers.length > layer.modifiers.length) break;
762 
763         }
764 
765         return null;
766 
767     }
768 
769     /// Bind a key stroke or button to given input action. Multiple key strokes are allowed to match given action.
770     void bindInput(InputActionID action, InputStroke stroke)
771     in (stroke.length != 0)
772     do {
773 
774         // TODO tests
775 
776         auto binding = InputBinding(action, stroke.input[$-1]);
777 
778         // Layer exists, add the binding
779         if (auto layer = layerForStroke(stroke)) {
780 
781             layer.bindings ~= binding;
782 
783         }
784 
785         // Layer doesn't exist, create it
786         else {
787 
788             auto modifiers = stroke.modifiers;
789             auto newLayer = InputLayer(modifiers, [binding]);
790             bool found;
791 
792             // Insert the layer before any layer that is less complex
793             foreach (i, layer; boundInputs) {
794 
795                 if (modifiers.length > layer.modifiers.length) {
796 
797                     boundInputs = boundInputs[0..i] ~ newLayer ~ boundInputs[i..$];
798                     found = true;
799                     break;
800 
801                 }
802 
803             }
804 
805             if (!found) boundInputs ~= newLayer;
806 
807             assert(isSorted(boundInputs));
808 
809         }
810 
811     }
812 
813     /// Bind a key stroke or button to given input action, replacing any previously bound inputs.
814     void bindInputReplace(InputActionID action, InputStroke stroke)
815     in (stroke.length != 0)
816     do {
817 
818         import std.array;
819 
820         // Find a matching layer
821         if (auto layer = layerForStroke(stroke)) {
822 
823             // Remove any stroke that matches
824             layer.bindings = layer.bindings.filter!(a => a.trigger == stroke.input[$-1]).array;
825 
826             // Insert the binding
827             layer.bindings ~= InputBinding(action, stroke.input[$-1]);
828 
829         }
830 
831         // Layer doesn't exist, bind it the straightforward way
832         else bindInput(action, stroke);
833 
834     }
835 
836     /// List actions in the tree, remove finished actions while iterating.
837     auto filterActions() {
838 
839         struct ActionIterator {
840 
841             LayoutTree* tree;
842 
843             int opApply(int delegate(TreeAction) @safe fun) {
844 
845                 tree._actionAccessCounter++;
846                 scope (exit) tree._actionAccessCounter--;
847 
848                 // Regular access
849                 if (tree._actionAccessCounter == 1) {
850 
851                     for (auto range = tree.actions[]; !range.empty; ) {
852 
853                         // Yield the item
854                         auto result = fun(range.front);
855 
856                         // If finished, remove from the queue
857                         if (range.front.toStop) tree.actions.popFirstOf(range);
858 
859                         // Continue to the next item
860                         else range.popFront();
861 
862                         // Stop iteration if requested
863                         if (result) return result;
864 
865                     }
866 
867                 }
868 
869                 // Nested access
870                 else {
871 
872                     for (auto range = tree.actions[]; !range.empty; ) {
873 
874                         auto front = range.front;
875                         range.popFront();
876 
877                         // Ignore stopped items
878                         if (front.toStop) continue;
879 
880                         // Yield the item
881                         if (auto result = fun(front)) {
882 
883                             return result;
884 
885                         }
886 
887                     }
888 
889                 }
890 
891                 return 0;
892 
893             }
894 
895         }
896 
897         return ActionIterator(&this);
898 
899     }
900 
901     /// Intersect the given rectangle against current scissor area.
902     Rectangle intersectScissors(Rectangle rect) {
903 
904         import std.algorithm : min, max;
905 
906         // No limit applied
907         if (scissors is scissors.init) return rect;
908 
909         Rectangle result;
910 
911         // Intersect
912         result.x = max(rect.x, scissors.x);
913         result.y = max(rect.y, scissors.y);
914         result.w = max(0, min(rect.x + rect.w, scissors.x + scissors.w) - result.x);
915         result.h = max(0, min(rect.y + rect.h, scissors.y + scissors.h) - result.y);
916 
917         return result;
918 
919     }
920 
921     /// Start scissors mode.
922     /// Returns: Previous scissors mode value. Pass that value to `popScissors`.
923     Rectangle pushScissors(Rectangle rect) {
924 
925         const lastScissors = scissors;
926 
927         // Intersect with the current scissors rectangle.
928         io.area = scissors = intersectScissors(rect);
929 
930         return lastScissors;
931 
932     }
933 
934     void popScissors(Rectangle lastScissorsMode) @trusted {
935 
936         // Pop the stack
937         scissors = lastScissorsMode;
938 
939         // No scissors left
940         if (scissors is scissors.init) {
941 
942             // Restore full draw area
943             backend.restoreArea();
944 
945         }
946 
947         else {
948 
949             // Start again
950             backend.area = scissors;
951 
952         }
953 
954     }
955 
956     /// Fetch tree events (e.g. actions)
957     package void poll() {
958 
959         // Run texture reaper
960         io.reaper.check();
961 
962         // Reset all actions
963         downActions.clear();
964         activeActions.clear();
965 
966         // Test all bindings
967         foreach (layer; boundInputs) {
968 
969             // Check if the layer is active
970             if (!layer.modifiers.isDown(backend)) continue;
971 
972             // Found an active layer, test all bound strokes
973             foreach (binding; layer.bindings) {
974 
975                 // Register held-down actions
976                 if (InputStroke.isItemDown(backend, binding.trigger)) {
977 
978                     downActions ~= binding;
979 
980                 }
981 
982                 // Register triggered actions
983                 if (InputStroke.isItemActive(backend, binding.trigger)) {
984 
985                     activeActions ~= binding;
986 
987                 }
988 
989             }
990 
991             // End on this layer
992             break;
993 
994         }
995 
996     }
997 
998 }