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