1 ///
2 module fluid.node;
3 
4 import std.math;
5 import std.traits;
6 import std.string;
7 import std.algorithm;
8 
9 import fluid.backend;
10 import fluid.tree;
11 import fluid.style;
12 import fluid.utils;
13 import fluid.input;
14 import fluid.actions;
15 import fluid.structs;
16 import fluid.theme : Breadcrumbs;
17 
18 
19 @safe:
20 
21 
22 /// Represents a Fluid node.
23 abstract class Node {
24 
25     public import fluid.structs : NodeAlign, Layout;
26     public import fluid.structs : Align = NodeAlign;
27 
28     static class Extra {
29 
30         private struct CacheKey {
31 
32             size_t dataPtr;
33             FluidBackend backend;
34 
35         }
36 
37         /// Styling texture cache, by image pointer.
38         private TextureGC[CacheKey] cache;
39 
40         /// Load a texture from the image. May return null if there's no valid image.
41         TextureGC* getTexture(FluidBackend backend, Image image) @trusted {
42 
43             // No image
44             if (image.area == 0) return null;
45 
46             const key = CacheKey(cast(size_t) image.data.ptr, backend);
47 
48             // Find or create the entry
49             return &cache.require(key, TextureGC(backend, image));
50 
51         }
52 
53     }
54 
55     public {
56 
57         /// Tree data for the node. Note: requires at least one draw before this will work.
58         LayoutTree* tree;
59 
60         /// Layout for this node.
61         Layout layout;
62 
63         /// Tags assigned for this node.
64         TagList tags;
65 
66         /// Breadcrumbs assigned and applicable to this node. Loaded every resize and every draw.
67         Breadcrumbs breadcrumbs;
68 
69         /// If true, this node will be removed from the tree on the next draw.
70         bool toRemove;
71 
72         /// If true, mouse focus will be disabled for this node, so mouse signals will "go through" to its parents, as
73         /// if the node wasn't there. The node will still detect hover like normal.
74         bool ignoreMouse;
75 
76         /// True if the theme has been assigned explicitly by a direct assignment. If false, the node will instead
77         /// inherit themes from the parent.
78         ///
79         /// This can be set to false to reset the theme.
80         bool isThemeExplicit;
81 
82     }
83 
84     /// Minimum size of the node.
85     protected auto minSize = Vector2(0, 0);
86 
87     private {
88 
89         /// If true, this node must update its size.
90         bool _resizePending = true;
91 
92         /// If true, this node is hidden and won't be rendered.
93         bool _isHidden;
94 
95         /// If true, this node is currently hovered.
96         bool _isHovered;
97 
98         /// If true, this node is currently disabled.
99         bool _isDisabled;
100 
101         /// Check if this node is disabled, or has inherited the status.
102         bool _isDisabledInherited;
103 
104         /// Theme of this node.
105         Theme _theme;
106 
107         /// Cached style for this node.
108         Style _style;
109 
110         /// Attached styling delegates.
111         Rule.StyleDelegate[] _styleDelegates;
112 
113         /// Actions queued for this node; only used for queueing actions before the first `resize`; afterwards, all
114         /// actions are queued directly into the tree.
115         TreeAction[] _queuedActions;
116 
117     }
118 
119     @property {
120 
121         /// Check if the node is hidden.
122         bool isHidden() const return { return _isHidden; }
123 
124         /// Set the visibility
125         bool isHidden(bool value) return {
126 
127             // If changed, trigger resize
128             if (_isHidden != value) updateSize();
129 
130             return _isHidden = value;
131 
132         }
133 
134     }
135 
136     /// Construct a new node.
137     ///
138     /// The typical approach to constructing new nodes is via `fluid.utils.simpleConstructor`. A node component would
139     /// provide an alias pointing to the `simpleConstructor` instance, which can then be used as a factory function. For
140     /// example, `Label` provides the `label` simpleConstructor. Using these has increased convenience by making it
141     /// possible to specify special properties while constructing the node, for example
142     ///
143     /// ---
144     /// auto myLabel = label(.layout!1, .theme, "Hello, World!");
145     /// // Equivalent of:
146     /// auto myLabel = new Label("Hello, World!");
147     /// myLabel.layout = .layout!1;
148     /// myLabel.theme = .theme;
149     /// ---
150     ///
151     /// See_Also:
152     ///     `fluid.utils.simpleConstructor`
153     this() { }
154 
155     /// Get the current theme.
156     inout(Theme) theme() inout { return _theme; }
157 
158     /// Set the theme.
159     Theme theme(Theme value) {
160 
161         isThemeExplicit = true;
162         updateSize();
163         return _theme = value;
164 
165     }
166 
167     /// Nodes automatically inherit theme from their parent, and the root node implictly inherits the default theme.
168     /// An explicitly-set theme will override any inherited themes recursively, stopping at nodes that also have themes 
169     /// set explicitly.
170     /// Params:
171     ///     value = Theme to inherit.
172     /// See_Also: `theme`
173     void inheritTheme(Theme value) {
174 
175         // Do not override explicitly-set themes
176         if (isThemeExplicit) return;
177 
178         _theme = value;
179         updateSize();
180 
181     }
182 
183     @("Themes can be changed at runtime https://git.samerion.com/Samerion/Fluid/issues/114")
184     unittest {
185 
186         import fluid.frame;
187 
188         auto theme1 = nullTheme.derive(
189             rule!Frame(
190                 Rule.backgroundColor = color("#000"),
191             ),
192         );
193         auto theme2 = nullTheme.derive(
194             rule!Frame(
195                 Rule.backgroundColor = color("#fff"),
196             ),
197         );
198 
199         auto deepFrame = vframe();
200         auto blackFrame = vframe(theme1);
201         auto root = vframe(
202             theme1,
203             vframe(
204                 vframe(deepFrame),
205             ),
206             vframe(blackFrame),
207         );
208 
209         root.draw();
210         assert(deepFrame.pickStyle.backgroundColor == color("#000"));
211         assert(blackFrame.pickStyle.backgroundColor == color("#000"));
212         root.theme = theme2;
213         root.draw();
214         assert(deepFrame.pickStyle.backgroundColor == color("#fff"));
215         assert(blackFrame.pickStyle.backgroundColor == color("#000"));
216 
217     }
218 
219     /// Clear the currently assigned theme
220     void resetTheme() {
221 
222         _theme = Theme.init;
223         isThemeExplicit = false;
224         updateSize();
225 
226     }
227 
228     /// Current style, used for sizing. Does not include any changes made by `when` clauses or callbacks.
229     ///
230     /// Direct changes are discouraged, and are likely to be discarded when reloading themes. Use themes instead.
231     ref inout(Style) style() inout { return _style; }
232 
233     override bool opEquals(const Object other) const @safe {
234 
235         return this is other;
236 
237     }
238 
239     bool opEquals(const Node otherNode) const {
240 
241         return this is otherNode;
242 
243     }
244 
245     /// Show the node.
246     This show(this This = Node)() return {
247 
248         // Note: The default value for This is necessary, otherwise virtual calls don't work
249         isHidden = false;
250         return cast(This) this;
251 
252     }
253 
254     /// Hide the node.
255     This hide(this This = Node)() return {
256 
257         isHidden = true;
258         return cast(This) this;
259 
260     }
261 
262     unittest {
263 
264         auto io = new HeadlessBackend;
265         auto root = new class Node {
266 
267             override void resizeImpl(Vector2) {
268                 minSize = Vector2(10, 10);
269             }
270 
271             override void drawImpl(Rectangle outer, Rectangle inner) {
272                 io.drawRectangle(inner, color!"123");
273             }
274 
275         };
276 
277         root.io = io;
278         root.theme = nullTheme;
279         root.draw();
280 
281         io.assertRectangle(Rectangle(0, 0, 10, 10), color!"123");
282         io.nextFrame;
283 
284         // Hide the node now
285         root.hide();
286         root.draw();
287 
288         assert(io.rectangles.empty);
289 
290     }
291 
292     /// Disable this node.
293     This disable(this This = Node)() {
294 
295         // `scope return` attribute on disable() and enable() is broken, `isDisabled` just can't get return for reasons
296         // unknown
297 
298         isDisabled = true;
299         return cast(This) this;
300 
301     }
302 
303     /// Enable this node.
304     This enable(this This = Node)() {
305 
306         isDisabled = false;
307         return cast(This) this;
308 
309     }
310 
311     unittest {
312 
313         import fluid.space;
314         import fluid.button;
315         import fluid.text_input;
316 
317         int submitted;
318 
319         auto io = new HeadlessBackend;
320         auto button = fluid.button.button("Hello!", delegate { submitted++; });
321         auto input = fluid.textInput("Placeholder", delegate { submitted++; });
322         auto root = vspace(button, input);
323 
324         root.io = io;
325         root.draw();
326 
327         // Press the button
328         {
329             io.nextFrame;
330             io.press(KeyboardKey.enter);
331             button.focus();
332             root.draw();
333 
334             assert(submitted == 1);
335         }
336 
337         // Press the button while disabled
338         {
339             io.nextFrame;
340             io.press(KeyboardKey.enter);
341             button.disable();
342             root.draw();
343 
344             assert(button.isDisabled);
345             assert(submitted == 1, "Button shouldn't trigger again");
346         }
347 
348         // Enable the button and hit it again
349         {
350             io.nextFrame;
351             io.press(KeyboardKey.enter);
352             button.enable();
353             root.draw();
354 
355             assert(!button.isDisabledInherited);
356             assert(submitted == 2);
357         }
358 
359         // Try typing into the input box
360         {
361             io.nextFrame;
362             io.release(KeyboardKey.enter);
363             io.inputCharacter("Hello, ");
364             input.focus();
365             root.draw();
366 
367             assert(input.value == "Hello, ");
368         }
369 
370         // Disable the box and try typing again
371         {
372             io.nextFrame;
373             io.inputCharacter("World!");
374             input.disable();
375             root.draw();
376 
377             assert(input.value == "Hello, ", "Input should remain unchanged");
378         }
379 
380         // Attempt disabling the nodes recursively
381         {
382             io.nextFrame;
383             io.press(KeyboardKey.enter);
384             button.focus();
385             input.enable();
386             root.disable();
387             root.draw();
388 
389             assert(root.isDisabled);
390             assert(!button.isDisabled);
391             assert(!input.isDisabled);
392             assert(button.isDisabledInherited);
393             assert(input.isDisabledInherited);
394             assert(submitted == 2);
395         }
396 
397         // Check the input box
398         {
399             io.nextFrame;
400             io.press(KeyboardKey.enter);
401             io.inputCharacter("World!");
402             input.focus();
403 
404             root.draw();
405 
406             assert(submitted == 2);
407             assert(input.value == "Hello, ");
408         }
409 
410         // Enable input once again
411         {
412             io.nextFrame;
413             io.press(KeyboardKey.enter);
414             root.enable();
415             root.draw();
416 
417             assert(submitted == 3);
418             assert(input.value == "Hello, ");
419         }
420 
421     }
422 
423     inout(FluidBackend) backend() inout {
424 
425         return tree.backend;
426 
427     }
428 
429     FluidBackend backend(FluidBackend backend) {
430 
431         // Create the tree if not present
432         if (tree is null) {
433 
434             tree = new LayoutTree(this, backend);
435             return backend;
436 
437         }
438 
439         else return tree.backend = backend;
440 
441     }
442 
443     alias io = backend;
444 
445     /// Toggle the node's visibility.
446     final void toggleShow() {
447 
448         isHidden = !isHidden;
449 
450     }
451 
452     /// Remove this node from the tree before the next draw.
453     final void remove() {
454 
455         isHidden = true;
456         toRemove = true;
457 
458     }
459 
460     /// Get the minimum size of this node.
461     final Vector2 getMinSize() const {
462 
463         return minSize;
464 
465     }
466 
467     /// Check if this node is hovered.
468     ///
469     /// Returns false if the node or, while the node is being drawn, some of its ancestors are disabled.
470     @property
471     bool isHovered() const { return _isHovered && !_isDisabled && !tree.isBranchDisabled; }
472 
473     /// Check if this node is disabled.
474     ref inout(bool) isDisabled() inout { return _isDisabled; }
475 
476     /// Checks if the node is disabled, either by self, or by any of its ancestors. Updated when drawn.
477     bool isDisabledInherited() const { return _isDisabledInherited; }
478 
479     /// Queue an action to perform within this node's branch.
480     ///
481     /// This is recommended to use over `LayoutTree.queueAction`, as it can be used to limit the action to a specific
482     /// branch, and can also work before the first draw.
483     ///
484     /// This function is not safe to use while the tree is being drawn.
485     final void queueAction(TreeAction action)
486     in (action, "Invalid action queued (null)")
487     do {
488 
489         // Set this node as the start for the given action
490         action.startNode = this;
491 
492         // Reset the action
493         action.toStop = false;
494 
495         // Insert the action into the tree's queue
496         if (tree) tree.queueAction(action);
497 
498         // If there isn't a tree, wait for a resize
499         else _queuedActions ~= action;
500 
501     }
502 
503     @system unittest {
504 
505         import fluid.space;
506 
507         Node[4] allNodes;
508         Node[] visitedNodes;
509 
510         auto io = new HeadlessBackend;
511         auto root = allNodes[0] = vspace(
512             allNodes[1] = hspace(
513                 allNodes[2] = hspace(),
514             ),
515             allNodes[3] = hspace(),
516         );
517         auto action = new class TreeAction {
518 
519             override void beforeDraw(Node node, Rectangle) {
520 
521                 visitedNodes ~= node;
522 
523             }
524 
525         };
526 
527         // Queue the action before creating the tree
528         root.queueAction(action);
529 
530         // Assign the backend; note this would create a tree
531         root.io = io;
532 
533         root.draw();
534 
535         assert(visitedNodes == allNodes);
536 
537         // Clear visited nodes
538         io.nextFrame;
539         visitedNodes = [];
540         action.toStop = false;
541 
542         // Queue an action in a branch
543         allNodes[1].queueAction(action);
544 
545         root.draw();
546 
547         assert(visitedNodes[].equal(allNodes[1..3]));
548 
549     }
550 
551     /// True if this node is pending a resize.
552     bool resizePending() const {
553 
554         return _resizePending;
555 
556     }
557 
558     /// Recalculate the window size before next draw.
559     final void updateSize() scope {
560 
561         if (tree) tree.root._resizePending = true;
562         // Tree might be null — if so, the node will be resized regardless
563 
564     }
565 
566     unittest {
567 
568         int resizes;
569 
570         auto io = new HeadlessBackend;
571         auto root = new class Node {
572 
573             override void resizeImpl(Vector2) {
574 
575                 resizes++;
576 
577             }
578             override void drawImpl(Rectangle, Rectangle) { }
579 
580         };
581 
582         root.io = io;
583         assert(resizes == 0);
584 
585         // Resizes are only done on request
586         foreach (i; 0..10) {
587 
588             root.draw();
589             assert(resizes == 1);
590             io.nextFrame;
591 
592         }
593 
594         // Perform such a request
595         root.updateSize();
596         assert(resizes == 1);
597 
598         // Resize will be done right before next draw
599         root.draw();
600         assert(resizes == 2);
601         io.nextFrame;
602 
603         // This prevents unnecessary resizes if multiple things change in a single branch
604         root.updateSize();
605         root.updateSize();
606 
607         root.draw();
608         assert(resizes == 3);
609         io.nextFrame;
610 
611         // Another draw, no more resizes
612         root.draw();
613         assert(resizes == 3);
614 
615     }
616 
617     /// Draw this node as a root node.
618     final void draw() @trusted {
619 
620         // No tree set, create one
621         if (tree is null) {
622 
623             tree = new LayoutTree(this);
624 
625         }
626 
627         // No theme set, set the default
628         if (!theme) {
629 
630             import fluid.default_theme;
631             inheritTheme(fluidDefaultTheme);
632 
633         }
634 
635         assert(theme);
636 
637         const space = tree.io.windowSize;
638 
639         // Clear mouse hover if LMB is up
640         if (!isLMBHeld) tree.hover = null;
641 
642         // Clear scroll
643         tree.scroll = null;
644 
645         // Clear focus info
646         tree.focusDirection = FocusDirection(tree.focusBox);
647         tree.focusBox = Rectangle(float.nan);
648 
649         // Clear breadcrumbs
650         tree.breadcrumbs = Breadcrumbs.init;
651 
652         // Update input
653         tree.poll();
654 
655         // Request a resize if the window was resized
656         if (tree.io.hasJustResized) updateSize();
657 
658         // Resize if required
659         if (resizePending) {
660 
661             resize(tree, theme, space);
662             _resizePending = false;
663 
664         }
665 
666         /// Area to render on
667         const viewport = Rectangle(0, 0, space.x, space.y);
668 
669 
670         // Run beforeTree actions
671         foreach (action; tree.filterActions) {
672 
673             action.beforeTree(this, viewport);
674 
675         }
676 
677         // Draw this node
678         draw(viewport);
679 
680         // Run afterTree actions
681         foreach (action; tree.filterActions) {
682 
683             action.afterTree();
684 
685         }
686 
687 
688         // Set mouse cursor to match hovered node
689         if (tree.hover) {
690 
691             tree.io.mouseCursor = tree.hover.pickStyle().mouseCursor;
692 
693         }
694 
695 
696         // Note: pressed, not released; released activates input events, pressed activates focus
697         const mousePressed = tree.io.isPressed(MouseButton.left)
698             || tree.io.isPressed(MouseButton.right)
699             || tree.io.isPressed(MouseButton.middle);
700 
701         // Update scroll input
702         if (tree.scroll) tree.scroll.scrollImpl(io.scroll);
703 
704         // Mouse is hovering an input node
705         // Note that nodes will remain in tree.hover if LMB is pressed to prevent "hover slipping" — actions should
706         // only trigger if the button was both pressed and released on the node.
707         if (auto hoverInput = cast(FluidHoverable) tree.hover) {
708 
709             // Pass input to the node, unless it's disabled
710             if (!tree.hover.isDisabledInherited) {
711 
712                 // Check if the node is focusable
713                 auto focusable = cast(FluidFocusable) tree.hover;
714 
715                 // If the left mouse button is pressed down, give the node focus
716                 if (mousePressed && focusable) focusable.focus();
717 
718                 // Pass the input to it
719                 hoverInput.runMouseInputActions || hoverInput.mouseImpl;
720 
721             }
722 
723         }
724 
725         // Mouse pressed over a non-focusable node, remove focus
726         else if (mousePressed) tree.focus = null;
727 
728 
729         // Pass keyboard input to the currently focused node
730         if (tree.focus && !tree.focus.asNode.isDisabledInherited) {
731 
732             // TODO BUG: also fires for removed nodes
733 
734             // Let it handle input
735             tree.keyboardHandled = either(
736                 tree.focus.runFocusInputActions,
737                 tree.focus.focusImpl,
738             );
739 
740         }
741 
742         // Nothing has focus
743         else with (FluidInputAction)
744         tree.keyboardHandled = {
745 
746             // Check the first focusable node
747             if (auto first = tree.focusDirection.first) {
748 
749                 // Check for focus action
750                 const focusFirst = tree.isFocusActive!(FluidInputAction.focusNext)
751                     || tree.isFocusActive!(FluidInputAction.focusDown)
752                     || tree.isFocusActive!(FluidInputAction.focusRight)
753                     || tree.isFocusActive!(FluidInputAction.focusLeft);
754 
755                 // Switch focus
756                 if (focusFirst) {
757 
758                     first.focus();
759                     return true;
760 
761                 }
762 
763             }
764 
765             // Or maybe, get the last focusable node
766             if (auto last = tree.focusDirection.last) {
767 
768                 // Check for focus action
769                 const focusLast = tree.isFocusActive!(FluidInputAction.focusPrevious)
770                     || tree.isFocusActive!(FluidInputAction.focusUp);
771 
772                 // Switch focus
773                 if (focusLast) {
774 
775                     last.focus();
776                     return true;
777 
778                 }
779 
780             }
781 
782             return false;
783 
784         }();
785 
786         foreach (action; tree.filterActions) {
787 
788             action.afterInput(tree.keyboardHandled);
789 
790         }
791 
792     }
793 
794     unittest {
795 
796         import fluid.space;
797         import fluid.button;
798 
799         auto io = new HeadlessBackend;
800         auto root = vspace(
801             button("1", delegate { }),
802             button("2", delegate { }),
803             button("3", delegate { }),
804         );
805 
806         root.io = io;
807 
808         root.draw();
809 
810         assert(root.tree.focus is null);
811 
812         // Autofocus first
813         {
814 
815             io.nextFrame;
816             io.press(KeyboardKey.tab);
817             root.draw();
818 
819             // Fluid will automatically try to find the first focusable node
820             assert(root.tree.focus.asNode is root.children[0]);
821 
822             io.nextFrame;
823             io.release(KeyboardKey.tab);
824             root.draw();
825 
826             assert(root.tree.focus.asNode is root.children[0]);
827 
828         }
829 
830         // Tab into the next node
831         {
832 
833             io.nextFrame;
834             io.press(KeyboardKey.tab);
835             root.draw();
836             io.release(KeyboardKey.tab);
837 
838             assert(root.tree.focus.asNode is root.children[1]);
839 
840         }
841 
842         // Autofocus last
843         {
844             root.tree.focus = null;
845 
846             io.nextFrame;
847             io.press(KeyboardKey.leftShift);
848             io.press(KeyboardKey.tab);
849             root.draw();
850 
851             // If left-shift tab is pressed, the last focusable node will be used
852             assert(root.tree.focus.asNode is root.children[$-1]);
853 
854             io.nextFrame;
855             io.release(KeyboardKey.leftShift);
856             io.release(KeyboardKey.tab);
857             root.draw();
858 
859             assert(root.tree.focus.asNode is root.children[$-1]);
860 
861         }
862 
863     }
864 
865     /// Switch to the previous or next focused item
866     @(FluidInputAction.focusPrevious,FluidInputAction.focusNext)
867     protected void focusPreviousOrNext(FluidInputAction actionType) {
868 
869         auto direction = tree.focusDirection;
870 
871         // Get the node to switch to
872         auto node = actionType == FluidInputAction.focusPrevious
873 
874             // Requesting previous item
875             ? either(direction.previous, direction.last)
876 
877             // Requesting next
878             : either(direction.next, direction.first);
879 
880         // Switch focus
881         if (node) node.focus();
882 
883     }
884 
885     /// Switch focus towards a specified direction.
886     @(FluidInputAction.focusLeft, FluidInputAction.focusRight)
887     @(FluidInputAction.focusUp, FluidInputAction.focusDown)
888     protected void focusInDirection(FluidInputAction action) {
889 
890         with (FluidInputAction) {
891 
892             // Check which side we're going
893             const side = action.predSwitch(
894                 focusLeft,  Style.Side.left,
895                 focusRight, Style.Side.right,
896                 focusUp,    Style.Side.top,
897                 focusDown,  Style.Side.bottom,
898             );
899 
900             // Get the node
901             auto node = tree.focusDirection.positional[side];
902 
903             // Switch focus to the node
904             if (node !is null) node.focus();
905 
906         }
907 
908     }
909 
910     /// Draw this node at the specified location from within of another (parent) node.
911     ///
912     /// The drawn node will be aligned according to the `layout` field within the box given.
913     ///
914     /// Params:
915     ///     space = Space the node should be drawn in. It should be limited to space within the parent node.
916     ///             If the node can't fit, it will be cropped.
917     final protected void draw(Rectangle space) @trusted {
918 
919         import std.range;
920 
921         assert(!toRemove, "A toRemove child wasn't removed from container.");
922         assert(tree !is null, toString ~ " wasn't resized prior to drawing. You might be missing an `updateSize`"
923             ~ " call!");
924 
925         // If hidden, don't draw anything
926         if (isHidden) return;
927 
928         const spaceV = Vector2(space.width, space.height);
929 
930         // Get parameters
931         const size = Vector2(
932             layout.nodeAlign[0] == NodeAlign.fill ? space.width  : min(space.width,  minSize.x),
933             layout.nodeAlign[1] == NodeAlign.fill ? space.height : min(space.height, minSize.y),
934         );
935         const position = position(space, size);
936 
937         // Calculate the boxes
938         const marginBox  = Rectangle(position.tupleof, size.tupleof);
939         const borderBox  = style.cropBox(marginBox, style.margin);
940         const paddingBox = style.cropBox(borderBox, style.border);
941         const contentBox = style.cropBox(paddingBox, style.padding);
942         const mainBox    = borderBox;
943 
944         // Load breadcrumbs from the tree
945         breadcrumbs = tree.breadcrumbs;
946         auto currentStyle = pickStyle();
947 
948         // Write dynamic breadcrumbs to the tree
949         // Restore when done
950         tree.breadcrumbs ~= currentStyle.breadcrumbs;
951         scope (exit) tree.breadcrumbs = breadcrumbs;
952 
953         // Get the visible part of the padding box — so overflowed content doesn't get mouse focus
954         const visibleBox = tree.intersectScissors(paddingBox);
955 
956         // Check if hovered
957         _isHovered = hoveredImpl(visibleBox, tree.io.mousePosition);
958 
959         // Set tint
960         auto previousTint = io.tint;
961         io.tint = multiply(previousTint, currentStyle.tint);
962         scope (exit) io.tint = previousTint;
963 
964         // If there's a border active, draw it
965         if (currentStyle.borderStyle) {
966 
967             currentStyle.borderStyle.apply(io, borderBox, style.border);
968             // TODO wouldn't it be better to draw borders as background?
969 
970         }
971 
972         // Check if the mouse stroke started this node
973         const heldElsewhere = !tree.io.isPressed(MouseButton.left)
974             && isLMBHeld;
975 
976         // Check for hover, unless ignored by this node
977         if (isHovered && !ignoreMouse) {
978 
979             // Set global hover as long as the mouse isn't held down
980             if (!heldElsewhere) tree.hover = this;
981 
982             // Update scroll
983             if (auto scrollable = cast(FluidScrollable) this) {
984 
985                 // Only if scrolling is possible
986                 if (scrollable.canScroll(io.scroll))  {
987 
988                     tree.scroll = scrollable;
989 
990                 }
991 
992             }
993 
994         }
995 
996         assert(
997             only(size.tupleof).all!isFinite,
998             format!"Node %s resulting size is invalid: %s; given space = %s, minSize = %s"(
999                 typeid(this), size, space, minSize
1000             ),
1001         );
1002         assert(
1003             only(mainBox.tupleof, contentBox.tupleof).all!isFinite,
1004             format!"Node %s size is invalid: borderBox = %s, contentBox = %s"(
1005                 typeid(this), mainBox, contentBox
1006             )
1007         );
1008 
1009         /// Descending into a disabled tree
1010         const branchDisabled = isDisabled || tree.isBranchDisabled;
1011 
1012         /// True if this node is disabled, and none of its ancestors are disabled
1013         const disabledRoot = isDisabled && !tree.isBranchDisabled;
1014 
1015         // Toggle disabled branch if we're owning the root
1016         if (disabledRoot) tree.isBranchDisabled = true;
1017         scope (exit) if (disabledRoot) tree.isBranchDisabled = false;
1018 
1019         // Save disabled status
1020         _isDisabledInherited = branchDisabled;
1021 
1022         // Count depth
1023         tree.depth++;
1024         scope (exit) tree.depth--;
1025 
1026         // Run beforeDraw actions
1027         foreach (action; tree.filterActions) {
1028 
1029             action.beforeDrawImpl(this, space, mainBox, contentBox);
1030 
1031         }
1032 
1033         // Draw the node cropped
1034         // Note: minSize includes margin!
1035         if (minSize.x > space.width || minSize.y > space.height) {
1036 
1037             const lastScissors = tree.pushScissors(mainBox);
1038             scope (exit) tree.popScissors(lastScissors);
1039 
1040             drawImpl(mainBox, contentBox);
1041 
1042         }
1043 
1044         // Draw the node
1045         else drawImpl(mainBox, contentBox);
1046 
1047 
1048         // If not disabled
1049         if (!branchDisabled) {
1050 
1051             const focusBox = focusBoxImpl(contentBox);
1052 
1053             // Update focus info
1054             tree.focusDirection.update(this, focusBox, tree.depth);
1055 
1056             // If this node is focused
1057             if (this is cast(Node) tree.focus) {
1058 
1059                 // Set the focus box
1060                 tree.focusBox = focusBox;
1061 
1062             }
1063 
1064         }
1065 
1066         // Run afterDraw actions
1067         foreach (action; tree.filterActions) {
1068 
1069             action.afterDrawImpl(this, space, mainBox, contentBox);
1070 
1071         }
1072 
1073     }
1074 
1075     /// Recalculate the minimum node size and update the `minSize` property.
1076     /// Params:
1077     ///     tree  = The parent's tree to pass down to this node.
1078     ///     theme = Theme to inherit from the parent.
1079     ///     space = Available space.
1080     protected final void resize(LayoutTree* tree, Theme theme, Vector2 space)
1081     in(tree, "Tree for Node.resize() must not be null.")
1082     in(theme, "Theme for Node.resize() must not be null.")
1083     do {
1084 
1085         // Inherit tree and theme
1086         this.tree = tree;
1087         inheritTheme(theme);
1088 
1089         // Load breadcrumbs from the tree
1090         breadcrumbs = tree.breadcrumbs;
1091 
1092         // Load the theme
1093         reloadStyles();
1094 
1095         // Write breadcrumbs into the tree
1096         tree.breadcrumbs ~= _style.breadcrumbs;
1097         scope (exit) tree.breadcrumbs = breadcrumbs;
1098 
1099         // Queue actions into the tree
1100         tree.actions ~= _queuedActions;
1101         _queuedActions = null;
1102 
1103 
1104         // The node is hidden, reset size
1105         if (isHidden) minSize = Vector2(0, 0);
1106 
1107         // Otherwise perform like normal
1108         else {
1109 
1110             import std.range;
1111 
1112             const fullMargin = style.fullMargin;
1113             const spacingX = chain(fullMargin.sideX[], style.padding.sideX[]).sum;
1114             const spacingY = chain(fullMargin.sideY[], style.padding.sideY[]).sum;
1115 
1116             // Reduce space by margins
1117             space.x = max(0, space.x - spacingX);
1118             space.y = max(0, space.y - spacingY);
1119 
1120             assert(
1121                 space.x.isFinite && space.y.isFinite,
1122                 format!"Internal error — Node %s was given infinite space: %s; spacing(x = %s, y = %s)"(typeid(this),
1123                     space, spacingX, spacingY)
1124             );
1125 
1126             // Run beforeResize actions
1127             foreach (action; tree.filterActions) {
1128 
1129                 action.beforeResize(this, space);
1130 
1131             }
1132 
1133             // Resize the node
1134             resizeImpl(space);
1135 
1136             assert(
1137                 minSize.x.isFinite && minSize.y.isFinite,
1138                 format!"Node %s resizeImpl requested infinite minSize: %s"(typeid(this), minSize)
1139             );
1140 
1141             // Add margins
1142             minSize.x = ceil(minSize.x + spacingX);
1143             minSize.y = ceil(minSize.y + spacingY);
1144 
1145         }
1146 
1147         assert(
1148             minSize.x.isFinite && minSize.y.isFinite,
1149             format!"Internal error — Node %s returned invalid minSize %s"(typeid(this), minSize)
1150         );
1151 
1152     }
1153 
1154     /// Ditto
1155     ///
1156     /// This is the implementation of resizing to be provided by children.
1157     ///
1158     /// If style margins/paddings are non-zero, they are automatically subtracted from space, so they are handled
1159     /// automatically.
1160     protected abstract void resizeImpl(Vector2 space);
1161 
1162     /// Draw this node.
1163     ///
1164     /// Tip: Instead of directly accessing `style`, use `pickStyle` to enable temporarily changing styles as visual
1165     ///     feedback. `resize` should still use the normal style.
1166     ///
1167     /// Params:
1168     ///     paddingBox = Area which should be used by the node. It should include styling elements such as background,
1169     ///         but no content.
1170     ///     contentBox = Area which should be filled with content of the node, such as child nodes, text, etc.
1171     protected abstract void drawImpl(Rectangle paddingBox, Rectangle contentBox);
1172 
1173     /// Check if the node is hovered.
1174     ///
1175     /// This will be called right before drawImpl for each node in order to determine the which node should handle mouse
1176     /// input.
1177     ///
1178     /// The default behavior considers the entire area of the node to be "hoverable".
1179     ///
1180     /// Params:
1181     ///     rect          = Area the node should be drawn in, as provided by drawImpl.
1182     ///     mousePosition = Current mouse position within the window.
1183     protected bool hoveredImpl(Rectangle rect, Vector2 mousePosition) {
1184 
1185         return rect.contains(mousePosition);
1186 
1187     }
1188 
1189     alias ImplHoveredRect = implHoveredRect;
1190 
1191     deprecated("implHoveredRect is now the default behavior; implHoveredRect is to be removed in 0.8.0")
1192     protected mixin template implHoveredRect() {
1193 
1194         private import fluid.backend : Rectangle, Vector2;
1195 
1196         protected override bool hoveredImpl(Rectangle rect, Vector2 mousePosition) const {
1197 
1198             import fluid.utils : contains;
1199 
1200             return rect.contains(mousePosition);
1201 
1202         }
1203 
1204     }
1205 
1206     /// The focus box defines the *focused* part of the node. This is relevant in nodes which may have a selectable 
1207     /// subset, such as a dropdown box, which may be more important at present moment (selected). Scrolling actions 
1208     /// like `scrollIntoView` will use the focus box to make sure the selected area is presented to the user.
1209     /// Returns: The focus box of the node. 
1210     Rectangle focusBoxImpl(Rectangle inner) const {
1211 
1212         return inner;
1213 
1214     }
1215 
1216     /// Get the current style.
1217     Style pickStyle() {
1218 
1219         // Pick the current style
1220         auto result = _style;
1221 
1222         // Load style from breadcrumbs
1223         // Note breadcrumbs may change while drawing, but should also be able to affect sizing
1224         // For this reason static breadcrumbs are applied both when reloading and when picking
1225         breadcrumbs.applyStatic(this, result);
1226 
1227         // Run delegates
1228         foreach (dg; _styleDelegates) {
1229 
1230             dg(this).apply(this, result);
1231 
1232         }
1233 
1234         // Load dynamic breadcrumb styles
1235         breadcrumbs.applyDynamic(this, result);
1236 
1237         return result;
1238 
1239     }
1240 
1241     /// Reload style from the current theme.
1242     protected void reloadStyles() {
1243 
1244         import fluid.typeface;
1245 
1246         // Reset style
1247         _style = Style.init;
1248 
1249         // Apply theme to the given style
1250         _styleDelegates = theme.apply(this, _style);
1251 
1252         // Apply breadcrumbs
1253         breadcrumbs.applyStatic(this, _style);
1254 
1255         // Update size
1256         updateSize();
1257 
1258     }
1259 
1260     /// Get the node's position in its  box.
1261     private Vector2 position(Rectangle space, Vector2 usedSpace) const {
1262 
1263         float positionImpl(NodeAlign align_, lazy float spaceLeft) {
1264 
1265             with (NodeAlign)
1266             final switch (align_) {
1267 
1268                 case start, fill: return 0;
1269                 case center: return spaceLeft / 2;
1270                 case end: return spaceLeft;
1271 
1272             }
1273 
1274         }
1275 
1276         return Vector2(
1277             space.x + positionImpl(layout.nodeAlign[0], space.width  - usedSpace.x),
1278             space.y + positionImpl(layout.nodeAlign[1], space.height - usedSpace.y),
1279         );
1280 
1281     }
1282 
1283     @system  // catching Error
1284     unittest {
1285 
1286         import std.exception;
1287         import core.exception;
1288         import fluid.frame;
1289 
1290         static class Square : Frame {
1291             @safe:
1292             Color color;
1293             this(Color color) {
1294                 this.color = color;
1295             }
1296             override void resizeImpl(Vector2) {
1297                 minSize = Vector2(100, 100);
1298             }
1299             override void drawImpl(Rectangle, Rectangle inner) {
1300                 io.drawRectangle(inner, color);
1301             }
1302         }
1303 
1304         alias square = simpleConstructor!Square;
1305 
1306         auto io = new HeadlessBackend;
1307         auto colors = [
1308             color!"7ff0a5",
1309             color!"17cccc",
1310             color!"a6a415",
1311             color!"cd24cf",
1312         ];
1313         auto root = vframe(
1314             .layout!"fill",
1315             square(.layout!"start",  colors[0]),
1316             square(.layout!"center", colors[1]),
1317             square(.layout!"end",    colors[2]),
1318             square(.layout!"fill",   colors[3]),
1319         );
1320 
1321         root.theme = Theme.init.derive(
1322             rule!Frame(Rule.backgroundColor = color!"1c1c1c")
1323         );
1324         root.io = io;
1325 
1326         // Test the layout
1327         {
1328 
1329             root.draw();
1330 
1331             // Each square in order
1332             io.assertRectangle(Rectangle(0, 0, 100, 100), colors[0]);
1333             io.assertRectangle(Rectangle(350, 100, 100, 100), colors[1]);
1334             io.assertRectangle(Rectangle(700, 200, 100, 100), colors[2]);
1335 
1336             // Except the last one, which is turned into a rectangle by "fill"
1337             // A proper rectangle class would change its target rectangles to keep aspect ratio
1338             io.assertRectangle(Rectangle(0, 300, 800, 100), colors[3]);
1339 
1340         }
1341 
1342         // Now do the same, but expand each node
1343         {
1344 
1345             io.nextFrame;
1346 
1347             foreach (child; root.children) {
1348                 child.layout.expand = 1;
1349             }
1350 
1351             root.draw().assertThrown!AssertError;  // Oops, forgot to resize!
1352             root.updateSize;
1353             root.draw();
1354 
1355             io.assertRectangle(Rectangle(0, 0, 100, 100), colors[0]);
1356             io.assertRectangle(Rectangle(350, 175, 100, 100), colors[1]);
1357             io.assertRectangle(Rectangle(700, 350, 100, 100), colors[2]);
1358             io.assertRectangle(Rectangle(0, 450, 800, 150), colors[3]);
1359 
1360         }
1361 
1362         // Change Y alignment
1363         {
1364 
1365             io.nextFrame;
1366 
1367             root.children[0].layout = .layout!(1, "start", "end");
1368             root.children[1].layout = .layout!(1, "center", "fill");
1369             root.children[2].layout = .layout!(1, "end", "start");
1370             root.children[3].layout = .layout!(1, "fill", "center");
1371 
1372             root.updateSize;
1373             root.draw();
1374 
1375             io.assertRectangle(Rectangle(0, 50, 100, 100), colors[0]);
1376             io.assertRectangle(Rectangle(350, 150, 100, 150), colors[1]);
1377             io.assertRectangle(Rectangle(700, 300, 100, 100), colors[2]);
1378             io.assertRectangle(Rectangle(0, 475, 800, 100), colors[3]);
1379 
1380         }
1381 
1382         // Try different expand values
1383         {
1384 
1385             io.nextFrame;
1386 
1387             root.children[0].layout = .layout!(0, "center", "fill");
1388             root.children[1].layout = .layout!(1, "center", "fill");
1389             root.children[2].layout = .layout!(2, "center", "fill");
1390             root.children[3].layout = .layout!(3, "center", "fill");
1391 
1392             root.updateSize;
1393             root.draw();
1394 
1395             // The first rectangle doesn't expand so it should be exactly 100×100 in size
1396             io.assertRectangle(Rectangle(350, 0, 100, 100), colors[0]);
1397 
1398             // The remaining space is 500px, so divided into 1+2+3=6 pieces, it should be about 83.33px per piece
1399             io.assertRectangle(Rectangle(350, 100.00, 100,  83.33), colors[1]);
1400             io.assertRectangle(Rectangle(350, 183.33, 100, 166.66), colors[2]);
1401             io.assertRectangle(Rectangle(350, 350.00, 100, 250.00), colors[3]);
1402 
1403         }
1404 
1405     }
1406 
1407     private bool isLMBHeld() @trusted {
1408 
1409         return tree.io.isDown(MouseButton.left)
1410             || tree.io.isReleased(MouseButton.left);
1411 
1412     }
1413 
1414     override string toString() const {
1415 
1416         return format!"%s(%s)"(typeid(this), layout);
1417 
1418     }
1419 
1420 }
1421 
1422 ///
1423 void run(Node node) {
1424 
1425     // Mock run callback is available
1426     if (mockRun) {
1427 
1428         mockRun()(node);
1429 
1430     }
1431 
1432     // TODO Create the event loop interface
1433     else assert(false, "Default backend does not expose an event loop interface.");
1434 
1435 }
1436 
1437 alias RunCallback = void delegate(Node node) @safe;
1438 
1439 /// Set a new function to use instead of `run`.
1440 RunCallback mockRun(RunCallback callback) {
1441 
1442     // Assign the callback
1443     mockRun() = callback;
1444     return mockRun();
1445 
1446 }
1447 
1448 ref RunCallback mockRun() {
1449 
1450     static RunCallback callback;
1451     return callback;
1452 
1453 }
1454