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