1 /// Definitions for common tree actions; This is the Fluid tree equivalent to std.algorithm.2 modulefluid.actions;
3 4 importfluid.node;
5 importfluid.tree;
6 importfluid.input;
7 importfluid.scroll;
8 importfluid.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 during15 /// 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 FocusRecurseActionfocusRecurse(Nodeparent) {
19 20 autoaction = newFocusRecurseAction;
21 22 // Perform a tree action to find the child23 parent.queueAction(action);
24 25 returnaction;
26 27 }
28 29 unittest {
30 31 importfluid.space;
32 importfluid.label;
33 importfluid.button;
34 35 autoio = newHeadlessBackend;
36 autoroot = vspace(
37 label(""),
38 button("", delegate { }),
39 button("", delegate { }),
40 button("", delegate { }),
41 );
42 43 // First paint: no node focused44 root.io = io;
45 root.draw();
46 47 assert(root.tree.focusisnull, "No focus assigned on the first frame");
48 49 io.nextFrame;
50 51 // Recurse into the tree to focus on the first node52 root.focusRecurse();
53 root.draw();
54 55 assert(root.tree.focus.asNodeisroot.children[1], "First child is now focused");
56 assert((cast(FluidFocusable) root.children[1]).isFocused);
57 58 }
59 60 /// ditto61 FocusRecurseActionfocusRecurseChildren(Nodeparent) {
62 63 autoaction = newFocusRecurseAction;
64 action.excludeStartNode = true;
65 parent.queueAction(action);
66 67 returnaction;
68 69 }
70 71 /// ditto72 FocusRecurseActionfocusChild(Nodeparent) {
73 74 returnfocusRecurseChildren(parent);
75 76 }
77 78 unittest {
79 80 importfluid.space;
81 importfluid.button;
82 83 autoio = newHeadlessBackend;
84 autoroot = vframeButton(
85 button("", delegate { }),
86 button("", delegate { }),
87 delegate { }
88 );
89 90 root.io = io;
91 92 // Typical focusRecurse call will focus the button93 root.focusRecurse;
94 root.draw();
95 96 assert(root.tree.focusisroot);
97 98 io.nextFrame;
99 100 // If we want to make sure the action descends below the root, we must101 root.focusRecurseChildren;
102 root.draw();
103 104 assert(root.tree.focus.asNodeisroot.children[0]);
105 106 }
107 108 classFocusRecurseAction : TreeAction {
109 110 public {
111 112 boolexcludeStartNode;
113 voiddelegate(FluidFocusable) @safefinished;
114 115 }
116 117 overridevoidbeforeDraw(Nodenode, Rectangle) {
118 119 // Ignore if the branch is disabled120 if (node.isDisabledInherited) return;
121 122 // Ignore the start node if excluded123 if (excludeStartNode && nodeisstartNode) return;
124 125 // Check if the node is focusable126 if (autofocusable = cast(FluidFocusable) node) {
127 128 // Give it focus129 focusable.focus();
130 131 // Submit the result132 if (finished) finished(focusable);
133 134 // We're done here135 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 ScrollIntoViewActionscrollIntoView(Nodenode, boolalignToTop = false) {
148 149 autoaction = newScrollIntoViewAction;
150 node.queueAction(action);
151 action.alignToTop = alignToTop;
152 153 returnaction;
154 155 }
156 157 /// Scroll so that the given node appears at the top, if possible.158 ScrollIntoViewActionscrollToTop(Nodenode) {
159 160 returnscrollIntoView(node, true);
161 162 }
163 164 unittest {
165 166 importfluid;
167 importstd.math;
168 importstd.array;
169 importstd.range;
170 importstd.algorithm;
171 172 constviewportHeight = 10;
173 174 autoio = newHeadlessBackend(Vector2(10, viewportHeight));
175 autoroot = 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 scrolling188 // Note: Changes made when scrolling will be visible during the next frame189 root.children[1].scrollIntoView;
190 root.draw();
191 192 autogetPositions() {
193 returnio.textures.map!(a => a.position).array;
194 }
195 196 // Find label positions197 autopositions = getPositions();
198 199 // No theme so everything is as compact as it can be: the first label should be at the very top200 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 offscreen204 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 the210 // viewport which probably isn't appropriate in case *like this* where it should reveal the top of the node.211 autotexture1 = 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 autoscrolledPositions = getPositions();
219 220 // TODO more tests. Scrolling while already in the viewport, scrolling while partially out of the view, etc.221 222 }
223 224 classScrollIntoViewAction : TreeAction {
225 226 public {
227 228 /// If true, try to display the child at the top.229 boolalignToTop;
230 231 }
232 233 private {
234 235 /// The node this action attempts to put into view.236 Nodetarget;
237 238 Vector2viewport;
239 RectanglechildBox;
240 241 }
242 243 voidreset(boolalignToTop = false) {
244 245 this.toStop = false;
246 this.alignToTop = alignToTop;
247 248 }
249 250 overridevoidafterDraw(Nodenode, Rectangle, RectanglepaddingBox, RectanglecontentBox) {
251 252 // Target node was drawn253 if (nodeisstartNode) {
254 255 // Make sure the action reaches the end of the tree256 target = node;
257 startNode = null;
258 259 // Get viewport size260 viewport = node.tree.io.windowSize;
261 262 // Get the node's padding box263 childBox = node.focusBoxImpl(contentBox);
264 265 266 }
267 268 // Ignore children of the target node269 // Note: startNode is set until reached270 elseif (startNode !isnull) return;
271 272 // Reached a scroll node273 // TODO What if the container isn't an ancestor274 elseif (autoscrollable = cast(FluidScrollable) node) {
275 276 // Perform the scroll277 childBox = scrollable.shallowScrollTo(target, paddingBox, childBox);
278 279 // Aligning to top, make sure the child aligns with the parent280 if (alignToTop && childBox.y > paddingBox.y) {
281 282 constoffset = childBox.y - paddingBox.y;
283 284 scrollable.scroll = scrollable.scroll + cast(size_t) offset;
285 286 }
287 288 }
289 290 }
291 292 }