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 (focus 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 import fluid.theme : Breadcrumbs; 382 383 // Nodes 384 public { 385 386 /// Root node of the tree. 387 Node root; 388 389 /// Top-most hovered node in the tree. 390 Node hover; 391 392 /// Currently focused node. 393 /// 394 /// Changing this value directly is discouraged. Some nodes might not want the focus! Be gentle, call 395 /// `FluidFocusable.focus()` instead and let the node set the value on its own. 396 FluidFocusable focus; 397 398 /// Deepest hovered scrollable node. 399 FluidScrollable scroll; 400 401 } 402 403 // Input 404 public { 405 406 /// Focus direction data. 407 FocusDirection focusDirection; 408 409 /// Padding box of the currently focused node. Only available after the node has been drawn. 410 /// 411 /// See_also: `focusDirection.lastFocusBox`. 412 Rectangle focusBox; 413 414 /// Tree actions queued to execute during next draw. 415 DList!TreeAction actions; 416 417 /// Input strokes bound to emit given action signals. 418 /// 419 /// Input layers have to be sorted. 420 InputLayer[] boundInputs; 421 422 invariant(boundInputs.isSorted); 423 424 /// Actions that are currently held down. 425 DList!InputBinding downActions; 426 427 /// Actions that have just triggered. 428 DList!InputBinding activeActions; 429 430 /// Access to core input and output facilities. 431 FluidBackend backend; 432 alias io = backend; 433 434 /// Check if keyboard input was handled; updated after rendering has completed. 435 bool keyboardHandled; 436 437 } 438 439 /// Miscelleanous, technical properties. 440 public { 441 442 /// Current node drawing depth. 443 uint depth; 444 445 /// Current rectangle drawing is limited to. 446 Rectangle scissors; 447 448 /// True if the current tree branch is marked as disabled (doesn't take input). 449 bool isBranchDisabled; 450 451 /// Current breadcrumbs. These are assigned to any node that is resized or drawn at the time. 452 /// 453 /// Any node that introduces its own breadcrumbs will push onto this stack, and pop once finished. 454 Breadcrumbs breadcrumbs; 455 456 } 457 458 /// Incremented for every `filterActions` access to prevent nested accesses from breaking previously made ranges. 459 private int _actionAccessCounter; 460 461 /// Create a new tree with the given node as its root, and using the given backend for I/O. 462 this(Node root, FluidBackend backend) { 463 464 this.root = root; 465 this.backend = backend; 466 this.restoreDefaultInputBinds(); 467 468 } 469 470 /// Create a new tree with the given node as its root. Use the default backend, if any is present. 471 this(Node root) { 472 473 this(root, defaultFluidBackend); 474 475 assert(backend, "Cannot create LayoutTree; no backend was chosen, and no default is set."); 476 477 } 478 479 /// Returns true if this branch requested a resize or is pending a resize. 480 bool resizePending() const { 481 482 return root.resizePending; 483 484 } 485 486 /// Queue an action to perform while iterating the tree. 487 /// 488 /// Avoid using this; most of the time `Node.queueAction` is what you want. `LayoutTree.queueAction` might fire 489 /// too early 490 void queueAction(TreeAction action) 491 in (action, "Invalid action queued") 492 do { 493 494 actions ~= action; 495 496 } 497 498 /// Restore defaults for given actions. 499 void restoreDefaultInputBinds() { 500 501 /// Get the ID of an input action. 502 auto bind(alias a, T)(T arg) { 503 504 return InputBinding(InputAction!a.id, InputStroke.Item(arg)); 505 506 } 507 508 with (FluidInputAction) { 509 510 // System-independent keys 511 auto universalShift = InputLayer( 512 InputStroke(KeyboardKey.leftShift), 513 [ 514 bind!focusPrevious(KeyboardKey.tab), 515 bind!entryPrevious(KeyboardKey.tab), 516 bind!outdent(KeyboardKey.tab), 517 bind!selectPreviousChar(KeyboardKey.left), 518 bind!selectNextChar(KeyboardKey.right), 519 bind!selectPreviousLine(KeyboardKey.up), 520 bind!selectNextLine(KeyboardKey.down), 521 bind!selectToLineStart(KeyboardKey.home), 522 bind!selectToLineEnd(KeyboardKey.end), 523 bind!breakLine(KeyboardKey.enter), 524 bind!contextMenu(KeyboardKey.f10), 525 ] 526 ); 527 auto universal = InputLayer( 528 InputStroke(), 529 [ 530 // Press 531 bind!press(MouseButton.left), 532 bind!press(KeyboardKey.enter), 533 bind!press(GamepadButton.cross), 534 535 // Submit 536 bind!submit(KeyboardKey.enter), 537 bind!submit(GamepadButton.cross), 538 539 // Cancel 540 bind!cancel(KeyboardKey.escape), 541 bind!cancel(GamepadButton.circle), 542 543 // Menu 544 bind!contextMenu(MouseButton.right), 545 bind!contextMenu(KeyboardKey.contextMenu), 546 547 // Tabbing; index-focus 548 bind!focusPrevious(GamepadButton.leftButton), 549 bind!focusNext(KeyboardKey.tab), 550 bind!focusNext(GamepadButton.rightButton), 551 552 // Directional focus 553 bind!focusLeft(KeyboardKey.left), 554 bind!focusLeft(GamepadButton.dpadLeft), 555 bind!focusRight(KeyboardKey.right), 556 bind!focusRight(GamepadButton.dpadRight), 557 bind!focusUp(KeyboardKey.up), 558 bind!focusUp(GamepadButton.dpadUp), 559 bind!focusDown(KeyboardKey.down), 560 bind!focusDown(GamepadButton.dpadDown), 561 562 // Text input 563 bind!backspace(KeyboardKey.backspace), 564 bind!deleteChar(KeyboardKey.delete_), 565 bind!breakLine(KeyboardKey.enter), 566 bind!previousChar(KeyboardKey.left), 567 bind!nextChar(KeyboardKey.right), 568 bind!previousLine(KeyboardKey.up), 569 bind!nextLine(KeyboardKey.down), 570 bind!entryPrevious(KeyboardKey.up), 571 bind!entryPrevious(GamepadButton.dpadUp), 572 bind!entryNext(KeyboardKey.down), 573 bind!entryNext(KeyboardKey.tab), 574 bind!entryNext(GamepadButton.dpadDown), 575 bind!toLineStart(KeyboardKey.home), 576 bind!toLineEnd(KeyboardKey.end), 577 bind!insertTab(KeyboardKey.tab), 578 579 // Scrolling 580 bind!scrollLeft(KeyboardKey.left), 581 bind!scrollLeft(GamepadButton.dpadLeft), 582 bind!scrollRight(KeyboardKey.right), 583 bind!scrollRight(GamepadButton.dpadRight), 584 bind!scrollUp(KeyboardKey.up), 585 bind!scrollUp(GamepadButton.dpadUp), 586 bind!scrollDown(KeyboardKey.down), 587 bind!scrollDown(GamepadButton.dpadDown), 588 bind!pageUp(KeyboardKey.pageUp), 589 bind!pageDown(KeyboardKey.pageDown), 590 ] 591 ); 592 593 // TODO universal left/right key 594 version (Fluid_MacKeyboard) 595 boundInputs = [ 596 597 // Shift + Command 598 InputLayer( 599 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftSuper), 600 [ 601 // TODO Command should *expand selection* on macOS instead of current 602 // toLineStart/toLineEnd behavior 603 bind!selectToLineStart(KeyboardKey.left), 604 bind!selectToLineEnd(KeyboardKey.right), 605 bind!selectToStart(KeyboardKey.up), 606 bind!selectToEnd(KeyboardKey.down), 607 bind!redo(KeyboardKey.z), 608 ] 609 ), 610 611 // Shift + Option 612 InputLayer( 613 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftAlt), 614 [ 615 bind!selectPreviousWord(KeyboardKey.left), 616 bind!selectNextWord(KeyboardKey.right), 617 ] 618 ), 619 620 // Command 621 InputLayer( 622 InputStroke(KeyboardKey.leftSuper), 623 [ 624 bind!toLineStart(KeyboardKey.left), 625 bind!toLineEnd(KeyboardKey.right), 626 bind!toStart(KeyboardKey.up), 627 bind!toEnd(KeyboardKey.down), 628 bind!selectAll(KeyboardKey.a), 629 bind!copy(KeyboardKey.c), 630 bind!cut(KeyboardKey.x), 631 bind!paste(KeyboardKey.v), 632 bind!undo(KeyboardKey.z), 633 bind!redo(KeyboardKey.y), 634 bind!submit(KeyboardKey.enter), 635 ] 636 ), 637 638 // Option 639 InputLayer( 640 InputStroke(KeyboardKey.leftAlt), 641 [ 642 bind!deleteWord(KeyboardKey.delete_), 643 bind!backspaceWord(KeyboardKey.backspace), 644 bind!previousWord(KeyboardKey.left), 645 bind!nextWord(KeyboardKey.right), 646 ] 647 ), 648 649 // Control 650 InputLayer( 651 InputStroke(KeyboardKey.leftControl), 652 [ 653 bind!backspaceWord(KeyboardKey.w), // emacs & vim 654 bind!entryPrevious(KeyboardKey.k), // vim 655 bind!entryPrevious(KeyboardKey.p), // emacs 656 bind!entryNext(KeyboardKey.j), // vim 657 bind!entryNext(KeyboardKey.n), // emacs 658 ] 659 ), 660 661 universalShift, 662 universal, 663 ]; 664 else 665 boundInputs = [ 666 667 InputLayer( 668 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftControl), 669 [ 670 bind!selectPreviousWord(KeyboardKey.left), 671 bind!selectNextWord(KeyboardKey.right), 672 bind!selectToStart(KeyboardKey.home), 673 bind!selectToEnd(KeyboardKey.end), 674 bind!redo(KeyboardKey.z), 675 ] 676 ), 677 678 InputLayer( 679 InputStroke(KeyboardKey.leftControl), 680 [ 681 bind!deleteWord(KeyboardKey.delete_), 682 bind!backspaceWord(KeyboardKey.backspace), 683 bind!backspaceWord(KeyboardKey.w), // emacs & vim 684 bind!entryPrevious(KeyboardKey.k), // vim 685 bind!entryPrevious(KeyboardKey.p), // emacs 686 bind!entryNext(KeyboardKey.j), // vim 687 bind!entryNext(KeyboardKey.n), // emacs 688 bind!previousWord(KeyboardKey.left), 689 bind!nextWord(KeyboardKey.right), 690 bind!selectAll(KeyboardKey.a), 691 bind!copy(KeyboardKey.c), 692 bind!cut(KeyboardKey.x), 693 bind!paste(KeyboardKey.v), 694 bind!undo(KeyboardKey.z), 695 bind!redo(KeyboardKey.y), 696 bind!toStart(KeyboardKey.home), 697 bind!toEnd(KeyboardKey.end), 698 699 // Submit with ctrl+enter 700 bind!submit(KeyboardKey.enter), 701 ] 702 ), 703 704 InputLayer( 705 InputStroke(KeyboardKey.leftAlt), 706 [ 707 bind!entryUp(KeyboardKey.up), 708 ] 709 ), 710 711 universalShift, 712 universal, 713 714 ]; 715 716 } 717 718 } 719 720 /// Remove any inputs bound to given input action. 721 /// Returns: `true` if the action was cleared. 722 bool clearBoundInput(InputActionID action) { 723 724 import std.array; 725 726 // TODO test 727 728 bool found; 729 730 foreach (ref layer; boundInputs) { 731 732 const oldLength = layer.bindings.length; 733 734 layer.bindings = layer.bindings.filter!(a => a.action == action).array; 735 736 if (layer.bindings.length != oldLength) { 737 found = true; 738 } 739 740 } 741 742 return found; 743 744 } 745 746 /// Find a layer for the given input stroke. 747 /// Returns: Layer found for the given input stroke. `null` if none found. 748 inout(InputLayer)* layerForStroke(InputStroke stroke) inout scope return { 749 750 auto modifiers = stroke.modifiers; 751 752 foreach (i, layer; boundInputs) { 753 754 // Found a matching layer 755 if (modifiers == layer.modifiers) { 756 757 return &boundInputs[i]; 758 759 } 760 761 // Stop if other layers are less complex 762 if (modifiers.length > layer.modifiers.length) break; 763 764 } 765 766 return null; 767 768 } 769 770 /// Bind a key stroke or button to given input action. Multiple key strokes are allowed to match given action. 771 void bindInput(InputActionID action, InputStroke stroke) 772 in (stroke.length != 0) 773 do { 774 775 // TODO tests 776 777 auto binding = InputBinding(action, stroke.input[$-1]); 778 779 // Layer exists, add the binding 780 if (auto layer = layerForStroke(stroke)) { 781 782 layer.bindings ~= binding; 783 784 } 785 786 // Layer doesn't exist, create it 787 else { 788 789 auto modifiers = stroke.modifiers; 790 auto newLayer = InputLayer(modifiers, [binding]); 791 bool found; 792 793 // Insert the layer before any layer that is less complex 794 foreach (i, layer; boundInputs) { 795 796 if (modifiers.length > layer.modifiers.length) { 797 798 boundInputs = boundInputs[0..i] ~ newLayer ~ boundInputs[i..$]; 799 found = true; 800 break; 801 802 } 803 804 } 805 806 if (!found) boundInputs ~= newLayer; 807 808 assert(isSorted(boundInputs)); 809 810 } 811 812 } 813 814 /// Bind a key stroke or button to given input action, replacing any previously bound inputs. 815 void bindInputReplace(InputActionID action, InputStroke stroke) 816 in (stroke.length != 0) 817 do { 818 819 import std.array; 820 821 // Find a matching layer 822 if (auto layer = layerForStroke(stroke)) { 823 824 // Remove any stroke that matches 825 layer.bindings = layer.bindings.filter!(a => a.trigger == stroke.input[$-1]).array; 826 827 // Insert the binding 828 layer.bindings ~= InputBinding(action, stroke.input[$-1]); 829 830 } 831 832 // Layer doesn't exist, bind it the straightforward way 833 else bindInput(action, stroke); 834 835 } 836 837 /// List actions in the tree, remove finished actions while iterating. 838 auto filterActions() { 839 840 struct ActionIterator { 841 842 LayoutTree* tree; 843 844 int opApply(int delegate(TreeAction) @safe fun) { 845 846 tree._actionAccessCounter++; 847 scope (exit) tree._actionAccessCounter--; 848 849 // Regular access 850 if (tree._actionAccessCounter == 1) { 851 852 for (auto range = tree.actions[]; !range.empty; ) { 853 854 // Yield the item 855 auto result = fun(range.front); 856 857 // If finished, remove from the queue 858 if (range.front.toStop) tree.actions.popFirstOf(range); 859 860 // Continue to the next item 861 else range.popFront(); 862 863 // Stop iteration if requested 864 if (result) return result; 865 866 } 867 868 } 869 870 // Nested access 871 else { 872 873 for (auto range = tree.actions[]; !range.empty; ) { 874 875 auto front = range.front; 876 range.popFront(); 877 878 // Ignore stopped items 879 if (front.toStop) continue; 880 881 // Yield the item 882 if (auto result = fun(front)) { 883 884 return result; 885 886 } 887 888 } 889 890 } 891 892 return 0; 893 894 } 895 896 } 897 898 return ActionIterator(&this); 899 900 } 901 902 /// Intersect the given rectangle against current scissor area. 903 Rectangle intersectScissors(Rectangle rect) { 904 905 import std.algorithm : min, max; 906 907 // No limit applied 908 if (scissors is scissors.init) return rect; 909 910 Rectangle result; 911 912 // Intersect 913 result.x = max(rect.x, scissors.x); 914 result.y = max(rect.y, scissors.y); 915 result.w = max(0, min(rect.x + rect.w, scissors.x + scissors.w) - result.x); 916 result.h = max(0, min(rect.y + rect.h, scissors.y + scissors.h) - result.y); 917 918 return result; 919 920 } 921 922 /// Start scissors mode. 923 /// Returns: Previous scissors mode value. Pass that value to `popScissors`. 924 Rectangle pushScissors(Rectangle rect) { 925 926 const lastScissors = scissors; 927 928 // Intersect with the current scissors rectangle. 929 io.area = scissors = intersectScissors(rect); 930 931 return lastScissors; 932 933 } 934 935 void popScissors(Rectangle lastScissorsMode) @trusted { 936 937 // Pop the stack 938 scissors = lastScissorsMode; 939 940 // No scissors left 941 if (scissors is scissors.init) { 942 943 // Restore full draw area 944 backend.restoreArea(); 945 946 } 947 948 else { 949 950 // Start again 951 backend.area = scissors; 952 953 } 954 955 } 956 957 /// Fetch tree events (e.g. actions) 958 package void poll() { 959 960 // Run texture reaper 961 io.reaper.check(); 962 963 // Reset all actions 964 downActions.clear(); 965 activeActions.clear(); 966 967 // Test all bindings 968 foreach (layer; boundInputs) { 969 970 // Check if the layer is active 971 if (!layer.modifiers.isDown(backend)) continue; 972 973 // Found an active layer, test all bound strokes 974 foreach (binding; layer.bindings) { 975 976 // Register held-down actions 977 if (InputStroke.isItemDown(backend, binding.trigger)) { 978 979 downActions ~= binding; 980 981 } 982 983 // Register triggered actions 984 if (InputStroke.isItemActive(backend, binding.trigger)) { 985 986 activeActions ~= binding; 987 988 } 989 990 } 991 992 // End on this layer 993 break; 994 995 } 996 997 } 998 999 }