1 module nodes.node;
2 
3 import fluid;
4 import std.algorithm;
5 
6 @safe:
7 
8 @("Themes can be changed at runtime https://git.samerion.com/Samerion/Fluid/issues/114")
9 unittest {
10 
11     auto theme1 = nullTheme.derive(
12         rule!Frame(
13             Rule.backgroundColor = color("#000"),
14         ),
15     );
16     auto theme2 = nullTheme.derive(
17         rule!Frame(
18             Rule.backgroundColor = color("#fff"),
19         ),
20     );
21 
22     auto deepFrame = vframe();
23     auto blackFrame = vframe(theme1);
24     auto root = vframe(
25         theme1,
26         vframe(
27             vframe(deepFrame),
28         ),
29         vframe(blackFrame),
30     );
31 
32     root.draw();
33     assert(deepFrame.pickStyle.backgroundColor == color("#000"));
34     assert(blackFrame.pickStyle.backgroundColor == color("#000"));
35     root.theme = theme2;
36     root.draw();
37     assert(deepFrame.pickStyle.backgroundColor == color("#fff"));
38     assert(blackFrame.pickStyle.backgroundColor == color("#000"));
39 
40 }
41 
42 @("Node.hide() can be used to prevent nodes from drawing")
43 unittest {
44 
45     int drawCount;
46 
47     auto root = new class Node {
48 
49         CanvasIO canvasIO;
50 
51         override void resizeImpl(Vector2) {
52             require(canvasIO);
53             minSize = Vector2(10, 10);
54         }
55 
56         override void drawImpl(Rectangle outer, Rectangle inner) {
57             drawCount++;
58             canvasIO.drawRectangle(inner,
59                 color("#123"));
60         }
61 
62     };
63 
64     auto test = testSpace(nullTheme, root);
65     test.drawAndAssert(
66         root.drawsRectangle(0, 0, 10, 10).ofColor("123"),
67         root.doesNotDraw(),
68     );
69     assert(drawCount == 1);
70 
71     // Hide the node now
72     root.hide();
73     test.drawAndAssertFailure(
74         root.draws(),
75     );
76     assert(drawCount == 1);
77 
78 }
79 
80 @("isDisabled applies transitively")
81 unittest {
82 
83     int clicked;
84 
85     Button firstButton, secondButton;
86     Space space;
87 
88     auto root = focusChain(
89         vspace(
90             firstButton = button("One", delegate { clicked++; }),
91             space       = vspace(
92                 secondButton = button("One", delegate { clicked++; }),
93             ),
94         ),
95     );
96 
97     // Disable the space and press both buttons
98     space.disable();
99     root.draw();
100     root.currentFocus = firstButton;
101     root.runInputAction!(FluidInputAction.press);
102     assert(clicked == 1);
103     root.currentFocus = secondButton;
104     root.runInputAction!(FluidInputAction.press);
105     assert(clicked == 1);
106 
107     // Enable it
108     space.enable();
109     root.draw();
110     root.currentFocus = secondButton;
111     root.runInputAction!(FluidInputAction.press);
112     assert(clicked == 2);
113 
114     // Disable the root
115     root.disable();
116     root.draw();
117     root.currentFocus = secondButton;
118     root.runInputAction!(FluidInputAction.press);
119     assert(clicked == 2);
120     root.currentFocus = firstButton;
121     root.runInputAction!(FluidInputAction.press);
122     assert(clicked == 2);
123 
124 }
125 
126 @("TreeAction can be attached to the tree, or to a branch")
127 unittest {
128 
129     import fluid.space;
130 
131     Node[4] allNodes;
132     Node[] visitedNodes;
133 
134     auto action = new class TreeAction {
135 
136         override void beforeDraw(Node node, Rectangle) {
137             visitedNodes ~= node;
138         }
139 
140     };
141 
142     auto root = allNodes[0] = vspace(
143         allNodes[1] = hspace(
144             allNodes[2] = hspace(),
145         ),
146         allNodes[3] = hspace(),
147     );
148 
149     // Start the action before creating the tree
150     root.startAction(action);
151     root.draw();
152     assert(visitedNodes == allNodes);
153 
154     // Start an action in a branch
155     visitedNodes = [];
156     allNodes[1].startAction(action);
157     root.draw();
158 
159     // @system on LDC 1.28
160     () @trusted {
161         assert(visitedNodes[].equal(allNodes[1..3]));
162     }();
163 
164 }
165 
166 @("Resizes only happen once after updateSize()")
167 unittest {
168 
169     int resizes;
170 
171     auto root = new class Node {
172 
173         override void resizeImpl(Vector2) {
174             resizes++;
175         }
176         override void drawImpl(Rectangle, Rectangle) { }
177 
178     };
179     auto test = testSpace(nullTheme, root);
180 
181     assert(resizes == 0);
182 
183     // Resizes are only done on request
184     foreach (i; 0..10) {
185         test.draw();
186         assert(resizes == 1);
187     }
188 
189     // Perform such a request
190     root.updateSize();
191     assert(resizes == 1);
192 
193     // Resize will be done right before next draw
194     test.draw();
195     assert(resizes == 2);
196 
197     // No unnecessary resizes if multiple things change in a single branch
198     root.updateSize();
199     root.updateSize();
200 
201     test.draw();
202     assert(resizes == 3);
203 
204     // Another draw, no more resizes
205     test.draw();
206     assert(resizes == 3);
207 
208 }
209 
210 @("NodeAlign changes how a node is placed in its available box")
211 unittest {
212 
213     import std.exception;
214     import core.exception;
215     import fluid.frame;
216 
217     static class Square : Frame {
218 
219         CanvasIO canvasIO;
220         Color color;
221 
222         this(Color color) @safe {
223             this.color = color;
224         }
225 
226         override void resizeImpl(Vector2) {
227             require(canvasIO);
228             minSize = Vector2(100, 100);
229         }
230 
231         override void drawImpl(Rectangle, Rectangle inner) {
232             canvasIO.drawRectangle(inner, color);
233         }
234 
235     }
236 
237     alias square = simpleConstructor!Square;
238 
239     auto colors = [
240         color("7ff0a5"),
241         color("17cccc"),
242         color("a6a415"),
243         color("cd24cf"),
244     ];
245     auto theme = nullTheme.derive(
246         rule!Frame(Rule.backgroundColor = color("1c1c1c"))
247     );
248     auto squares = [
249         square(.layout!"start",  colors[0]),
250         square(.layout!"center", colors[1]),
251         square(.layout!"end",    colors[2]),
252         square(.layout!"fill",   colors[3]),
253     ];
254     auto root = vframe(
255         .layout!(1, "fill"),
256         squares,
257     );
258     auto test = testSpace(
259         .layout!"fill",
260         theme,
261         root
262     );
263 
264     // Each square is placed in order
265     test.drawAndAssert(
266         squares[0].drawsRectangle(  0,   0, 100, 100).ofColor(colors[0]),
267         squares[1].drawsRectangle(350, 100, 100, 100).ofColor(colors[1]),
268         squares[2].drawsRectangle(700, 200, 100, 100).ofColor(colors[2]),
269 
270         // Except the last one, which is turned into a rectangle by "fill"
271         // A proper rectangle class would change its target rectangles to keep aspect ratio
272         squares[3].drawsRectangle(  0, 300, 800, 100).ofColor(colors[3]),
273     );
274 
275     // Now do the same, but expand each node
276     foreach (child; root.children) {
277         child.layout.expand = 1;
278     }
279     test.drawAndAssertFailure();  // Oops, forgot to resize!
280 
281     // Update the size
282     root.updateSize;
283     test.drawAndAssert(
284         squares[0].drawsRectangle(  0,   0, 100, 100).ofColor(colors[0]),
285         squares[1].drawsRectangle(350, 175, 100, 100).ofColor(colors[1]),
286         squares[2].drawsRectangle(700, 350, 100, 100).ofColor(colors[2]),
287         squares[3].drawsRectangle(  0, 450, 800, 150).ofColor(colors[3]),
288     );
289 
290     // Change Y alignment
291     root.children[0].layout = .layout!(1, "start", "end");
292     root.children[1].layout = .layout!(1, "center", "fill");
293     root.children[2].layout = .layout!(1, "end", "start");
294     root.children[3].layout = .layout!(1, "fill", "center");
295 
296     root.updateSize;
297     test.drawAndAssert(
298         squares[0].drawsRectangle(  0,  50, 100, 100).ofColor(colors[0]),
299         squares[1].drawsRectangle(350, 150, 100, 150).ofColor(colors[1]),
300         squares[2].drawsRectangle(700, 300, 100, 100).ofColor(colors[2]),
301         squares[3].drawsRectangle(  0, 475, 800, 100).ofColor(colors[3]),
302     );
303 
304     // Try different expand values
305     root.children[0].layout = .layout!(0, "center", "fill");
306     root.children[1].layout = .layout!(1, "center", "fill");
307     root.children[2].layout = .layout!(2, "center", "fill");
308     root.children[3].layout = .layout!(3, "center", "fill");
309 
310     root.updateSize;
311     test.drawAndAssert(
312         // The first rectangle doesn't expand so it should be exactly 100×100 in size
313         squares[0].drawsRectangle(350,   0, 100, 100).ofColor(colors[0]),
314 
315         // The remaining space is 500px, so divided into 1+2+3=6 pieces, it should be about 83.33px per piece
316         squares[1].drawsRectangle(350, 100.00, 100,  83.33).ofColor(colors[1]),
317         squares[2].drawsRectangle(350, 183.33, 100, 166.66).ofColor(colors[2]),
318         squares[3].drawsRectangle(350, 350.00, 100, 250.00).ofColor(colors[3]),
319     );
320 
321 }