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 }