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() { 193 return io.textures.map!(a => a.position).array; 194 } 195 196 // Find label positions 197 auto positions = getPositions(); 198 199 // No theme so everything is as compact as it can be: the first label should be at the very top 200 assert(positions[0].y.isClose(0)); 201 202 // It is reasonable to assume the text will be larger than 10 pixels (viewport height) 203 // Other text will not render, since it's offscreen 204 assert(positions.length == 1); 205 206 io.nextFrame; 207 root.draw(); 208 209 // TODO Because the label was hidden below the viewport, Fluid will align the bottom of the selected node with the 210 // viewport which probably isn't appropriate in case *like this* where it should reveal the top of the node. 211 auto texture1 = io.textures.front; 212 assert(isClose(texture1.position.y + texture1.height, viewportHeight)); 213 assert(isClose(root.scroll, (root.scrollMax + 10) * 2/3 - 10)); 214 215 io.nextFrame; 216 root.draw(); 217 218 auto scrolledPositions = getPositions(); 219 220 // TODO more tests. Scrolling while already in the viewport, scrolling while partially out of the view, etc. 221 222 } 223 224 class ScrollIntoViewAction : TreeAction { 225 226 public { 227 228 /// If true, try to display the child at the top. 229 bool alignToTop; 230 231 } 232 233 private { 234 235 /// The node this action attempts to put into view. 236 Node target; 237 238 Vector2 viewport; 239 Rectangle childBox; 240 241 } 242 243 void reset(bool alignToTop = false) { 244 245 this.toStop = false; 246 this.alignToTop = alignToTop; 247 248 } 249 250 override void afterDraw(Node node, Rectangle, Rectangle paddingBox, Rectangle contentBox) { 251 252 // Target node was drawn 253 if (node is startNode) { 254 255 // Make sure the action reaches the end of the tree 256 target = node; 257 startNode = null; 258 259 // Get viewport size 260 viewport = node.tree.io.windowSize; 261 262 // Get the node's padding box 263 childBox = node.focusBoxImpl(contentBox); 264 265 266 } 267 268 // Ignore children of the target node 269 // Note: startNode is set until reached 270 else if (startNode !is null) return; 271 272 // Reached a scroll node 273 // TODO What if the container isn't an ancestor 274 else if (auto scrollable = cast(FluidScrollable) node) { 275 276 // Perform the scroll 277 childBox = scrollable.shallowScrollTo(target, paddingBox, childBox); 278 279 // Aligning to top, make sure the child aligns with the parent 280 if (alignToTop && childBox.y > paddingBox.y) { 281 282 const offset = childBox.y - paddingBox.y; 283 284 scrollable.scroll = scrollable.scroll + cast(size_t) offset; 285 286 } 287 288 } 289 290 } 291 292 }