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