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