1 /// Definitions for common tree actions; This is the Fluid tree equivalent to std.algorithm. 2 module fluid.actions; 3 4 import fluid.node; 5 import fluid.tree; 6 import fluid.input; 7 import fluid.backend; 8 import fluid.container; 9 10 11 @safe: 12 13 14 /// Set focus on the given node, if focusable, or the first of its focusable children. This will be done lazily during 15 /// the next draw. If calling `focusRecurseChildren`, the subject of the call will be excluded from taking focus. 16 /// Params: 17 /// parent = Container node to search in. 18 void focusRecurse(Node parent) { 19 20 // Perform a tree action to find the child 21 parent.queueAction(new FocusRecurseAction); 22 23 } 24 25 unittest { 26 27 import fluid.space; 28 import fluid.label; 29 import fluid.button; 30 31 auto io = new HeadlessBackend; 32 auto root = vspace( 33 label(""), 34 button("", delegate { }), 35 button("", delegate { }), 36 button("", delegate { }), 37 ); 38 39 // First paint: no node focused 40 root.io = io; 41 root.draw(); 42 43 assert(root.tree.focus is null, "No focus assigned on the first frame"); 44 45 io.nextFrame; 46 47 // Recurse into the tree to focus on the first node 48 root.focusRecurse(); 49 root.draw(); 50 51 assert(root.tree.focus.asNode is root.children[1], "First child is now focused"); 52 assert((cast(FluidFocusable) root.children[1]).isFocused); 53 54 } 55 56 /// ditto 57 void focusRecurseChildren(Node parent) { 58 59 auto action = new FocusRecurseAction; 60 action.excludeStartNode = true; 61 62 parent.queueAction(action); 63 64 } 65 66 unittest { 67 68 import fluid.space; 69 import fluid.button; 70 71 auto io = new HeadlessBackend; 72 auto root = frameButton( 73 button("", delegate { }), 74 button("", delegate { }), 75 delegate { } 76 ); 77 78 root.io = io; 79 80 // Typical focusRecurse call will focus the button 81 root.focusRecurse; 82 root.draw(); 83 84 assert(root.tree.focus is root); 85 86 io.nextFrame; 87 88 // If we want to make sure the action descends below the root, we must 89 root.focusRecurseChildren; 90 root.draw(); 91 92 assert(root.tree.focus.asNode is root.children[0]); 93 94 } 95 96 class FocusRecurseAction : TreeAction { 97 98 public { 99 100 bool excludeStartNode; 101 102 } 103 104 override void beforeDraw(Node node, Rectangle) { 105 106 // Ignore if the branch is disabled 107 if (node.isDisabledInherited) return; 108 109 // Ignore the start node if excluded 110 if (excludeStartNode && node is startNode) return; 111 112 // Check if the node is focusable 113 if (auto focusable = cast(FluidFocusable) node) { 114 115 // Give it focus 116 focusable.focus(); 117 118 // We're done here 119 stop; 120 121 } 122 123 } 124 125 } 126 127 /// Scroll so the given node becomes visible. 128 /// Params: 129 /// node = Node to scroll to. 130 void scrollIntoView(Node node) { 131 132 node.queueAction(new ScrollIntoViewAction); 133 134 } 135 136 unittest { 137 138 import fluid; 139 import std.math; 140 import std.array; 141 import std.range; 142 import std.algorithm; 143 144 const viewportHeight = 10; 145 146 auto io = new HeadlessBackend(Vector2(10, viewportHeight)); 147 auto root = vscrollFrame( 148 layout!(1, "fill"), 149 cast(Theme) null, 150 151 label("a"), 152 label("b"), 153 label("c"), 154 ); 155 156 root.io = io; 157 root.scrollBar.width = 0; // TODO replace this with scrollBar.hide() 158 159 // Prepare scrolling 160 // Note: Changes made when scrolling will be visible during the next frame 161 root.children[1].scrollIntoView; 162 root.draw(); 163 164 auto getPositions() => io.textures.map!(a => a.position).array; 165 166 // Find label positions 167 auto positions = getPositions(); 168 169 // No theme so everything is as compact as it can be: the first label should be at the very top 170 assert(positions[0].y.isClose(0)); 171 assert(positions[1].y > positions[0].y); 172 assert(positions[2].y > positions[1].y); 173 174 // It is reasonable to assume the text will be larger than 10 pixels (viewport height) 175 assert(positions[1].y > viewportHeight); 176 177 // TODO Because the label was hidden below the viewport, Fluid will align the bottom of the selected node with the 178 // viewport which probably isn't appropriate in case *like this* where it should reveal the top of the node. 179 auto texture1 = io.textures.dropOne.front; 180 assert(root.scroll.isClose(texture1.position.y + texture1.height - viewportHeight)); 181 182 io.nextFrame; 183 root.draw(); 184 185 auto scrolledPositions = getPositions(); 186 187 // Make sure all the labels are scrolled 188 assert(equal!((a, b) => isClose(a.y - root.scroll, b.y))(positions, scrolledPositions)); 189 190 // TODO more tests. Scrolling while already in the viewport, scrolling while partially out of the view, etc. 191 192 } 193 194 class ScrollIntoViewAction : TreeAction { 195 196 private { 197 198 /// The node this action attempts to put into view. 199 Node target; 200 201 Vector2 viewport; 202 Rectangle childBox; 203 204 } 205 206 override void afterDraw(Node node, Rectangle, Rectangle paddingBox, Rectangle) { 207 208 // Target node was drawn 209 if (node is startNode) { 210 211 // Make sure the action reaches the end of the tree 212 target = node; 213 startNode = null; 214 215 // Get viewport size 216 viewport = node.tree.io.windowSize; 217 218 // Get the node's padding box 219 childBox = paddingBox; 220 221 222 } 223 224 // Ignore children of the target node 225 // Note: startNode is set until reached 226 else if (startNode !is null) return; 227 228 // Reached a container node 229 else if (auto container = cast(FluidContainer) node) { 230 231 // Perform the scroll 232 childBox = container.shallowScrollTo(node, viewport, paddingBox, childBox); 233 234 } 235 236 } 237 238 }