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 }