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