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.scroll; 8 import fluid.backend; 9 10 import fluid.io.focus; 11 12 import fluid.future.pipe; 13 14 15 @safe: 16 17 18 /// Abstract class for tree actions that find and return a node. 19 abstract class NodeSearchAction : TreeAction, Publisher!Node { 20 21 public { 22 23 /// Node this action has found. 24 Node result; 25 26 } 27 28 private { 29 30 /// Event that runs when the tree action finishes. 31 Event!Node finished; 32 33 } 34 35 alias then = typeof(super).then; 36 alias then = Publisher!Node.then; 37 alias subscribe = typeof(super).subscribe; 38 alias subscribe = Publisher!Node.subscribe; 39 40 override void clearSubscribers() { 41 super.clearSubscribers(); 42 finished.clearSubscribers(); 43 } 44 45 override void subscribe(Subscriber!Node subscriber) { 46 finished.subscribe(subscriber); 47 } 48 49 override void beforeTree(Node node, Rectangle rect) { 50 super.beforeTree(node, rect); 51 result = null; 52 } 53 54 override void stopped() { 55 super.stopped(); 56 finished(result); 57 } 58 59 } 60 61 abstract class FocusSearchAction : NodeSearchAction, Publisher!Focusable { 62 63 private { 64 65 /// Event that runs when the tree action finishes. 66 Event!Focusable finished; 67 68 } 69 70 alias then = typeof(super).then; 71 alias then = Publisher!Focusable.then; 72 alias subscribe = typeof(super).subscribe; 73 alias subscribe = Publisher!Focusable.subscribe; 74 75 override void clearSubscribers() { 76 super.clearSubscribers(); 77 finished.clearSubscribers(); 78 } 79 80 override void subscribe(Subscriber!Focusable subscriber) { 81 finished.subscribe(subscriber); 82 } 83 84 override void stopped() { 85 super.stopped(); 86 finished(cast(Focusable) result); 87 } 88 89 } 90 91 /// Set focus on the given node, if focusable, or the first of its focusable children. This will be done lazily during 92 /// the next draw. 93 /// 94 /// If focusing the given node is not desired, use `focusRecurseChildren`. 95 /// 96 /// Params: 97 /// parent = Container node to search in. 98 FocusRecurseAction focusRecurse(Node parent) { 99 100 auto action = new FocusRecurseAction; 101 102 // Perform a tree action to find the child 103 parent.queueAction(action); 104 105 return action; 106 107 } 108 109 unittest { 110 111 import fluid.space; 112 import fluid.label; 113 import fluid.button; 114 115 auto io = new HeadlessBackend; 116 auto root = vspace( 117 label(""), 118 button("", delegate { }), 119 button("", delegate { }), 120 button("", delegate { }), 121 ); 122 123 // First paint: no node focused 124 root.io = io; 125 root.draw(); 126 127 assert(root.tree.focus is null, "No focus assigned on the first frame"); 128 129 io.nextFrame; 130 131 // Recurse into the tree to focus on the first node 132 root.focusRecurse(); 133 root.draw(); 134 135 assert(root.tree.focus.asNode is root.children[1], "First child is now focused"); 136 assert((cast(FluidFocusable) root.children[1]).isFocused); 137 138 } 139 140 /// Set focus on the first of the node's focusable children. This will be done lazily during the next draw. 141 /// 142 /// Params: 143 /// parent = Container node to search in. 144 FocusRecurseAction focusRecurseChildren(Node parent) { 145 146 auto action = new FocusRecurseAction; 147 action.excludeStartNode = true; 148 parent.queueAction(action); 149 150 return action; 151 152 } 153 154 /// ditto 155 FocusRecurseAction focusChild(Node parent) { 156 157 return focusRecurseChildren(parent); 158 159 } 160 161 @("FocusRecurse works") 162 unittest { 163 164 import fluid.space; 165 import fluid.button; 166 167 auto root = vframeButton( 168 button("", delegate { }), 169 button("", delegate { }), 170 delegate { } 171 ); 172 173 // Typical focusRecurse call will focus the button 174 root.focusRecurse; 175 root.draw(); 176 177 assert(root.tree.focus is root); 178 179 // If we want to make sure the action descends below the root, we must 180 root.focusRecurseChildren; 181 root.draw(); 182 183 assert(root.tree.focus.asNode is root.children[0]); 184 185 } 186 187 class FocusRecurseAction : FocusSearchAction { 188 189 public { 190 191 bool excludeStartNode; 192 bool isReverse; 193 void delegate(FluidFocusable) @safe finished; 194 195 } 196 197 override void beforeDraw(Node node, Rectangle) { 198 199 // Ignore if the branch is disabled 200 if (node.isDisabledInherited) return; 201 202 // Ignore the start node if excluded 203 if (excludeStartNode && node is startNode) return; 204 205 // Check if the node is focusable 206 if (auto focusable = cast(FluidFocusable) node) { 207 208 // Mark the node 209 result = node; 210 211 // Stop here if selecting the first 212 if (!isReverse) stop; 213 214 } 215 216 } 217 218 override void stopped() { 219 220 if (auto focusable = cast(FluidFocusable) result) { 221 focusable.focus(); 222 if (finished) finished(focusable); 223 } 224 super.stopped(); 225 226 } 227 228 } 229 230 /// Scroll so the given node becomes visible. 231 /// Params: 232 /// node = Node to scroll to. 233 /// alignToTop = If true, the top of the element will be aligned to the top of the scrollable area. 234 ScrollIntoViewAction scrollIntoView(Node node, bool alignToTop = false) { 235 236 auto action = new ScrollIntoViewAction; 237 node.queueAction(action); 238 action.alignToTop = alignToTop; 239 240 return action; 241 242 } 243 244 /// Scroll so that the given node appears at the top, if possible. 245 ScrollIntoViewAction scrollToTop(Node node) { 246 247 return scrollIntoView(node, true); 248 249 } 250 251 @("Legacy: ScrollIntoViewAction works (migrated)") 252 unittest { 253 254 import fluid; 255 import std.math; 256 import std.array; 257 import std.range; 258 import std.algorithm; 259 260 const viewportHeight = 10; 261 262 auto io = new HeadlessBackend(Vector2(10, viewportHeight)); 263 auto root = vscrollFrame( 264 layout!(1, "fill"), 265 nullTheme, 266 267 label("a"), 268 label("b"), 269 label("c"), 270 ); 271 272 root.io = io; 273 root.scrollBar.width = 0; // TODO replace this with scrollBar.hide() 274 275 // Prepare scrolling 276 // Note: Changes made when scrolling will be visible during the next frame 277 root.children[1].scrollIntoView; 278 root.draw(); 279 280 auto getPositions() { 281 return io.textures.map!(a => a.position).array; 282 } 283 284 // Find label positions 285 auto positions = getPositions(); 286 287 // No theme so everything is as compact as it can be: the first label should be at the very top 288 assert(positions[0].y.isClose(0)); 289 290 // It is reasonable to assume the text will be larger than 10 pixels (viewport height) 291 // Other text will not render, since it's offscreen 292 assert(positions.length == 1); 293 294 io.nextFrame; 295 root.draw(); 296 297 // TODO Because the label was hidden below the viewport, Fluid will align the bottom of the selected node with the 298 // viewport which probably isn't appropriate in case *like this* where it should reveal the top of the node. 299 auto texture1 = io.textures.front; 300 assert(isClose(texture1.position.y + texture1.height, viewportHeight)); 301 assert(isClose(root.scroll, (root.scrollMax + 10) * 2/3 - 10)); 302 303 io.nextFrame; 304 root.draw(); 305 306 auto scrolledPositions = getPositions(); 307 308 // TODO more tests. Scrolling while already in the viewport, scrolling while partially out of the view, etc. 309 310 } 311 312 class ScrollIntoViewAction : TreeAction { 313 314 public { 315 316 /// If true, try to display the child at the top. 317 bool alignToTop; 318 319 } 320 321 private { 322 323 /// The node this action attempts to put into view. 324 Node target; 325 326 Vector2 viewport; 327 Rectangle childBox; 328 329 } 330 331 void reset(bool alignToTop = false) { 332 333 this.toStop = false; 334 this.alignToTop = alignToTop; 335 336 } 337 338 override void afterDraw(Node node, Rectangle, Rectangle paddingBox, Rectangle contentBox) { 339 340 // Target node was drawn 341 if (node is startNode) { 342 343 // Make sure the action reaches the end of the tree 344 target = node; 345 startNode = null; 346 347 // Get viewport size 348 viewport = node.tree.io.windowSize; 349 350 // Get the node's padding box 351 childBox = node.focusBoxImpl(contentBox); 352 353 354 } 355 356 // Ignore children of the target node 357 // Note: startNode is set until reached 358 else if (startNode !is null) return; 359 360 // Reached a scroll node 361 // TODO What if the container isn't an ancestor 362 else if (auto scrollable = cast(FluidScrollable) node) { 363 364 // Perform the scroll 365 childBox = scrollable.shallowScrollTo(target, paddingBox, childBox); 366 367 // Aligning to top, make sure the child aligns with the parent 368 if (alignToTop && childBox.y > paddingBox.y) { 369 370 const offset = childBox.y - paddingBox.y; 371 372 scrollable.scroll = scrollable.scroll + cast(size_t) offset; 373 374 } 375 376 } 377 378 } 379 380 } 381 382 /// Wait for the next frame. This action is a polyfill that can be used in tree action chains to make sure they're 383 /// added during `beforeTree`. 384 NextFrameAction nextFrame(Node node) { 385 386 auto action = new NextFrameAction; 387 node.startAction(action); 388 return action; 389 390 } 391 392 class NextFrameAction : TreeAction { 393 394 // Yes! It's that simple! 395 396 }