1 module fluid.tree; 2 3 import std.conv; 4 import std.math; 5 import std.container; 6 import std.algorithm; 7 8 import fluid.node; 9 import fluid.input; 10 import fluid.style; 11 import fluid.backend; 12 13 import fluid.future.pipe; 14 import fluid.future.context; 15 16 17 @safe: 18 19 20 version (OSX) 21 version = Fluid_MacKeyboard; 22 23 /// 24 struct FocusDirection { 25 26 struct WithPriority { 27 28 /// Pick priority based on tree distance from the focused node. 29 int priority; 30 31 /// Square of the distance between this node and the focused node. 32 float distance2; 33 34 /// The node. 35 FluidFocusable node; 36 37 alias node this; 38 39 } 40 41 /// Available space box of the focused item after last frame. 42 Rectangle lastFocusBox; 43 44 /// Nodes that may get focus with tab navigation. 45 FluidFocusable previous, next; 46 47 /// First and last focusable nodes in the tree. 48 FluidFocusable first, last; 49 50 /// Focusable nodes, by direction from the focused node. 51 WithPriority[4] positional; 52 53 /// Focus priority for the currently drawn node. 54 /// 55 /// Increased until the focused node is found, decremented afterwards. As a result, values will be the highest for 56 /// nodes near the focused one. Changes with tree depth rather than individual nodes. 57 int priority; 58 59 private { 60 61 /// Value `priority` is summed with on each step. `1` before finding the focused node, `-1` after. 62 int priorityDirection = 1; 63 64 /// Current tree depth. 65 uint depth; 66 67 } 68 69 /// Update focus info with the given node. Automatically called when a node is drawn, shouldn't be called manually. 70 /// 71 /// `previous` will be the last focusable node encountered before the focused node, and `next` will be the first one 72 /// after. `first` and `last will be the last focusable nodes in the entire tree. 73 /// 74 /// Params: 75 /// current = Node to update the focus info with. 76 /// box = Box defining node boundaries (focus box) 77 /// depth = Current tree depth. Pass in `tree.depth`. 78 void update(Node current, Rectangle box, uint depth) 79 in (current !is null, "Current node must not be null") 80 do { 81 82 import std.algorithm : either; 83 84 auto currentFocusable = cast(FluidFocusable) current; 85 86 // Count focus priority 87 { 88 89 // Get depth difference since last time 90 const int depthDiff = depth - this.depth; 91 92 // Count steps in change of depth 93 priority += priorityDirection * abs(depthDiff); 94 95 // Update depth 96 this.depth = depth; 97 98 } 99 100 // Stop if the current node can't take focus 101 if (!currentFocusable) return; 102 103 // And it DOES have focus 104 if (current.tree.focus is currentFocusable) { 105 106 // Mark the node preceding it to the last encountered focusable node 107 previous = last; 108 109 // Clear the next node, so it can be overwritten by a correct value. 110 next = null; 111 112 // Reverse priority target 113 priorityDirection = -1; 114 115 } 116 117 else { 118 119 // Update positional focus 120 updatePositional(currentFocusable, box); 121 122 // There's no node to take focus next, set it now 123 if (next is null) next = currentFocusable; 124 125 } 126 127 128 // Set the current node as the first focusable, if true 129 if (first is null) first = currentFocusable; 130 131 // Replace the last 132 last = currentFocusable; 133 134 } 135 136 /// Check the given node's position and update `positional` to match. 137 private void updatePositional(FluidFocusable node, Rectangle box) { 138 139 // Note: This might give false-positives if the focused node has changed during this frame 140 141 // Check each direction 142 foreach (i, ref otherNode; positional) { 143 144 const side = cast(Style.Side) i; 145 const dist = distance2(box, side); 146 147 // If a node took this spot before 148 if (otherNode !is null) { 149 150 // Ignore if the other node has higher priority 151 if (otherNode.priority > priority) continue; 152 153 // If priorities are equal, check if we're closer than the other node 154 if (otherNode.priority == priority 155 && otherNode.distance2 < dist) continue; 156 157 } 158 159 // Check if this node matches the direction 160 if (checkDirection(box, side)) { 161 162 // Replace the node 163 otherNode = WithPriority(priority, dist, node); 164 165 } 166 167 } 168 169 } 170 171 /// Check if the given box is located to the given side of the focus box. 172 bool checkDirection(Rectangle box, Style.Side side) { 173 174 // Distance between box sides facing each other. 175 // 176 // ↓ lastFocusBox ↓ box 177 // +======+ +------+ 178 // | | | | 179 // | | ~~~~~~ | | 180 // | | | | 181 // +======+ +------+ 182 // side ↑ ↑ side.reverse 183 const distanceExternal = lastFocusBox.getSide(side) - box.getSide(side.reverse); 184 185 // Distance between corresponding box sides. 186 // 187 // ↓ lastFocusBox ↓ box 188 // +======+ +------+ 189 // | | : | 190 // | | ~~~~~~~~~~~~~ | 191 // | | : | 192 // +======+ +------+ 193 // side ↑ side ↑ 194 const distanceInternal = lastFocusBox.getSide(side) - box.getSide(side); 195 196 // The condition for the return value to be true, is for distanceInternal to be greater than distanceExternal. 197 // This is not the case in the opposite situation. 198 // 199 // For example, if we're checking if the box is on the *right* of lastFocusBox: 200 // 201 // trueish scenario: falseish scenario: 202 // Box is to the right of lastFocusBox Box is the left of lastFocusBox 203 // 204 // ↓ lastFocusBox ↓ box ↓ box ↓ lastFocusBox 205 // +======+ +------+ +------+ +======+ 206 // | | ~~~~~~ : | external | ~~~~~~~~~~~~~~~~~~~~ | external 207 // | | : | < | : : | > 208 // | | ~~~~~~~~~~~~~ | internal | : ~~~~~~~~~~~~~ | internal 209 // +======+ +------+ +------+ +======+ 210 // side ↑ ↑ side.reverse side ↑ side ↑ 211 const condition = abs(distanceInternal) > abs(distanceExternal); 212 213 // ↓ box There is an edgecase though. If one box entirely overlaps the other on one axis, we 214 // +--------------------+ might end up with unwanted behavior, for example, in a ScrollFrame, focus might 215 // | ↓ lastFocusBox | switch to the scrollbar instead of a child, as we would normally expect. 216 // | +============+ | 217 // | | | | For this reason, we require both `distanceInternal` and `distanceExternal` to have 218 // +---| |---+ the same sign, as it normally would, but not here. 219 // | | 220 // +============+ One can still navigate to the `box` using controls for the other axis. 221 return condition 222 && distanceInternal * distanceExternal >= 0; 223 224 } 225 226 /// Get the square of the distance between given box and `lastFocusBox`. 227 float distance2(Rectangle box, Style.Side side) { 228 229 /// Get the center of given rectangle on the axis opposite to the results of getSide. 230 float center(Rectangle rect) { 231 232 return side == Style.Side.left || side == Style.Side.right 233 ? rect.y + rect.height 234 : rect.x + rect.width; 235 236 } 237 238 // Distance between box sides facing each other, see `checkDirection` 239 const distanceExternal = lastFocusBox.getSide(side) - box.getSide(side.reverse); 240 241 /// Distance between centers of the boxes on the other axis 242 const distanceOpposite = center(box) - center(lastFocusBox); 243 244 return distanceExternal^^2 + distanceOpposite^^2; 245 246 } 247 248 } 249 250 /// A class for iterating over the node tree. 251 abstract class TreeAction : Publisher!() { 252 253 public { 254 255 /// Node to descend into; `beforeDraw` and `afterDraw` will only be emitted for this node and its children. 256 /// 257 /// May be null to enable iteration over the entire tree. 258 Node startNode; 259 260 /// If true, this action is complete and no callbacks should be ran. 261 /// 262 /// Overloads of the same callbacks will still be called for the event that prompted stopping. 263 bool toStop; // this should be private 264 265 /// Keeps track of the number of times the action has been started or stopped. Every start and every stop 266 /// bumps the generation number. 267 /// 268 /// The generation number is used to determine if the action runner should continue or discontinue the action. 269 /// If the number is greater than the one the runner stored at the time it was scheduled, it will stop running. 270 /// This means that if an action is restarted, the old run will be unregistered, preventing the action from 271 /// running twice at a time. 272 /// 273 /// Only applies to actions started using `Node.startAction`, introduced in 0.7.2, and not `Node.runAction`. 274 int generation; 275 276 } 277 278 private { 279 280 /// Subscriber for events, i.e. `then` 281 Event!() _finished; 282 283 /// Set to true once the action has descended into `startNode`. 284 bool _inStartNode; 285 286 /// Set to true once `beforeTree` is called. Set to `false` afterwards. 287 bool _inTree; 288 289 } 290 291 /// Returns: True if the tree is currently drawing `startNode` or any of its children. 292 bool inStartNode() const { 293 return _inStartNode; 294 } 295 296 /// Returns: True if the tree has been entered. Set to true on `beforeTree` and to false on `afterTree`. 297 bool inTree() const { 298 return _inTree; 299 } 300 301 /// Remove all event handlers attached to this tree. 302 void clearSubscribers() { 303 _finished.clearSubscribers(); 304 } 305 306 override final void subscribe(Subscriber!() subscriber) { 307 _finished.subscribe(subscriber); 308 } 309 310 /// Stop the action. 311 /// 312 /// No further hooks will be triggered after calling this, and the action will soon be removed from the list 313 /// of running actions. Overloads of the same hook that called `stop` may still be called. 314 final void stop() { 315 316 if (toStop) return; 317 318 // Perform the stop 319 generation++; 320 toStop = true; 321 stopped(); 322 323 // Reset state 324 _inStartNode = false; 325 _inTree = false; 326 327 } 328 329 /// Called whenever this action is started — added to the list of running actions in the `LayoutTree` 330 /// or `TreeContext`. 331 /// 332 /// This hook may not be called immediately when added through `node.queueAction` or `node.startAction`; 333 /// it will wait until the node's first resize so it can connect to the tree. 334 void started() { 335 336 } 337 338 /// Called whenever this action is stopped by calling `stop`. 339 /// 340 /// This can be used to trigger user-assigned callbacks. Call `super.stopped()` when overriding to make sure all 341 /// finish hooks are called. 342 void stopped() { 343 _finished(); 344 } 345 346 /// Determine whether `beforeTree` and `afterTree` should be called. 347 /// 348 /// By default, `afterTree` is disabled if `beforeTree` wasn't called before. 349 /// Subclasses may change this to adjust this behavior. 350 /// 351 /// Returns: 352 /// For `filterBeforeTree`, true if `beforeTree` is to be called. 353 /// For `filterAfterTree`, true if `afterTree` is to be called. 354 bool filterBeforeTree() { 355 return true; 356 } 357 358 /// ditto 359 bool filterAfterTree() { 360 return inTree; 361 } 362 363 /// Determine whether `beforeDraw` and `afterDraw` should be called for the given node. 364 /// 365 /// By default, this is used to filter out all nodes except for `startNode` and its children, and to keep 366 /// the action from starting in the middle of the tree. 367 /// Subclasses may change this to adjust this behavior. 368 /// 369 /// Params: 370 /// node = Node that is subject to the hook call. 371 /// Returns: 372 /// For `filterBeforeDraw`, true if `beforeDraw` is to be called for this node. 373 /// For `filterAfterDraw`, true if `afterDraw` is to be called for this node. 374 bool filterBeforeDraw(Node node) { 375 376 // Not in tree 377 if (!inTree) return false; 378 379 // Start mode must have been reached 380 return startNode is null || inStartNode; 381 382 } 383 384 /// ditto 385 bool filterAfterDraw(Node node) { 386 387 // Not in tree 388 if (!inTree) return false; 389 390 // Start mode must have been reached 391 return startNode is null || inStartNode; 392 393 } 394 395 /// Called before the tree is drawn. Keep in mind this might not be called if the action is started when tree 396 /// iteration has already begun. 397 /// Params: 398 /// root = Root of the tree. 399 /// viewport = Screen space for the node. 400 void beforeTree(Node root, Rectangle viewport) { } 401 402 final package void beforeTreeImpl(Node root, Rectangle viewport) { 403 404 _inTree = true; 405 406 if (filterBeforeTree()) { 407 beforeTree(root, viewport); 408 } 409 410 } 411 412 /// Called before a node is resized. 413 void beforeResize(Node node, Vector2 viewportSpace) { } 414 415 final package void beforeResizeImpl(Node node, Vector2 viewport) { 416 beforeResize(node, viewport); 417 } 418 419 /// Called after a node is resized. 420 void afterResize(Node node, Vector2 viewportSpace) { } 421 422 final package void afterResizeImpl(Node node, Vector2 viewport) { 423 afterResize(node, viewport); 424 } 425 426 /// Called before each `drawImpl` call of any node in the tree, so supplying parent nodes before their children. 427 /// 428 /// This might not be called if the node is offscreen. If you need to find all nodes, try `beforeResize`. 429 /// 430 /// Params: 431 /// node = Node that's about to be drawn. 432 /// space = Space given for the node. 433 /// paddingBox = Padding box of the node. 434 /// contentBox = Content box of teh node. 435 void beforeDraw(Node node, Rectangle space, Rectangle paddingBox, Rectangle contentBox) { } 436 437 /// ditto 438 void beforeDraw(Node node, Rectangle space) { } 439 440 /// internal 441 final package void beforeDrawImpl(Node node, Rectangle space, Rectangle paddingBox, Rectangle contentBox) { 442 443 // Open the start branch 444 if (startNode && node.opEquals(startNode)) { 445 _inStartNode = true; 446 } 447 448 // Run the hooks if the filter passes 449 if (filterBeforeDraw(node)) { 450 beforeDraw(node, space, paddingBox, contentBox); 451 beforeDraw(node, space); 452 } 453 454 } 455 456 /// Called after each `drawImpl` call of any node in the tree, so supplying children nodes before their parents. 457 /// 458 /// This might not be called if the node is offscreen. If you need to find all nodes, try `beforeResize`. 459 /// 460 /// Params: 461 /// node = Node that's about to be drawn. 462 /// space = Space given for the node. 463 /// paddingBox = Padding box of the node. 464 /// contentBox = Content box of teh node. 465 void afterDraw(Node node, Rectangle space, Rectangle paddingBox, Rectangle contentBox) { } 466 467 /// ditto 468 void afterDraw(Node node, Rectangle space) { } 469 470 /// internal 471 final package void afterDrawImpl(Node node, Rectangle space, Rectangle paddingBox, Rectangle contentBox) { 472 473 // Run the filter 474 if (filterAfterDraw(node)) { 475 afterDraw(node, space, paddingBox, contentBox); 476 afterDraw(node, space); 477 } 478 479 // Close the start branch 480 if (startNode && node.opEquals(startNode)) { 481 _inStartNode = false; 482 } 483 484 } 485 486 /// Called after the tree is drawn. Called before input events, so they can assume actions have completed. 487 /// 488 /// By default, calls `stop()` preventing the action from evaluating during next draw. 489 void afterTree() { 490 491 stop(); 492 493 } 494 495 final package void afterTreeImpl() { 496 497 if (filterAfterTree()) { 498 afterTree(); 499 } 500 _inTree = false; 501 502 } 503 504 /// Hook that triggers after processing input. Useful if post-processing is necessary to, perhaps, implement 505 /// fallback input. 506 /// 507 /// Warning: This will **not trigger** unless `afterTree` is overrided not to stop the action. If you make use of 508 /// this, make sure to make the action stop in this method. 509 /// 510 /// Params: 511 /// keyboardHandled = If true, keyboard input was handled. Passed by reference, so if you react to input, change 512 /// this to true. 513 void afterInput(ref bool keyboardHandled) { } 514 515 } 516 517 /// Global data for the layout tree. 518 struct LayoutTree { 519 520 import fluid.theme : Breadcrumbs; 521 522 // Nodes 523 public { 524 525 /// Root node of the tree. 526 Node root; 527 528 /// Node the mouse is hovering over if any. 529 /// 530 /// This is the last — topmost — node in the tree with `isHovered` set to true. 531 Node hover; 532 533 /// Currently focused node. 534 /// 535 /// Changing this value directly is discouraged. Some nodes might not want the focus! Be gentle, call 536 /// `FluidFocusable.focus()` instead and let the node set the value on its own. 537 FluidFocusable focus; 538 539 /// Deepest hovered scrollable node. 540 FluidScrollable scroll; 541 542 } 543 544 // Input 545 public { 546 547 /// Focus direction data. 548 FocusDirection focusDirection; 549 550 /// Padding box of the currently focused node. Only available after the node has been drawn. 551 /// 552 /// See_also: `focusDirection.lastFocusBox`. 553 Rectangle focusBox; 554 555 /// Tree actions queued to execute during next draw. 556 DList!TreeAction actions; 557 558 /// Input strokes bound to emit given action signals. 559 /// 560 /// Input layers have to be sorted. 561 InputLayer[] boundInputs; 562 563 invariant(boundInputs.isSorted); 564 565 /// Actions that are currently held down. 566 DList!InputBinding downActions; 567 568 /// Actions that have just triggered. 569 DList!InputBinding activeActions; 570 571 /// Access to core input and output facilities. 572 FluidBackend backend; 573 alias io = backend; 574 575 /// True if keyboard input was handled during the last frame; updated after tree rendering has completed. 576 bool wasKeyboardHandled; 577 578 deprecated("keyboardHandled was renamed to wasKeyboardHandled and will be removed in Fluid 0.8.0.") 579 alias keyboardHandled = wasKeyboardHandled; 580 581 } 582 583 /// Miscelleanous, technical properties. 584 public { 585 586 /// Current node drawing depth. 587 uint depth; 588 589 /// Current rectangle drawing is limited to. 590 Rectangle scissors; 591 592 /// True if the current tree branch is marked as disabled (doesn't take input). 593 bool isBranchDisabled; 594 595 /// Current breadcrumbs. These are assigned to any node that is resized or drawn at the time. 596 /// 597 /// Any node that introduces its own breadcrumbs will push onto this stack, and pop once finished. 598 Breadcrumbs breadcrumbs; 599 600 /// Context for the new I/O system. https://git.samerion.com/Samerion/Fluid/issues/148 601 TreeContextData context; 602 603 } 604 605 /// Incremented for every `filterActions` access to prevent nested accesses from breaking previously made ranges. 606 private int _actionAccessCounter; 607 608 /// Create a new tree with the given node as its root, and using the given backend for I/O. 609 this(Node root, FluidBackend backend) { 610 611 this.root = root; 612 this.backend = backend; 613 this.restoreDefaultInputBinds(); 614 615 } 616 617 /// Create a new tree with the given node as its root. Use the default backend, if any is present. 618 this(Node root) { 619 620 this(root, defaultFluidBackend); 621 622 assert(backend, "Cannot create LayoutTree; no backend was chosen, and no default is set."); 623 624 } 625 626 /// Returns true if this branch requested a resize or is pending a resize. 627 bool resizePending() const { 628 629 return root.resizePending; 630 631 } 632 633 /// Returns: True if the mouse is currently hovering a node in the tree. 634 bool isHovered() const { 635 636 return hover !is null; 637 638 } 639 640 /// Queue an action to perform while iterating the tree. 641 /// 642 /// Avoid using this; most of the time `Node.queueAction` is what you want. `LayoutTree.queueAction` might fire 643 /// too early 644 void queueAction(TreeAction action) 645 in (action, "Invalid action queued") 646 do { 647 648 actions ~= action; 649 650 // Run the first hook 651 action.started(); 652 653 } 654 655 /// Restore defaults for given actions. 656 void restoreDefaultInputBinds() { 657 658 /// Get the ID of an input action. 659 auto bind(alias a, T)(T arg) { 660 661 return InputBinding(InputAction!a.id, InputStroke.Item(arg)); 662 663 } 664 665 with (FluidInputAction) { 666 667 // System-independent keys 668 auto universalShift = InputLayer( 669 InputStroke(KeyboardKey.leftShift), 670 [ 671 bind!focusPrevious(KeyboardKey.tab), 672 bind!entryPrevious(KeyboardKey.tab), 673 bind!outdent(KeyboardKey.tab), 674 bind!selectPreviousChar(KeyboardKey.left), 675 bind!selectNextChar(KeyboardKey.right), 676 bind!selectPreviousLine(KeyboardKey.up), 677 bind!selectNextLine(KeyboardKey.down), 678 bind!selectToLineStart(KeyboardKey.home), 679 bind!selectToLineEnd(KeyboardKey.end), 680 bind!breakLine(KeyboardKey.enter), 681 bind!contextMenu(KeyboardKey.f10), 682 ] 683 ); 684 auto universal = InputLayer( 685 InputStroke(), 686 [ 687 // Press 688 bind!press(MouseButton.left), 689 bind!press(KeyboardKey.enter), 690 bind!press(GamepadButton.cross), 691 692 // Submit 693 bind!submit(KeyboardKey.enter), 694 bind!submit(GamepadButton.cross), 695 696 // Cancel 697 bind!cancel(KeyboardKey.escape), 698 bind!cancel(GamepadButton.circle), 699 700 // Menu 701 bind!contextMenu(MouseButton.right), 702 bind!contextMenu(KeyboardKey.contextMenu), 703 704 // Tabbing; index-focus 705 bind!focusPrevious(GamepadButton.leftButton), 706 bind!focusNext(KeyboardKey.tab), 707 bind!focusNext(GamepadButton.rightButton), 708 709 // Directional focus 710 bind!focusLeft(KeyboardKey.left), 711 bind!focusLeft(GamepadButton.dpadLeft), 712 bind!focusRight(KeyboardKey.right), 713 bind!focusRight(GamepadButton.dpadRight), 714 bind!focusUp(KeyboardKey.up), 715 bind!focusUp(GamepadButton.dpadUp), 716 bind!focusDown(KeyboardKey.down), 717 bind!focusDown(GamepadButton.dpadDown), 718 719 // Text input 720 bind!backspace(KeyboardKey.backspace), 721 bind!deleteChar(KeyboardKey.delete_), 722 bind!breakLine(KeyboardKey.enter), 723 bind!previousChar(KeyboardKey.left), 724 bind!nextChar(KeyboardKey.right), 725 bind!previousLine(KeyboardKey.up), 726 bind!nextLine(KeyboardKey.down), 727 bind!entryPrevious(KeyboardKey.up), 728 bind!entryPrevious(GamepadButton.dpadUp), 729 bind!entryNext(KeyboardKey.down), 730 bind!entryNext(KeyboardKey.tab), 731 bind!entryNext(GamepadButton.dpadDown), 732 bind!toLineStart(KeyboardKey.home), 733 bind!toLineEnd(KeyboardKey.end), 734 bind!insertTab(KeyboardKey.tab), 735 736 // Scrolling 737 bind!scrollLeft(KeyboardKey.left), 738 bind!scrollLeft(GamepadButton.dpadLeft), 739 bind!scrollRight(KeyboardKey.right), 740 bind!scrollRight(GamepadButton.dpadRight), 741 bind!scrollUp(KeyboardKey.up), 742 bind!scrollUp(GamepadButton.dpadUp), 743 bind!scrollDown(KeyboardKey.down), 744 bind!scrollDown(GamepadButton.dpadDown), 745 bind!pageUp(KeyboardKey.pageUp), 746 bind!pageDown(KeyboardKey.pageDown), 747 ] 748 ); 749 750 // TODO universal left/right key 751 version (Fluid_MacKeyboard) 752 boundInputs = [ 753 754 // Shift + Command 755 InputLayer( 756 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftSuper), 757 [ 758 // TODO Command should *expand selection* on macOS instead of current 759 // toLineStart/toLineEnd behavior 760 bind!selectToLineStart(KeyboardKey.left), 761 bind!selectToLineEnd(KeyboardKey.right), 762 bind!selectToStart(KeyboardKey.up), 763 bind!selectToEnd(KeyboardKey.down), 764 bind!redo(KeyboardKey.z), 765 ] 766 ), 767 768 // Shift + Option 769 InputLayer( 770 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftAlt), 771 [ 772 bind!selectPreviousWord(KeyboardKey.left), 773 bind!selectNextWord(KeyboardKey.right), 774 ] 775 ), 776 777 // Command 778 InputLayer( 779 InputStroke(KeyboardKey.leftSuper), 780 [ 781 bind!toLineStart(KeyboardKey.left), 782 bind!toLineEnd(KeyboardKey.right), 783 bind!toStart(KeyboardKey.up), 784 bind!toEnd(KeyboardKey.down), 785 bind!selectAll(KeyboardKey.a), 786 bind!copy(KeyboardKey.c), 787 bind!cut(KeyboardKey.x), 788 bind!paste(KeyboardKey.v), 789 bind!undo(KeyboardKey.z), 790 bind!redo(KeyboardKey.y), 791 bind!submit(KeyboardKey.enter), 792 ] 793 ), 794 795 // Option 796 InputLayer( 797 InputStroke(KeyboardKey.leftAlt), 798 [ 799 bind!deleteWord(KeyboardKey.delete_), 800 bind!backspaceWord(KeyboardKey.backspace), 801 bind!previousWord(KeyboardKey.left), 802 bind!nextWord(KeyboardKey.right), 803 ] 804 ), 805 806 // Control 807 InputLayer( 808 InputStroke(KeyboardKey.leftControl), 809 [ 810 bind!backspaceWord(KeyboardKey.w), // emacs & vim 811 bind!entryPrevious(KeyboardKey.k), // vim 812 bind!entryPrevious(KeyboardKey.p), // emacs 813 bind!entryNext(KeyboardKey.j), // vim 814 bind!entryNext(KeyboardKey.n), // emacs 815 ] 816 ), 817 818 universalShift, 819 universal, 820 ]; 821 else 822 boundInputs = [ 823 824 InputLayer( 825 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftControl), 826 [ 827 bind!selectPreviousWord(KeyboardKey.left), 828 bind!selectNextWord(KeyboardKey.right), 829 bind!selectToStart(KeyboardKey.home), 830 bind!selectToEnd(KeyboardKey.end), 831 bind!redo(KeyboardKey.z), 832 ] 833 ), 834 835 InputLayer( 836 InputStroke(KeyboardKey.leftControl), 837 [ 838 bind!deleteWord(KeyboardKey.delete_), 839 bind!backspaceWord(KeyboardKey.backspace), 840 bind!backspaceWord(KeyboardKey.w), // emacs & vim 841 bind!entryPrevious(KeyboardKey.k), // vim 842 bind!entryPrevious(KeyboardKey.p), // emacs 843 bind!entryNext(KeyboardKey.j), // vim 844 bind!entryNext(KeyboardKey.n), // emacs 845 bind!previousWord(KeyboardKey.left), 846 bind!nextWord(KeyboardKey.right), 847 bind!selectAll(KeyboardKey.a), 848 bind!copy(KeyboardKey.c), 849 bind!cut(KeyboardKey.x), 850 bind!paste(KeyboardKey.v), 851 bind!undo(KeyboardKey.z), 852 bind!redo(KeyboardKey.y), 853 bind!toStart(KeyboardKey.home), 854 bind!toEnd(KeyboardKey.end), 855 856 // Submit with ctrl+enter 857 bind!submit(KeyboardKey.enter), 858 ] 859 ), 860 861 InputLayer( 862 InputStroke(KeyboardKey.leftAlt), 863 [ 864 bind!entryUp(KeyboardKey.up), 865 ] 866 ), 867 868 universalShift, 869 universal, 870 871 ]; 872 873 } 874 875 } 876 877 /// Remove any inputs bound to given input action. 878 /// Returns: `true` if the action was cleared. 879 bool clearBoundInput(InputActionID action) { 880 881 import std.array; 882 883 // TODO test 884 885 bool found; 886 887 foreach (ref layer; boundInputs) { 888 889 const oldLength = layer.bindings.length; 890 891 layer.bindings = layer.bindings.filter!(a => a.action == action).array; 892 893 if (layer.bindings.length != oldLength) { 894 found = true; 895 } 896 897 } 898 899 return found; 900 901 } 902 903 /// Find a layer for the given input stroke. 904 /// Returns: Layer found for the given input stroke. `null` if none found. 905 inout(InputLayer)* layerForStroke(InputStroke stroke) inout scope return { 906 907 auto modifiers = stroke.modifiers; 908 909 foreach (i, layer; boundInputs) { 910 911 // Found a matching layer 912 if (modifiers == layer.modifiers) { 913 914 return &boundInputs[i]; 915 916 } 917 918 // Stop if other layers are less complex 919 if (modifiers.length > layer.modifiers.length) break; 920 921 } 922 923 return null; 924 925 } 926 927 /// Bind a key stroke or button to given input action. Multiple key strokes are allowed to match given action. 928 void bindInput(InputActionID action, InputStroke stroke) 929 in (stroke.length != 0) 930 do { 931 932 // TODO tests 933 934 auto binding = InputBinding(action, stroke.input[$-1]); 935 936 // Layer exists, add the binding 937 if (auto layer = layerForStroke(stroke)) { 938 939 layer.bindings ~= binding; 940 941 } 942 943 // Layer doesn't exist, create it 944 else { 945 946 auto modifiers = stroke.modifiers; 947 auto newLayer = InputLayer(modifiers, [binding]); 948 bool found; 949 950 // Insert the layer before any layer that is less complex 951 foreach (i, layer; boundInputs) { 952 953 if (modifiers.length > layer.modifiers.length) { 954 955 boundInputs = boundInputs[0..i] ~ newLayer ~ boundInputs[i..$]; 956 found = true; 957 break; 958 959 } 960 961 } 962 963 if (!found) boundInputs ~= newLayer; 964 965 assert(isSorted(boundInputs)); 966 967 } 968 969 } 970 971 /// Bind a key stroke or button to given input action, replacing any previously bound inputs. 972 void bindInputReplace(InputActionID action, InputStroke stroke) 973 in (stroke.length != 0) 974 do { 975 976 import std.array; 977 978 // Find a matching layer 979 if (auto layer = layerForStroke(stroke)) { 980 981 // Remove any stroke that matches 982 layer.bindings = layer.bindings.filter!(a => a.trigger == stroke.input[$-1]).array; 983 984 // Insert the binding 985 layer.bindings ~= InputBinding(action, stroke.input[$-1]); 986 987 } 988 989 // Layer doesn't exist, bind it the straightforward way 990 else bindInput(action, stroke); 991 992 } 993 994 /// List actions in the tree, remove finished actions while iterating. 995 auto filterActions() { 996 997 struct ActionIterator { 998 999 LayoutTree* tree; 1000 1001 int opApply(int delegate(TreeAction) @safe fun) { 1002 1003 tree._actionAccessCounter++; 1004 scope (exit) tree._actionAccessCounter--; 1005 1006 // Regular access 1007 if (tree._actionAccessCounter == 1) { 1008 1009 for (auto range = tree.actions[]; !range.empty; ) { 1010 1011 // Yield the item 1012 auto result = fun(range.front); 1013 1014 // If finished, remove from the queue 1015 if (range.front.toStop) tree.actions.popFirstOf(range); 1016 1017 // Continue to the next item 1018 else range.popFront(); 1019 1020 // Stop iteration if requested 1021 if (result) return result; 1022 1023 } 1024 1025 } 1026 1027 // Nested access 1028 else { 1029 1030 for (auto range = tree.actions[]; !range.empty; ) { 1031 1032 auto front = range.front; 1033 range.popFront(); 1034 1035 // Ignore stopped items 1036 if (front.toStop) continue; 1037 1038 // Yield the item 1039 if (auto result = fun(front)) { 1040 1041 return result; 1042 1043 } 1044 1045 } 1046 1047 } 1048 1049 // Run new actions too 1050 foreach (action; tree.context.actions) { 1051 1052 if (auto result = fun(action)) { 1053 return result; 1054 } 1055 1056 } 1057 1058 return 0; 1059 1060 } 1061 1062 } 1063 1064 return ActionIterator(&this); 1065 1066 } 1067 1068 /// Intersect the given rectangle against current scissor area. 1069 Rectangle intersectScissors(Rectangle rect) { 1070 1071 import fluid.utils : intersect; 1072 1073 // No limit applied 1074 if (scissors is scissors.init) return rect; 1075 1076 return intersect(rect, scissors); 1077 1078 } 1079 1080 /// Start scissors mode. 1081 /// Returns: Previous scissors mode value. Pass that value to `popScissors`. 1082 Rectangle pushScissors(Rectangle rect) { 1083 1084 const lastScissors = scissors; 1085 1086 // Intersect with the current scissors rectangle. 1087 io.area = scissors = intersectScissors(rect); 1088 1089 return lastScissors; 1090 1091 } 1092 1093 void popScissors(Rectangle lastScissorsMode) @trusted { 1094 1095 // Pop the stack 1096 scissors = lastScissorsMode; 1097 1098 // No scissors left 1099 if (scissors is scissors.init) { 1100 1101 // Restore full draw area 1102 backend.restoreArea(); 1103 1104 } 1105 1106 else { 1107 1108 // Start again 1109 backend.area = scissors; 1110 1111 } 1112 1113 } 1114 1115 /// Fetch tree events (e.g. actions) 1116 package void poll() { 1117 1118 // Run texture reaper 1119 io.reaper.check(); 1120 1121 // Reset all actions 1122 downActions.clear(); 1123 activeActions.clear(); 1124 1125 // Test all bindings 1126 foreach (layer; boundInputs) { 1127 1128 // Check if the layer is active 1129 if (!layer.modifiers.isDown(backend)) continue; 1130 1131 // Found an active layer, test all bound strokes 1132 foreach (binding; layer.bindings) { 1133 1134 // Register held-down actions 1135 if (InputStroke.isItemDown(backend, binding.trigger)) { 1136 1137 downActions ~= binding; 1138 1139 } 1140 1141 // Register triggered actions 1142 if (InputStroke.isItemActive(backend, binding.trigger)) { 1143 1144 activeActions ~= binding; 1145 1146 } 1147 1148 } 1149 1150 // End on this layer 1151 break; 1152 1153 } 1154 1155 } 1156 1157 } 1158 1159 @("Legacy: LayoutTree.isHovered is true when a node is hovered, false when not (abandoned)") 1160 unittest { 1161 1162 import fluid.space; 1163 import fluid.label; 1164 import fluid.structs; 1165 import fluid.default_theme; 1166 1167 auto io = new HeadlessBackend; 1168 auto text = label("Hello, World!"); 1169 auto root = vspace( 1170 layout!"fill", 1171 nullTheme, 1172 text 1173 ); 1174 1175 io.mousePosition = Vector2(5, 5); 1176 root.io = io; 1177 root.draw(); 1178 1179 assert(root.tree.hover is text); 1180 assert(root.tree.isHovered); 1181 1182 io.nextFrame; 1183 io.mousePosition = Vector2(-1, -1); 1184 root.draw(); 1185 1186 assert(root.tree.hover !is text); 1187 assert(!root.tree.isHovered); 1188 1189 io.nextFrame; 1190 io.mousePosition = Vector2(5, 50); 1191 root.draw(); 1192 1193 assert(root.tree.hover !is text); 1194 assert(!root.tree.isHovered); 1195 1196 }