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