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 }