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