1 ///
2 module fluid.switch_slot;
3 
4 import fluid.node;
5 import fluid.utils;
6 import fluid.style;
7 import fluid.backend;
8 
9 
10 @safe:
11 
12 
13 /// A switch slot will try each of its children and pick the first one that fits the available space. If the a node
14 /// is too large to fit, it will try the next one in the list until it finds one that matches, or the last node in the
15 /// list.
16 ///
17 /// `null` is an acceptable value, indicating that no node should be drawn.
18 alias switchSlot = simpleConstructor!SwitchSlot;
19 
20 /// ditto
21 class SwitchSlot : Node {
22 
23     public {
24 
25         Node[] availableNodes;
26         Node node;
27 
28         /// If present, this node will only be drawn in case its principal node is hidden. In case the principal node is
29         /// another `SwitchSlot`, this might be because it failed to match any non-null node.
30         Node principalNode;
31 
32     }
33 
34     protected {
35 
36         /// Last available space assigned to this node.
37         Vector2 _availableSpace;
38 
39     }
40 
41     @property {
42 
43         alias isHidden = typeof(super).isHidden;
44 
45         override bool isHidden() const return {
46 
47             // Tree is available and resized
48             if (tree && !tree.resizePending) {
49 
50                 // Principal node is visible, hide self
51                 if (principalNode && !principalNode.isHidden)
52                     return true;
53 
54                 // Hide if no node was chosen
55                 return super.isHidden || node is null;
56 
57             }
58 
59             return super.isHidden;
60 
61         }
62 
63     }
64 
65     this(Node[] nodes...) {
66 
67         this.availableNodes ~= nodes;
68 
69     }
70 
71     /// Create a new slot that will only draw if this slot is hidden or ends up with a `null` node.
72     SwitchSlot retry(Args...)(Args args) {
73 
74         auto slot = switchSlot(args);
75         slot.principalNode = this;
76         return slot;
77 
78     }
79 
80     override void resizeImpl(Vector2 availableSpace) {
81 
82         minSize = Vector2();
83         this.node = null;
84         _availableSpace = availableSpace;
85 
86         // Try each option
87         foreach (i, node; availableNodes) {
88 
89             this.node = node;
90 
91             // Null node reached, stop with no minSize
92             if (node is null) return;
93 
94             auto previousTree = node.tree;
95             auto previousTheme = node.theme;
96             auto previousSize = node.minSize;
97 
98             node.resize(tree, theme, availableSpace);
99 
100             // Stop if it fits within available space
101             if (node.minSize.x <= availableSpace.x && node.minSize.y <= availableSpace.y) break;
102 
103             // Restore previous info, unless this is the last node
104             if (i+1 != availableNodes.length && previousTree) {
105 
106                 // Resize the node again to recursively restore old parameters
107                 node.tree = null;
108                 node.inheritTheme(Theme.init);
109                 node.resize(previousTree, previousTheme, previousSize);
110 
111             }
112 
113         }
114 
115         // Copy minSize
116         minSize = node.minSize;
117 
118     }
119 
120     override void drawImpl(Rectangle outer, Rectangle inner) {
121 
122         // No node to draw, stop
123         if (node is null) return;
124 
125         // Draw the node
126         node.draw(inner);
127 
128     }
129 
130     override bool hoveredImpl(Rectangle, Vector2) const {
131 
132         return false;
133 
134     }
135 
136 }
137 
138 @("SwitchSlot works")
139 unittest {
140 
141     import fluid.frame;
142 
143     Frame bigFrame, smallFrame;
144     int bigDrawn, smallDrawn;
145 
146     auto io = new HeadlessBackend;
147     auto slot = switchSlot(
148         bigFrame = new class Frame {
149             override void resizeImpl(Vector2) {
150                 minSize = Vector2(300, 300);
151             }
152             override void drawImpl(Rectangle outer, Rectangle) {
153                 io.drawRectangle(outer, color!"f00");
154                 bigDrawn++;
155             }
156         },
157         smallFrame = new class Frame {
158             override void resizeImpl(Vector2) {
159                 minSize = Vector2(100, 100);
160             }
161             override void drawImpl(Rectangle outer, Rectangle) {
162                 io.drawRectangle(outer, color!"0f0");
163                 smallDrawn++;
164             }
165         },
166     );
167 
168     slot.io = io;
169 
170     // By default, there should be enough space to draw the big frame
171     slot.draw();
172 
173     assert(slot.node is bigFrame);
174     assert(bigDrawn == 1);
175     assert(smallDrawn == 0);
176 
177     // Reduce the viewport, this time the small frame should be drawn
178     io.nextFrame;
179     io.windowSize = Vector2(200, 200);
180     slot.draw();
181 
182     assert(slot.node is smallFrame);
183     assert(bigDrawn == 1);
184     assert(smallDrawn == 1);
185 
186     // Do it again, but make it so neither fit
187     io.nextFrame;
188     io.windowSize = Vector2(50, 50);
189     slot.draw();
190 
191     // The small one should be drawn regardless
192     assert(slot.node is smallFrame);
193     assert(bigDrawn == 1);
194     assert(smallDrawn == 2);
195 
196     // Unless a null node is added
197     io.nextFrame;
198     slot.availableNodes ~= null;
199     slot.updateSize();
200     slot.draw();
201 
202     assert(slot.node is null);
203     assert(bigDrawn == 1);
204     assert(smallDrawn == 2);
205 
206     // Resize to fit the big node
207     io.nextFrame;
208     io.windowSize = Vector2(400, 400);
209     slot.draw();
210 
211     assert(slot.node is bigFrame);
212     assert(bigDrawn == 2);
213     assert(smallDrawn == 2);
214 
215 }
216 
217 unittest {
218 
219     import fluid.frame;
220     import fluid.structs;
221 
222     int principalDrawn, deputyDrawn;
223 
224     auto io = new HeadlessBackend;
225     auto principal = switchSlot(
226         layout!(1, "fill"),
227         new class Frame {
228             override void resizeImpl(Vector2) {
229                 minSize = Vector2(200, 200);
230             }
231             override void drawImpl(Rectangle outer, Rectangle) {
232                 io.drawRectangle(outer, color!"f00");
233                 principalDrawn++;
234             }
235         },
236         null
237     );
238     auto deputy = principal.retry(
239         layout!(1, "fill"),
240         new class Frame {
241             override void resizeImpl(Vector2 space) {
242                 minSize = Vector2(50, 200);
243             }
244             override void drawImpl(Rectangle outer, Rectangle) {
245                 io.drawRectangle(outer, color!"f00");
246                 deputyDrawn++;
247             }
248         }
249     );
250     auto root = vframe(
251         layout!(1, "fill"),
252         hframe(
253             layout!(1, "fill"),
254             deputy,
255         ),
256         hframe(
257             layout!(1, "fill"),
258             principal,
259         ),
260     );
261 
262     root.io = io;
263 
264     // At the default size, the principal should be preferred
265     root.draw();
266 
267     assert(principalDrawn == 1);
268     assert(deputyDrawn == 0);
269 
270     // Resize the window so that the principal can't fit
271     io.nextFrame;
272     io.windowSize = Vector2(300, 300);
273 
274     root.draw();
275 
276     assert(principalDrawn == 1);
277     assert(deputyDrawn == 1);
278 
279 }
280 
281 unittest {
282 
283     import std.algorithm;
284 
285     import fluid.space;
286     import fluid.structs;
287 
288     SwitchSlot slot;
289 
290     auto checker = new class Node {
291 
292         Vector2 size;
293         Vector2[] spacesGiven;
294 
295         override void resizeImpl(Vector2 space) {
296 
297             spacesGiven ~= space;
298             size = minSize = Vector2(500, 200);
299 
300         }
301 
302         override void drawImpl(Rectangle, Rectangle) {
303 
304         }
305 
306     };
307 
308     auto parentSlot = switchSlot(checker, null);
309     auto childSlot = parentSlot.retry(checker);
310 
311     auto root = vspace(
312         layout!"fill",
313         nullTheme,
314 
315         // Two slots: child slot that gets resized earlier
316         hspace(
317             layout!"fill",
318             childSlot,
319         ),
320 
321         // Parent slot that doesn't give enough space for the child to fit
322         hspace(
323             layout!"fill",
324             vspace(
325                 layout!(1, "fill"),
326                 parentSlot,
327             ),
328             vspace(
329                 layout!(3, "fill"),
330             ),
331         ),
332     );
333 
334     root.draw();
335 
336     // The principal slot gives the least space, namely the width of the window divided by 4
337     assert(checker.spacesGiven.map!"a.x".minElement == HeadlessBackend.defaultWindowSize.x / 4);
338 
339     // The window size that is accepted is equal to its size, as it was assigned by the fallback slot
340     assert(checker.spacesGiven[$-1] == checker.size);
341 
342     // A total of three resizes were performed: one by the fallback, one by the parent and one, final, by the parent
343     // using previous parameters
344     assert(checker.spacesGiven.length == 3);
345 
346     // The first one (which should be the child's) has the largest width given, equal to the window width
347     assert(checker.spacesGiven[0].x == HeadlessBackend.defaultWindowSize.x);
348 
349 }