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