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