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 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 FocusRecurseAction focusRecurse(Node parent) { 19 20 auto action = new FocusRecurseAction; 21 22 // Perform a tree action to find the child 23 parent.queueAction(action); 24 25 return action; 26 27 } 28 29 unittest { 30 31 import fluid.space; 32 import fluid.label; 33 import fluid.button; 34 35 auto io = new HeadlessBackend; 36 auto root = vspace( 37 label(""), 38 button("", delegate { }), 39 button("", delegate { }), 40 button("", delegate { }), 41 ); 42 43 // First paint: no node focused 44 root.io = io; 45 root.draw(); 46 47 assert(root.tree.focus is null, "No focus assigned on the first frame"); 48 49 io.nextFrame; 50 51 // Recurse into the tree to focus on the first node 52 root.focusRecurse(); 53 root.draw(); 54 55 assert(root.tree.focus.asNode is root.children[1], "First child is now focused"); 56 assert((cast(FluidFocusable) root.children[1]).isFocused); 57 58 } 59 60 /// ditto 61 FocusRecurseAction focusRecurseChildren(Node parent) { 62 63 auto action = new FocusRecurseAction; 64 action.excludeStartNode = true; 65 parent.queueAction(action); 66 67 return action; 68 69 } 70 71 /// ditto 72 FocusRecurseAction focusChild(Node parent) { 73 74 return focusRecurseChildren(parent); 75 76 } 77 78 unittest { 79 80 import fluid.space; 81 import fluid.button; 82 83 auto io = new HeadlessBackend; 84 auto root = vframeButton( 85 button("", delegate { }), 86 button("", delegate { }), 87 delegate { } 88 ); 89 90 root.io = io; 91 92 // Typical focusRecurse call will focus the button 93 root.focusRecurse; 94 root.draw(); 95 96 assert(root.tree.focus is root); 97 98 io.nextFrame; 99 100 // If we want to make sure the action descends below the root, we must 101 root.focusRecurseChildren; 102 root.draw(); 103 104 assert(root.tree.focus.asNode is root.children[0]); 105 106 } 107 108 class FocusRecurseAction : TreeAction { 109 110 public { 111 112 bool excludeStartNode; 113 void delegate(FluidFocusable) @safe finished; 114 115 } 116 117 override void beforeDraw(Node node, Rectangle) { 118 119 // Ignore if the branch is disabled 120 if (node.isDisabledInherited) return; 121 122 // Ignore the start node if excluded 123 if (excludeStartNode && node is startNode) return; 124 125 // Check if the node is focusable 126 if (auto focusable = cast(FluidFocusable) node) { 127 128 // Give it focus 129 focusable.focus(); 130 131 // Submit the result 132 if (finished) finished(focusable); 133 134 // We're done here 135 stop; 136 137 } 138 139 } 140 141 } 142 143 /// Scroll so the given node becomes visible. 144 /// Params: 145 /// node = Node to scroll to. 146 /// alignToTop = If true, the top of the element will be aligned to the top of the scrollable area. 147 ScrollIntoViewAction scrollIntoView(Node node, bool alignToTop = false) { 148 149 auto action = new ScrollIntoViewAction; 150 node.queueAction(action); 151 action.alignToTop = alignToTop; 152 153 return action; 154 155 } 156 157 /// Scroll so that the given node appears at the top, if possible. 158 ScrollIntoViewAction scrollToTop(Node node) { 159 160 return scrollIntoView(node, true); 161 162 } 163 164 unittest { 165 166 import fluid; 167 import std.math; 168 import std.array; 169 import std.range; 170 import std.algorithm; 171 172 const viewportHeight = 10; 173 174 auto io = new HeadlessBackend(Vector2(10, viewportHeight)); 175 auto root = vscrollFrame( 176 layout!(1, "fill"), 177 nullTheme, 178 179 label("a"), 180 label("b"), 181 label("c"), 182 ); 183 184 root.io = io; 185 root.scrollBar.width = 0; // TODO replace this with scrollBar.hide() 186 187 // Prepare scrolling 188 // Note: Changes made when scrolling will be visible during the next frame 189 root.children[1].scrollIntoView; 190 root.draw(); 191 192 auto getPositions() => io.textures.map!(a => a.position).array; 193 194 // Find label positions 195 auto positions = getPositions(); 196 197 // No theme so everything is as compact as it can be: the first label should be at the very top 198 assert(positions[0].y.isClose(0)); 199 200 // It is reasonable to assume the text will be larger than 10 pixels (viewport height) 201 // Other text will not render, since it's offscreen 202 assert(positions.length == 1); 203 204 io.nextFrame; 205 root.draw(); 206 207 // TODO Because the label was hidden below the viewport, Fluid will align the bottom of the selected node with the 208 // viewport which probably isn't appropriate in case *like this* where it should reveal the top of the node. 209 auto texture1 = io.textures.front; 210 assert(isClose(texture1.position.y + texture1.height, viewportHeight)); 211 assert(isClose(root.scroll, (root.scrollMax + 10) * 2/3 - 10)); 212 213 io.nextFrame; 214 root.draw(); 215 216 auto scrolledPositions = getPositions(); 217 218 // TODO more tests. Scrolling while already in the viewport, scrolling while partially out of the view, etc. 219 220 } 221 222 class ScrollIntoViewAction : TreeAction { 223 224 public { 225 226 /// If true, try to display the child at the top. 227 bool alignToTop; 228 229 } 230 231 private { 232 233 /// The node this action attempts to put into view. 234 Node target; 235 236 Vector2 viewport; 237 Rectangle childBox; 238 239 } 240 241 void reset(bool alignToTop = false) { 242 243 this.toStop = false; 244 this.alignToTop = alignToTop; 245 246 } 247 248 override void afterDraw(Node node, Rectangle, Rectangle paddingBox, Rectangle contentBox) { 249 250 // Target node was drawn 251 if (node is startNode) { 252 253 // Make sure the action reaches the end of the tree 254 target = node; 255 startNode = null; 256 257 // Get viewport size 258 viewport = node.tree.io.windowSize; 259 260 // Get the node's padding box 261 childBox = node.focusBoxImpl(contentBox); 262 263 264 } 265 266 // Ignore children of the target node 267 // Note: startNode is set until reached 268 else if (startNode !is null) return; 269 270 // Reached a scroll node 271 // TODO What if the container isn't an ancestor 272 else if (auto scrollable = cast(FluidScrollable) node) { 273 274 // Perform the scroll 275 childBox = scrollable.shallowScrollTo(target, paddingBox, childBox); 276 277 // Aligning to top, make sure the child aligns with the parent 278 if (alignToTop && childBox.y > paddingBox.y) { 279 280 const offset = childBox.y - paddingBox.y; 281 282 scrollable.scroll = scrollable.scroll + cast(size_t) offset; 283 284 } 285 286 } 287 288 } 289 290 }