1 modulefluid.tree;
2 3 importstd.conv;
4 importstd.math;
5 importstd.container;
6 importstd.algorithm;
7 8 importfluid.node;
9 importfluid.input;
10 importfluid.style;
11 importfluid.backend;
12 13 importfluid.future.pipe;
14 importfluid.future.context;
15 16 17 @safe:
18 19 20 version (OSX)
21 version = Fluid_MacKeyboard;
22 23 ///24 structFocusDirection {
25 26 structWithPriority {
27 28 /// Pick priority based on tree distance from the focused node.29 intpriority;
30 31 /// Square of the distance between this node and the focused node.32 floatdistance2;
33 34 /// The node.35 FluidFocusablenode;
36 37 aliasnodethis;
38 39 }
40 41 /// Available space box of the focused item after last frame.42 RectanglelastFocusBox;
43 44 /// Nodes that may get focus with tab navigation.45 FluidFocusableprevious, next;
46 47 /// First and last focusable nodes in the tree.48 FluidFocusablefirst, last;
49 50 /// Focusable nodes, by direction from the focused node.51 WithPriority[4] positional;
52 53 /// Focus priority for the currently drawn node.54 ///55 /// Increased until the focused node is found, decremented afterwards. As a result, values will be the highest for56 /// nodes near the focused one. Changes with tree depth rather than individual nodes.57 intpriority;
58 59 private {
60 61 /// Value `priority` is summed with on each step. `1` before finding the focused node, `-1` after.62 intpriorityDirection = 1;
63 64 /// Current tree depth.65 uintdepth;
66 67 }
68 69 /// Update focus info with the given node. Automatically called when a node is drawn, shouldn't be called manually.70 ///71 /// `previous` will be the last focusable node encountered before the focused node, and `next` will be the first one72 /// after. `first` and `last will be the last focusable nodes in the entire tree.73 ///74 /// Params:75 /// current = Node to update the focus info with.76 /// box = Box defining node boundaries (focus box)77 /// depth = Current tree depth. Pass in `tree.depth`.78 voidupdate(Nodecurrent, Rectanglebox, uintdepth)
79 in (current !isnull, "Current node must not be null")
80 do {
81 82 importstd.algorithm : either;
83 84 autocurrentFocusable = cast(FluidFocusable) current;
85 86 // Count focus priority87 {
88 89 // Get depth difference since last time90 constintdepthDiff = depth - this.depth;
91 92 // Count steps in change of depth93 priority += priorityDirection * abs(depthDiff);
94 95 // Update depth96 this.depth = depth;
97 98 }
99 100 // Stop if the current node can't take focus101 if (!currentFocusable) return;
102 103 // And it DOES have focus104 if (current.tree.focusiscurrentFocusable) {
105 106 // Mark the node preceding it to the last encountered focusable node107 previous = last;
108 109 // Clear the next node, so it can be overwritten by a correct value.110 next = null;
111 112 // Reverse priority target113 priorityDirection = -1;
114 115 }
116 117 else {
118 119 // Update positional focus120 updatePositional(currentFocusable, box);
121 122 // There's no node to take focus next, set it now123 if (nextisnull) next = currentFocusable;
124 125 }
126 127 128 // Set the current node as the first focusable, if true129 if (firstisnull) first = currentFocusable;
130 131 // Replace the last132 last = currentFocusable;
133 134 }
135 136 /// Check the given node's position and update `positional` to match.137 privatevoidupdatePositional(FluidFocusablenode, Rectanglebox) {
138 139 // Note: This might give false-positives if the focused node has changed during this frame140 141 // Check each direction142 foreach (i, refotherNode; positional) {
143 144 constside = cast(Style.Side) i;
145 constdist = distance2(box, side);
146 147 // If a node took this spot before148 if (otherNode !isnull) {
149 150 // Ignore if the other node has higher priority151 if (otherNode.priority > priority) continue;
152 153 // If priorities are equal, check if we're closer than the other node154 if (otherNode.priority == priority155 && otherNode.distance2 < dist) continue;
156 157 }
158 159 // Check if this node matches the direction160 if (checkDirection(box, side)) {
161 162 // Replace the node163 otherNode = WithPriority(priority, dist, node);
164 165 }
166 167 }
168 169 }
170 171 /// Check if the given box is located to the given side of the focus box.172 boolcheckDirection(Rectanglebox, Style.Sideside) {
173 174 // Distance between box sides facing each other.175 //176 // ↓ lastFocusBox ↓ box177 // +======+ +------+178 // | | | |179 // | | ~~~~~~ | |180 // | | | |181 // +======+ +------+182 // side ↑ ↑ side.reverse183 constdistanceExternal = lastFocusBox.getSide(side) - box.getSide(side.reverse);
184 185 // Distance between corresponding box sides.186 //187 // ↓ lastFocusBox ↓ box188 // +======+ +------+189 // | | : |190 // | | ~~~~~~~~~~~~~ |191 // | | : |192 // +======+ +------+193 // side ↑ side ↑194 constdistanceInternal = lastFocusBox.getSide(side) - box.getSide(side);
195 196 // The condition for the return value to be true, is for distanceInternal to be greater than distanceExternal.197 // This is not the case in the opposite situation.198 //199 // For example, if we're checking if the box is on the *right* of lastFocusBox:200 //201 // trueish scenario: falseish scenario:202 // Box is to the right of lastFocusBox Box is the left of lastFocusBox203 //204 // ↓ lastFocusBox ↓ box ↓ box ↓ lastFocusBox205 // +======+ +------+ +------+ +======+206 // | | ~~~~~~ : | external | ~~~~~~~~~~~~~~~~~~~~ | external207 // | | : | < | : : | >208 // | | ~~~~~~~~~~~~~ | internal | : ~~~~~~~~~~~~~ | internal209 // +======+ +------+ +------+ +======+210 // side ↑ ↑ side.reverse side ↑ side ↑211 constcondition = abs(distanceInternal) > abs(distanceExternal);
212 213 // ↓ box There is an edgecase though. If one box entirely overlaps the other on one axis, we214 // +--------------------+ might end up with unwanted behavior, for example, in a ScrollFrame, focus might215 // | ↓ lastFocusBox | switch to the scrollbar instead of a child, as we would normally expect.216 // | +============+ |217 // | | | | For this reason, we require both `distanceInternal` and `distanceExternal` to have218 // +---| |---+ the same sign, as it normally would, but not here.219 // | |220 // +============+ One can still navigate to the `box` using controls for the other axis.221 returncondition222 && distanceInternal * distanceExternal >= 0;
223 224 }
225 226 /// Get the square of the distance between given box and `lastFocusBox`.227 floatdistance2(Rectanglebox, Style.Sideside) {
228 229 /// Get the center of given rectangle on the axis opposite to the results of getSide.230 floatcenter(Rectanglerect) {
231 232 returnside == Style.Side.left || side == Style.Side.right233 ? rect.y + rect.height234 : rect.x + rect.width;
235 236 }
237 238 // Distance between box sides facing each other, see `checkDirection`239 constdistanceExternal = lastFocusBox.getSide(side) - box.getSide(side.reverse);
240 241 /// Distance between centers of the boxes on the other axis242 constdistanceOpposite = center(box) - center(lastFocusBox);
243 244 returndistanceExternal^^2 + distanceOpposite^^2;
245 246 }
247 248 }
249 250 /// A class for iterating over the node tree.251 abstractclassTreeAction : Publisher!() {
252 253 public {
254 255 /// Node to descend into; `beforeDraw` and `afterDraw` will only be emitted for this node and its children.256 ///257 /// May be null to enable iteration over the entire tree.258 NodestartNode;
259 260 /// If true, this action is complete and no callbacks should be ran.261 ///262 /// Overloads of the same callbacks will still be called for the event that prompted stopping.263 booltoStop; // this should be private264 265 /// Keeps track of the number of times the action has been started or stopped. Every start and every stop266 /// bumps the generation number.267 ///268 /// The generation number is used to determine if the action runner should continue or discontinue the action.269 /// If the number is greater than the one the runner stored at the time it was scheduled, it will stop running.270 /// This means that if an action is restarted, the old run will be unregistered, preventing the action from271 /// running twice at a time.272 ///273 /// Only applies to actions started using `Node.startAction`, introduced in 0.7.2, and not `Node.runAction`.274 intgeneration;
275 276 }
277 278 private {
279 280 /// Subscriber for events, i.e. `then`281 Event!() _finished;
282 283 /// Set to true once the action has descended into `startNode`.284 bool_inStartNode;
285 286 /// Set to true once `beforeTree` is called. Set to `false` afterwards.287 bool_inTree;
288 289 }
290 291 /// Returns: True if the tree is currently drawing `startNode` or any of its children.292 boolinStartNode() const {
293 return_inStartNode;
294 }
295 296 /// Returns: True if the tree has been entered. Set to true on `beforeTree` and to false on `afterTree`.297 boolinTree() const {
298 return_inTree;
299 }
300 301 /// Remove all event handlers attached to this tree.302 voidclearSubscribers() {
303 _finished.clearSubscribers();
304 }
305 306 overridefinalvoidsubscribe(Subscriber!() subscriber) {
307 _finished.subscribe(subscriber);
308 }
309 310 /// Stop the action.311 ///312 /// No further hooks will be triggered after calling this, and the action will soon be removed from the list313 /// of running actions. Overloads of the same hook that called `stop` may still be called.314 finalvoidstop() {
315 316 if (toStop) return;
317 318 // Perform the stop319 generation++;
320 toStop = true;
321 stopped();
322 323 // Reset state324 _inStartNode = false;
325 _inTree = false;
326 327 }
328 329 /// Called whenever this action is started — added to the list of running actions in the `LayoutTree`330 /// or `TreeContext`.331 ///332 /// This hook may not be called immediately when added through `node.queueAction` or `node.startAction`;333 /// it will wait until the node's first resize so it can connect to the tree.334 voidstarted() {
335 336 }
337 338 /// Called whenever this action is stopped by calling `stop`.339 ///340 /// This can be used to trigger user-assigned callbacks. Call `super.stopped()` when overriding to make sure all341 /// finish hooks are called.342 voidstopped() {
343 _finished();
344 }
345 346 /// Determine whether `beforeTree` and `afterTree` should be called.347 ///348 /// By default, `afterTree` is disabled if `beforeTree` wasn't called before.349 /// Subclasses may change this to adjust this behavior.350 ///351 /// Returns:352 /// For `filterBeforeTree`, true if `beforeTree` is to be called.353 /// For `filterAfterTree`, true if `afterTree` is to be called.354 boolfilterBeforeTree() {
355 returntrue;
356 }
357 358 /// ditto359 boolfilterAfterTree() {
360 returninTree;
361 }
362 363 /// Determine whether `beforeDraw` and `afterDraw` should be called for the given node.364 ///365 /// By default, this is used to filter out all nodes except for `startNode` and its children, and to keep366 /// the action from starting in the middle of the tree.367 /// Subclasses may change this to adjust this behavior.368 ///369 /// Params:370 /// node = Node that is subject to the hook call.371 /// Returns:372 /// For `filterBeforeDraw`, true if `beforeDraw` is to be called for this node.373 /// For `filterAfterDraw`, true if `afterDraw` is to be called for this node.374 boolfilterBeforeDraw(Nodenode) {
375 376 // Not in tree377 if (!inTree) returnfalse;
378 379 // Start mode must have been reached380 returnstartNodeisnull || inStartNode;
381 382 }
383 384 /// ditto385 boolfilterAfterDraw(Nodenode) {
386 387 // Not in tree388 if (!inTree) returnfalse;
389 390 // Start mode must have been reached391 returnstartNodeisnull || inStartNode;
392 393 }
394 395 /// Called before the tree is drawn. Keep in mind this might not be called if the action is started when tree396 /// iteration has already begun.397 /// Params:398 /// root = Root of the tree.399 /// viewport = Screen space for the node.400 voidbeforeTree(Noderoot, Rectangleviewport) { }
401 402 finalpackagevoidbeforeTreeImpl(Noderoot, Rectangleviewport) {
403 404 _inTree = true;
405 406 if (filterBeforeTree()) {
407 beforeTree(root, viewport);
408 }
409 410 }
411 412 /// Called before a node is resized.413 voidbeforeResize(Nodenode, Vector2viewportSpace) { }
414 415 finalpackagevoidbeforeResizeImpl(Nodenode, Vector2viewport) {
416 beforeResize(node, viewport);
417 }
418 419 /// Called after a node is resized.420 voidafterResize(Nodenode, Vector2viewportSpace) { }
421 422 finalpackagevoidafterResizeImpl(Nodenode, Vector2viewport) {
423 afterResize(node, viewport);
424 }
425 426 /// Called before each `drawImpl` call of any node in the tree, so supplying parent nodes before their children.427 ///428 /// This might not be called if the node is offscreen. If you need to find all nodes, try `beforeResize`.429 ///430 /// Params:431 /// node = Node that's about to be drawn.432 /// space = Space given for the node.433 /// paddingBox = Padding box of the node.434 /// contentBox = Content box of teh node.435 voidbeforeDraw(Nodenode, Rectanglespace, RectanglepaddingBox, RectanglecontentBox) { }
436 437 /// ditto438 voidbeforeDraw(Nodenode, Rectanglespace) { }
439 440 /// internal441 finalpackagevoidbeforeDrawImpl(Nodenode, Rectanglespace, RectanglepaddingBox, RectanglecontentBox) {
442 443 // Open the start branch444 if (startNode && node.opEquals(startNode)) {
445 _inStartNode = true;
446 }
447 448 // Run the hooks if the filter passes449 if (filterBeforeDraw(node)) {
450 beforeDraw(node, space, paddingBox, contentBox);
451 beforeDraw(node, space);
452 }
453 454 }
455 456 /// Called after each `drawImpl` call of any node in the tree, so supplying children nodes before their parents.457 ///458 /// This might not be called if the node is offscreen. If you need to find all nodes, try `beforeResize`.459 ///460 /// Params:461 /// node = Node that's about to be drawn.462 /// space = Space given for the node.463 /// paddingBox = Padding box of the node.464 /// contentBox = Content box of teh node.465 voidafterDraw(Nodenode, Rectanglespace, RectanglepaddingBox, RectanglecontentBox) { }
466 467 /// ditto468 voidafterDraw(Nodenode, Rectanglespace) { }
469 470 /// internal471 finalpackagevoidafterDrawImpl(Nodenode, Rectanglespace, RectanglepaddingBox, RectanglecontentBox) {
472 473 // Run the filter474 if (filterAfterDraw(node)) {
475 afterDraw(node, space, paddingBox, contentBox);
476 afterDraw(node, space);
477 }
478 479 // Close the start branch480 if (startNode && node.opEquals(startNode)) {
481 _inStartNode = false;
482 }
483 484 }
485 486 /// Called after the tree is drawn. Called before input events, so they can assume actions have completed.487 ///488 /// By default, calls `stop()` preventing the action from evaluating during next draw.489 voidafterTree() {
490 491 stop();
492 493 }
494 495 finalpackagevoidafterTreeImpl() {
496 497 if (filterAfterTree()) {
498 afterTree();
499 }
500 _inTree = false;
501 502 }
503 504 /// Hook that triggers after processing input. Useful if post-processing is necessary to, perhaps, implement505 /// fallback input.506 ///507 /// Warning: This will **not trigger** unless `afterTree` is overrided not to stop the action. If you make use of508 /// this, make sure to make the action stop in this method.509 ///510 /// Params:511 /// keyboardHandled = If true, keyboard input was handled. Passed by reference, so if you react to input, change512 /// this to true.513 voidafterInput(refboolkeyboardHandled) { }
514 515 }
516 517 /// Global data for the layout tree.518 structLayoutTree {
519 520 importfluid.theme : Breadcrumbs;
521 522 // Nodes523 public {
524 525 /// Root node of the tree.526 Noderoot;
527 528 /// Node the mouse is hovering over if any.529 ///530 /// This is the last — topmost — node in the tree with `isHovered` set to true.531 Nodehover;
532 533 /// Currently focused node.534 ///535 /// Changing this value directly is discouraged. Some nodes might not want the focus! Be gentle, call536 /// `FluidFocusable.focus()` instead and let the node set the value on its own.537 FluidFocusablefocus;
538 539 /// Deepest hovered scrollable node.540 FluidScrollablescroll;
541 542 }
543 544 // Input545 public {
546 547 /// Focus direction data.548 FocusDirectionfocusDirection;
549 550 /// Padding box of the currently focused node. Only available after the node has been drawn.551 ///552 /// See_also: `focusDirection.lastFocusBox`.553 RectanglefocusBox;
554 555 /// Tree actions queued to execute during next draw.556 DList!TreeActionactions;
557 558 /// Input strokes bound to emit given action signals.559 ///560 /// Input layers have to be sorted.561 InputLayer[] boundInputs;
562 563 invariant(boundInputs.isSorted);
564 565 /// Actions that are currently held down.566 DList!InputBindingdownActions;
567 568 /// Actions that have just triggered.569 DList!InputBindingactiveActions;
570 571 /// Access to core input and output facilities.572 FluidBackendbackend;
573 aliasio = backend;
574 575 /// True if keyboard input was handled during the last frame; updated after tree rendering has completed.576 boolwasKeyboardHandled;
577 578 deprecated("keyboardHandled was renamed to wasKeyboardHandled and will be removed in Fluid 0.8.0.")
579 aliaskeyboardHandled = wasKeyboardHandled;
580 581 }
582 583 /// Miscelleanous, technical properties.584 public {
585 586 /// Current node drawing depth.587 uintdepth;
588 589 /// Current rectangle drawing is limited to.590 Rectanglescissors;
591 592 /// True if the current tree branch is marked as disabled (doesn't take input).593 boolisBranchDisabled;
594 595 /// Current breadcrumbs. These are assigned to any node that is resized or drawn at the time.596 ///597 /// Any node that introduces its own breadcrumbs will push onto this stack, and pop once finished.598 Breadcrumbsbreadcrumbs;
599 600 /// Context for the new I/O system. https://git.samerion.com/Samerion/Fluid/issues/148601 TreeContextDatacontext;
602 603 }
604 605 /// Incremented for every `filterActions` access to prevent nested accesses from breaking previously made ranges.606 privateint_actionAccessCounter;
607 608 /// Create a new tree with the given node as its root, and using the given backend for I/O.609 this(Noderoot, FluidBackendbackend) {
610 611 this.root = root;
612 this.backend = backend;
613 this.restoreDefaultInputBinds();
614 615 }
616 617 /// Create a new tree with the given node as its root. Use the default backend, if any is present.618 this(Noderoot) {
619 620 this(root, defaultFluidBackend);
621 622 assert(backend, "Cannot create LayoutTree; no backend was chosen, and no default is set.");
623 624 }
625 626 /// Returns true if this branch requested a resize or is pending a resize.627 boolresizePending() const {
628 629 returnroot.resizePending;
630 631 }
632 633 /// Returns: True if the mouse is currently hovering a node in the tree.634 boolisHovered() const {
635 636 returnhover !isnull;
637 638 }
639 640 /// Queue an action to perform while iterating the tree.641 ///642 /// Avoid using this; most of the time `Node.queueAction` is what you want. `LayoutTree.queueAction` might fire643 /// too early644 voidqueueAction(TreeActionaction)
645 in (action, "Invalid action queued")
646 do {
647 648 actions ~= action;
649 650 // Run the first hook651 action.started();
652 653 }
654 655 /// Restore defaults for given actions.656 voidrestoreDefaultInputBinds() {
657 658 /// Get the ID of an input action.659 autobind(aliasa, T)(Targ) {
660 661 returnInputBinding(InputAction!a.id, InputStroke.Item(arg));
662 663 }
664 665 with (FluidInputAction) {
666 667 // System-independent keys668 autouniversalShift = InputLayer(
669 InputStroke(KeyboardKey.leftShift),
670 [
671 bind!focusPrevious(KeyboardKey.tab),
672 bind!entryPrevious(KeyboardKey.tab),
673 bind!outdent(KeyboardKey.tab),
674 bind!selectPreviousChar(KeyboardKey.left),
675 bind!selectNextChar(KeyboardKey.right),
676 bind!selectPreviousLine(KeyboardKey.up),
677 bind!selectNextLine(KeyboardKey.down),
678 bind!selectToLineStart(KeyboardKey.home),
679 bind!selectToLineEnd(KeyboardKey.end),
680 bind!breakLine(KeyboardKey.enter),
681 bind!contextMenu(KeyboardKey.f10),
682 ]
683 );
684 autouniversal = InputLayer(
685 InputStroke(),
686 [
687 // Press688 bind!press(MouseButton.left),
689 bind!press(KeyboardKey.enter),
690 bind!press(GamepadButton.cross),
691 692 // Submit693 bind!submit(KeyboardKey.enter),
694 bind!submit(GamepadButton.cross),
695 696 // Cancel697 bind!cancel(KeyboardKey.escape),
698 bind!cancel(GamepadButton.circle),
699 700 // Menu701 bind!contextMenu(MouseButton.right),
702 bind!contextMenu(KeyboardKey.contextMenu),
703 704 // Tabbing; index-focus705 bind!focusPrevious(GamepadButton.leftButton),
706 bind!focusNext(KeyboardKey.tab),
707 bind!focusNext(GamepadButton.rightButton),
708 709 // Directional focus710 bind!focusLeft(KeyboardKey.left),
711 bind!focusLeft(GamepadButton.dpadLeft),
712 bind!focusRight(KeyboardKey.right),
713 bind!focusRight(GamepadButton.dpadRight),
714 bind!focusUp(KeyboardKey.up),
715 bind!focusUp(GamepadButton.dpadUp),
716 bind!focusDown(KeyboardKey.down),
717 bind!focusDown(GamepadButton.dpadDown),
718 719 // Text input720 bind!backspace(KeyboardKey.backspace),
721 bind!deleteChar(KeyboardKey.delete_),
722 bind!breakLine(KeyboardKey.enter),
723 bind!previousChar(KeyboardKey.left),
724 bind!nextChar(KeyboardKey.right),
725 bind!previousLine(KeyboardKey.up),
726 bind!nextLine(KeyboardKey.down),
727 bind!entryPrevious(KeyboardKey.up),
728 bind!entryPrevious(GamepadButton.dpadUp),
729 bind!entryNext(KeyboardKey.down),
730 bind!entryNext(KeyboardKey.tab),
731 bind!entryNext(GamepadButton.dpadDown),
732 bind!toLineStart(KeyboardKey.home),
733 bind!toLineEnd(KeyboardKey.end),
734 bind!insertTab(KeyboardKey.tab),
735 736 // Scrolling737 bind!scrollLeft(KeyboardKey.left),
738 bind!scrollLeft(GamepadButton.dpadLeft),
739 bind!scrollRight(KeyboardKey.right),
740 bind!scrollRight(GamepadButton.dpadRight),
741 bind!scrollUp(KeyboardKey.up),
742 bind!scrollUp(GamepadButton.dpadUp),
743 bind!scrollDown(KeyboardKey.down),
744 bind!scrollDown(GamepadButton.dpadDown),
745 bind!pageUp(KeyboardKey.pageUp),
746 bind!pageDown(KeyboardKey.pageDown),
747 ]
748 );
749 750 // TODO universal left/right key751 version (Fluid_MacKeyboard)
752 boundInputs = [
753 754 // Shift + Command755 InputLayer(
756 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftSuper),
757 [
758 // TODO Command should *expand selection* on macOS instead of current759 // toLineStart/toLineEnd behavior760 bind!selectToLineStart(KeyboardKey.left),
761 bind!selectToLineEnd(KeyboardKey.right),
762 bind!selectToStart(KeyboardKey.up),
763 bind!selectToEnd(KeyboardKey.down),
764 bind!redo(KeyboardKey.z),
765 ]
766 ),
767 768 // Shift + Option769 InputLayer(
770 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftAlt),
771 [
772 bind!selectPreviousWord(KeyboardKey.left),
773 bind!selectNextWord(KeyboardKey.right),
774 ]
775 ),
776 777 // Command778 InputLayer(
779 InputStroke(KeyboardKey.leftSuper),
780 [
781 bind!toLineStart(KeyboardKey.left),
782 bind!toLineEnd(KeyboardKey.right),
783 bind!toStart(KeyboardKey.up),
784 bind!toEnd(KeyboardKey.down),
785 bind!selectAll(KeyboardKey.a),
786 bind!copy(KeyboardKey.c),
787 bind!cut(KeyboardKey.x),
788 bind!paste(KeyboardKey.v),
789 bind!undo(KeyboardKey.z),
790 bind!redo(KeyboardKey.y),
791 bind!submit(KeyboardKey.enter),
792 ]
793 ),
794 795 // Option796 InputLayer(
797 InputStroke(KeyboardKey.leftAlt),
798 [
799 bind!deleteWord(KeyboardKey.delete_),
800 bind!backspaceWord(KeyboardKey.backspace),
801 bind!previousWord(KeyboardKey.left),
802 bind!nextWord(KeyboardKey.right),
803 ]
804 ),
805 806 // Control807 InputLayer(
808 InputStroke(KeyboardKey.leftControl),
809 [
810 bind!backspaceWord(KeyboardKey.w), // emacs & vim811 bind!entryPrevious(KeyboardKey.k), // vim812 bind!entryPrevious(KeyboardKey.p), // emacs813 bind!entryNext(KeyboardKey.j), // vim814 bind!entryNext(KeyboardKey.n), // emacs815 ]
816 ),
817 818 universalShift,
819 universal,
820 ];
821 else822 boundInputs = [
823 824 InputLayer(
825 InputStroke(KeyboardKey.leftShift, KeyboardKey.leftControl),
826 [
827 bind!selectPreviousWord(KeyboardKey.left),
828 bind!selectNextWord(KeyboardKey.right),
829 bind!selectToStart(KeyboardKey.home),
830 bind!selectToEnd(KeyboardKey.end),
831 bind!redo(KeyboardKey.z),
832 ]
833 ),
834 835 InputLayer(
836 InputStroke(KeyboardKey.leftControl),
837 [
838 bind!deleteWord(KeyboardKey.delete_),
839 bind!backspaceWord(KeyboardKey.backspace),
840 bind!backspaceWord(KeyboardKey.w), // emacs & vim841 bind!entryPrevious(KeyboardKey.k), // vim842 bind!entryPrevious(KeyboardKey.p), // emacs843 bind!entryNext(KeyboardKey.j), // vim844 bind!entryNext(KeyboardKey.n), // emacs845 bind!previousWord(KeyboardKey.left),
846 bind!nextWord(KeyboardKey.right),
847 bind!selectAll(KeyboardKey.a),
848 bind!copy(KeyboardKey.c),
849 bind!cut(KeyboardKey.x),
850 bind!paste(KeyboardKey.v),
851 bind!undo(KeyboardKey.z),
852 bind!redo(KeyboardKey.y),
853 bind!toStart(KeyboardKey.home),
854 bind!toEnd(KeyboardKey.end),
855 856 // Submit with ctrl+enter857 bind!submit(KeyboardKey.enter),
858 ]
859 ),
860 861 InputLayer(
862 InputStroke(KeyboardKey.leftAlt),
863 [
864 bind!entryUp(KeyboardKey.up),
865 ]
866 ),
867 868 universalShift,
869 universal,
870 871 ];
872 873 }
874 875 }
876 877 /// Remove any inputs bound to given input action.878 /// Returns: `true` if the action was cleared.879 boolclearBoundInput(InputActionIDaction) {
880 881 importstd.array;
882 883 // TODO test884 885 boolfound;
886 887 foreach (reflayer; boundInputs) {
888 889 constoldLength = layer.bindings.length;
890 891 layer.bindings = layer.bindings.filter!(a => a.action == action).array;
892 893 if (layer.bindings.length != oldLength) {
894 found = true;
895 }
896 897 }
898 899 returnfound;
900 901 }
902 903 /// Find a layer for the given input stroke.904 /// Returns: Layer found for the given input stroke. `null` if none found.905 inout(InputLayer)* layerForStroke(InputStrokestroke) inoutscopereturn {
906 907 automodifiers = stroke.modifiers;
908 909 foreach (i, layer; boundInputs) {
910 911 // Found a matching layer912 if (modifiers == layer.modifiers) {
913 914 return &boundInputs[i];
915 916 }
917 918 // Stop if other layers are less complex919 if (modifiers.length > layer.modifiers.length) break;
920 921 }
922 923 returnnull;
924 925 }
926 927 /// Bind a key stroke or button to given input action. Multiple key strokes are allowed to match given action.928 voidbindInput(InputActionIDaction, InputStrokestroke)
929 in (stroke.length != 0)
930 do {
931 932 // TODO tests933 934 autobinding = InputBinding(action, stroke.input[$-1]);
935 936 // Layer exists, add the binding937 if (autolayer = layerForStroke(stroke)) {
938 939 layer.bindings ~= binding;
940 941 }
942 943 // Layer doesn't exist, create it944 else {
945 946 automodifiers = stroke.modifiers;
947 autonewLayer = InputLayer(modifiers, [binding]);
948 boolfound;
949 950 // Insert the layer before any layer that is less complex951 foreach (i, layer; boundInputs) {
952 953 if (modifiers.length > layer.modifiers.length) {
954 955 boundInputs = boundInputs[0..i] ~ newLayer ~ boundInputs[i..$];
956 found = true;
957 break;
958 959 }
960 961 }
962 963 if (!found) boundInputs ~= newLayer;
964 965 assert(isSorted(boundInputs));
966 967 }
968 969 }
970 971 /// Bind a key stroke or button to given input action, replacing any previously bound inputs.972 voidbindInputReplace(InputActionIDaction, InputStrokestroke)
973 in (stroke.length != 0)
974 do {
975 976 importstd.array;
977 978 // Find a matching layer979 if (autolayer = layerForStroke(stroke)) {
980 981 // Remove any stroke that matches982 layer.bindings = layer.bindings.filter!(a => a.trigger == stroke.input[$-1]).array;
983 984 // Insert the binding985 layer.bindings ~= InputBinding(action, stroke.input[$-1]);
986 987 }
988 989 // Layer doesn't exist, bind it the straightforward way990 elsebindInput(action, stroke);
991 992 }
993 994 /// List actions in the tree, remove finished actions while iterating.995 autofilterActions() {
996 997 structActionIterator {
998 999 LayoutTree* tree;
1000 1001 intopApply(intdelegate(TreeAction) @safefun) {
1002 1003 tree._actionAccessCounter++;
1004 scope (exit) tree._actionAccessCounter--;
1005 1006 // Regular access1007 if (tree._actionAccessCounter == 1) {
1008 1009 for (autorange = tree.actions[]; !range.empty; ) {
1010 1011 // Yield the item1012 autoresult = fun(range.front);
1013 1014 // If finished, remove from the queue1015 if (range.front.toStop) tree.actions.popFirstOf(range);
1016 1017 // Continue to the next item1018 elserange.popFront();
1019 1020 // Stop iteration if requested1021 if (result) returnresult;
1022 1023 }
1024 1025 }
1026 1027 // Nested access1028 else {
1029 1030 for (autorange = tree.actions[]; !range.empty; ) {
1031 1032 autofront = range.front;
1033 range.popFront();
1034 1035 // Ignore stopped items1036 if (front.toStop) continue;
1037 1038 // Yield the item1039 if (autoresult = fun(front)) {
1040 1041 returnresult;
1042 1043 }
1044 1045 }
1046 1047 }
1048 1049 // Run new actions too1050 foreach (action; tree.context.actions) {
1051 1052 if (autoresult = fun(action)) {
1053 returnresult;
1054 }
1055 1056 }
1057 1058 return0;
1059 1060 }
1061 1062 }
1063 1064 returnActionIterator(&this);
1065 1066 }
1067 1068 /// Intersect the given rectangle against current scissor area.1069 RectangleintersectScissors(Rectanglerect) {
1070 1071 importfluid.utils : intersect;
1072 1073 // No limit applied1074 if (scissorsisscissors.init) returnrect;
1075 1076 returnintersect(rect, scissors);
1077 1078 }
1079 1080 /// Start scissors mode.1081 /// Returns: Previous scissors mode value. Pass that value to `popScissors`.1082 RectanglepushScissors(Rectanglerect) {
1083 1084 constlastScissors = scissors;
1085 1086 // Intersect with the current scissors rectangle.1087 io.area = scissors = intersectScissors(rect);
1088 1089 returnlastScissors;
1090 1091 }
1092 1093 voidpopScissors(RectanglelastScissorsMode) @trusted {
1094 1095 // Pop the stack1096 scissors = lastScissorsMode;
1097 1098 // No scissors left1099 if (scissorsisscissors.init) {
1100 1101 // Restore full draw area1102 backend.restoreArea();
1103 1104 }
1105 1106 else {
1107 1108 // Start again1109 backend.area = scissors;
1110 1111 }
1112 1113 }
1114 1115 /// Fetch tree events (e.g. actions)1116 packagevoidpoll() {
1117 1118 // Run texture reaper1119 io.reaper.check();
1120 1121 // Reset all actions1122 downActions.clear();
1123 activeActions.clear();
1124 1125 // Test all bindings1126 foreach (layer; boundInputs) {
1127 1128 // Check if the layer is active1129 if (!layer.modifiers.isDown(backend)) continue;
1130 1131 // Found an active layer, test all bound strokes1132 foreach (binding; layer.bindings) {
1133 1134 // Register held-down actions1135 if (InputStroke.isItemDown(backend, binding.trigger)) {
1136 1137 downActions ~= binding;
1138 1139 }
1140 1141 // Register triggered actions1142 if (InputStroke.isItemActive(backend, binding.trigger)) {
1143 1144 activeActions ~= binding;
1145 1146 }
1147 1148 }
1149 1150 // End on this layer1151 break;
1152 1153 }
1154 1155 }
1156 1157 }
1158 1159 @("Legacy: LayoutTree.isHovered is true when a node is hovered, false when not (abandoned)")
1160 unittest {
1161 1162 importfluid.space;
1163 importfluid.label;
1164 importfluid.structs;
1165 importfluid.default_theme;
1166 1167 autoio = newHeadlessBackend;
1168 autotext = label("Hello, World!");
1169 autoroot = vspace(
1170 layout!"fill",
1171 nullTheme,
1172 text1173 );
1174 1175 io.mousePosition = Vector2(5, 5);
1176 root.io = io;
1177 root.draw();
1178 1179 assert(root.tree.hoveristext);
1180 assert(root.tree.isHovered);
1181 1182 io.nextFrame;
1183 io.mousePosition = Vector2(-1, -1);
1184 root.draw();
1185 1186 assert(root.tree.hover !istext);
1187 assert(!root.tree.isHovered);
1188 1189 io.nextFrame;
1190 io.mousePosition = Vector2(5, 50);
1191 root.draw();
1192 1193 assert(root.tree.hover !istext);
1194 assert(!root.tree.isHovered);
1195 1196 }