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.theme = 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 unittest {
139 
140     import fluid.frame;
141 
142     Frame bigFrame, smallFrame;
143     int bigDrawn, smallDrawn;
144 
145     auto io = new HeadlessBackend;
146     auto slot = switchSlot(
147         bigFrame = new class Frame {
148             override void resizeImpl(Vector2) {
149                 minSize = Vector2(300, 300);
150             }
151             override void drawImpl(Rectangle outer, Rectangle) {
152                 io.drawRectangle(outer, color!"f00");
153                 bigDrawn++;
154             }
155         },
156         smallFrame = new class Frame {
157             override void resizeImpl(Vector2) {
158                 minSize = Vector2(100, 100);
159             }
160             override void drawImpl(Rectangle outer, Rectangle) {
161                 io.drawRectangle(outer, color!"0f0");
162                 smallDrawn++;
163             }
164         },
165     );
166 
167     slot.io = io;
168 
169     // By default, there should be enough space to draw the big frame
170     slot.draw();
171 
172     assert(slot.node is bigFrame);
173     assert(bigDrawn == 1);
174     assert(smallDrawn == 0);
175 
176     // Reduce the viewport, this time the small frame should be drawn
177     io.nextFrame;
178     io.windowSize = Vector2(200, 200);
179     slot.draw();
180 
181     assert(slot.node is smallFrame);
182     assert(bigDrawn == 1);
183     assert(smallDrawn == 1);
184 
185     // Do it again, but make it so neither fit
186     io.nextFrame;
187     io.windowSize = Vector2(50, 50);
188     slot.draw();
189 
190     // The small one should be drawn regardless
191     assert(slot.node is smallFrame);
192     assert(bigDrawn == 1);
193     assert(smallDrawn == 2);
194 
195     // Unless a null node is added
196     io.nextFrame;
197     slot.availableNodes ~= null;
198     slot.updateSize();
199     slot.draw();
200 
201     assert(slot.node is null);
202     assert(bigDrawn == 1);
203     assert(smallDrawn == 2);
204 
205     // Resize to fit the big node
206     io.nextFrame;
207     io.windowSize = Vector2(400, 400);
208     slot.draw();
209 
210     assert(slot.node is bigFrame);
211     assert(bigDrawn == 2);
212     assert(smallDrawn == 2);
213 
214 }
215 
216 unittest {
217 
218     import fluid.frame;
219     import fluid.structs;
220 
221     int principalDrawn, deputyDrawn;
222 
223     auto io = new HeadlessBackend;
224     auto principal = switchSlot(
225         layout!(1, "fill"),
226         new class Frame {
227             override void resizeImpl(Vector2) {
228                 minSize = Vector2(200, 200);
229             }
230             override void drawImpl(Rectangle outer, Rectangle) {
231                 io.drawRectangle(outer, color!"f00");
232                 principalDrawn++;
233             }
234         },
235         null
236     );
237     auto deputy = principal.retry(
238         layout!(1, "fill"),
239         new class Frame {
240             override void resizeImpl(Vector2 space) {
241                 minSize = Vector2(50, 200);
242             }
243             override void drawImpl(Rectangle outer, Rectangle) {
244                 io.drawRectangle(outer, color!"f00");
245                 deputyDrawn++;
246             }
247         }
248     );
249     auto root = vframe(
250         layout!(1, "fill"),
251         hframe(
252             layout!(1, "fill"),
253             deputy,
254         ),
255         hframe(
256             layout!(1, "fill"),
257             principal,
258         ),
259     );
260 
261     root.io = io;
262 
263     // At the default size, the principal should be preferred
264     root.draw();
265 
266     assert(principalDrawn == 1);
267     assert(deputyDrawn == 0);
268 
269     // Resize the window so that the principal can't fit
270     io.nextFrame;
271     io.windowSize = Vector2(300, 300);
272 
273     root.draw();
274 
275     assert(principalDrawn == 1);
276     assert(deputyDrawn == 1);
277 
278 }
279 
280 unittest {
281 
282     import std.algorithm;
283 
284     import fluid.space;
285     import fluid.structs;
286 
287     SwitchSlot slot;
288 
289     auto checker = new class Node {
290 
291         Vector2 size;
292         Vector2[] spacesGiven;
293 
294         override void resizeImpl(Vector2 space) {
295 
296             spacesGiven ~= space;
297             size = minSize = Vector2(500, 200);
298 
299         }
300 
301         override void drawImpl(Rectangle, Rectangle) {
302 
303         }
304 
305     };
306 
307     auto parentSlot = switchSlot(checker, null);
308     auto childSlot = parentSlot.retry(checker);
309 
310     auto root = vspace(
311         layout!"fill",
312         nullTheme,
313 
314         // Two slots: child slot that gets resized earlier
315         hspace(
316             layout!"fill",
317             childSlot,
318         ),
319 
320         // Parent slot that doesn't give enough space for the child to fit
321         hspace(
322             layout!"fill",
323             vspace(
324                 layout!(1, "fill"),
325                 parentSlot,
326             ),
327             vspace(
328                 layout!(3, "fill"),
329             ),
330         ),
331     );
332 
333     root.draw();
334 
335     // The principal slot gives the least space, namely the width of the window divided by 4
336     assert(checker.spacesGiven.map!"a.x".minElement == HeadlessBackend.defaultWindowSize.x / 4);
337 
338     // The window size that is accepted is equal to its size, as it was assigned by the fallback slot
339     assert(checker.spacesGiven[$-1] == checker.size);
340 
341     // A total of three resizes were performed: one by the fallback, one by the parent and one, final, by the parent
342     // using previous parameters
343     assert(checker.spacesGiven.length == 3);
344 
345     // The first one (which should be the child's) has the largest width given, equal to the window width
346     assert(checker.spacesGiven[0].x == HeadlessBackend.defaultWindowSize.x);
347 
348 }