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