1 ///
2 module fluid.space;
3 
4 import std.math;
5 import std.range;
6 import std.string;
7 import std.traits;
8 import std.algorithm;
9 
10 import fluid.node;
11 import fluid.style;
12 import fluid.utils;
13 import fluid.backend;
14 import fluid.children;
15 import fluid.container;
16 
17 
18 @safe:
19 
20 
21 /// This is a space, a basic container for other nodes.
22 ///
23 /// Nodes are laid in a column (`vframe`) or in a row (`hframe`).
24 ///
25 /// Space only acts as a container and doesn't implement styles and doesn't take focus. It's very useful as a helper for
26 /// building layout, while `Frame` remains to provide styling.
27 alias vspace = simpleConstructor!Space;
28 
29 /// ditto
30 alias hspace = simpleConstructor!(Space, (a) {
31 
32     a.directionHorizontal = true;
33 
34 });
35 
36 /// ditto
37 class Space : Node, FluidContainer {
38 
39     mixin DefineStyles;
40 
41     /// Children of this frame.
42     Children children;
43 
44     /// Defines in what directions children of this frame should be placed.
45     ///
46     /// If true, children are placed horizontally, if false, vertically.
47     bool horizontal;
48 
49     alias directionHorizontal = horizontal;
50 
51     private {
52 
53         /// Denominator for content sizing.
54         uint denominator;
55 
56         /// Space reserved for shrinking elements.
57         uint reservedSpace;
58 
59     }
60 
61     // Generate constructors
62     deprecated("Use this(NodeParams params, Node[] nodes...) instead") {
63 
64         static foreach (index; 0 .. BasicNodeParamLength) {
65 
66             this(BasicNodeParam!index params, Node[] nodes...) {
67 
68                 super(params);
69                 this.children ~= nodes;
70 
71             }
72 
73         }
74 
75     }
76 
77     this(NodeParams params, Node[] nodes...) {
78 
79         super(params);
80         this.children ~= nodes;
81 
82     }
83 
84     this() {
85 
86     }
87 
88     /// Add children.
89     pragma(inline, true)
90     void opOpAssign(string operator : "~", T)(T nodes) {
91 
92         children ~= nodes;
93 
94     }
95 
96     override Rectangle shallowScrollTo(const Node, Vector2, Rectangle, Rectangle childBox) {
97 
98         // no-op, reordering should not be done without explicit orders
99         return childBox;
100 
101     }
102 
103     protected override void resizeImpl(Vector2 available) {
104 
105         import std.algorithm : max, map, fold;
106 
107         // Now that we're recalculating the layout, we can remove the dirty flag from children
108         children.clearDirty;
109 
110         // Reset size
111         minSize = Vector2(0, 0);
112         reservedSpace = 0;
113         denominator = 0;
114 
115         // Ignore the rest if there's no children
116         if (!children.length) return;
117 
118         Vector2 maxExpandSize;
119 
120         // Collect expanding children in a separate array
121         Node[] expandChildren;
122         foreach (child; children) {
123 
124             // This node expands and isn't hidden
125             if (child.layout.expand && !child.isHidden) {
126 
127                 // Make it happen later
128                 expandChildren ~= child;
129 
130                 // Add to the denominator
131                 denominator += child.layout.expand;
132 
133             }
134 
135             // Check non-expand nodes now
136             else {
137 
138                 child.resize(tree, theme, childSpace(child, available, false));
139                 minSize = childPosition(child.minSize, minSize);
140 
141                 // Reserve space for this node
142                 reservedSpace += directionHorizontal
143                     ? cast(uint) child.minSize.x
144                     : cast(uint) child.minSize.y;
145 
146             }
147 
148         }
149 
150         // Calculate the size of expanding children last
151         foreach (child; expandChildren) {
152 
153             // Resize the child
154             child.resize(tree, theme, childSpace(child, available, false));
155 
156             const childSize = child.minSize;
157             const childExpand = child.layout.expand;
158 
159             const segmentSize = horizontal
160                 ? Vector2(childSize.x / childExpand, childSize.y)
161                 : Vector2(childSize.x, childSize.y / childExpand);
162 
163             // Reserve expand space
164             maxExpandSize.x = max(maxExpandSize.x, segmentSize.x);
165             maxExpandSize.y = max(maxExpandSize.y, segmentSize.y);
166 
167         }
168 
169         const expandSize = horizontal
170             ? Vector2(maxExpandSize.x * denominator, maxExpandSize.y)
171             : Vector2(maxExpandSize.x, maxExpandSize.y * denominator);
172 
173         // Add the expand space
174         minSize = childPosition(expandSize, minSize);
175 
176     }
177 
178     protected override void drawImpl(Rectangle, Rectangle area) {
179 
180         assertClean(children, "Children were changed without calling updateSize().");
181 
182         auto position = Vector2(area.x, area.y);
183 
184         foreach (child; filterChildren) {
185 
186             // Get params
187             const size = childSpace(child, Vector2(area.width, area.height), true);
188             const rect = Rectangle(
189                 position.x, position.y,
190                 size.x, size.y
191             );
192 
193             // Draw the child
194             child.draw(rect);
195 
196             // Offset position
197             if (directionHorizontal) position.x += cast(int) size.x;
198             else position.y += cast(int) size.y;
199 
200         }
201 
202     }
203 
204     /// List children in the space, removing all nodes queued for deletion beforehand.
205     protected auto filterChildren() {
206 
207         struct ChildIterator {
208 
209             Space node;
210 
211             int opApply(int delegate(Node) @safe fun) @trusted {
212 
213                 node.children.lock();
214                 scope (exit) node.children.unlock();
215 
216                 size_t destinationIndex = 0;
217 
218                 // Iterate through all children. When we come upon ones that are queued for deletion,
219                 foreach (sourceIndex, child; node.children) {
220 
221                     const toRemove = child.toRemove;
222                     child.toRemove = false;
223 
224                     // Ignore children that are to be removed
225                     if (toRemove) continue;
226 
227                     // Yield the child
228                     const status = fun(child);
229 
230                     // Move the child if needed
231                     if (sourceIndex != destinationIndex) {
232 
233                         node.children.forceMutable[destinationIndex] = child;
234 
235                     }
236 
237                     // Stop iteration if requested
238                     else if (status) return status;
239 
240                     // Set space for next nodes
241                     destinationIndex++;
242 
243 
244                 }
245 
246                 // Adjust length
247                 node.children.forceMutable.length = destinationIndex;
248 
249                 return 0;
250 
251             }
252 
253         }
254 
255         return ChildIterator(this);
256 
257     }
258 
259     /// Iterate over every child and perform the painting function. Will automatically remove nodes queued for removal.
260     /// Returns: An iterator that goes over all nodes.
261     deprecated("Use filterChildren instead")
262     protected void drawChildren(void delegate(Node) @safe painter) {
263 
264         Node[] leftovers;
265 
266         children.lock();
267         scope (exit) children.unlock();
268 
269         // Draw each child and get rid of removed children
270         auto range = children[]
271 
272             // Check if the node is queued for removal
273             .filter!((node) {
274                 const status = node.toRemove;
275                 node.toRemove = false;
276                 return !status;
277             })
278 
279             // Draw the node
280             .tee!((node) => painter(node));
281 
282         // Do what we ought to do
283         () @trusted {
284 
285             // Process the children and move them back to the original array
286             auto leftovers = range.moveAll(children.forceMutable);
287 
288             // Adjust the array size
289             children.forceMutable.length -= leftovers.length;
290 
291         }();
292 
293     }
294 
295     protected override bool hoveredImpl(Rectangle, Vector2) const {
296 
297         return false;
298 
299     }
300 
301     protected override inout(Style) pickStyle() inout {
302 
303         return null;
304 
305     }
306 
307     /// Params:
308     ///     child     = Child size to add.
309     ///     previous  = Previous position.
310     private Vector2 childPosition(Vector2 child, Vector2 previous) const {
311 
312         import std.algorithm : max;
313 
314         // Horizontal
315         if (directionHorizontal) {
316 
317             return Vector2(
318                 previous.x + child.x,
319                 max(minSize.y, child.y),
320             );
321 
322         }
323 
324         // Vertical
325         else return Vector2(
326             max(minSize.x, child.x),
327             previous.y + child.y,
328         );
329 
330     }
331 
332     /// Get space for a child.
333     /// Params:
334     ///     child     = Child to place
335     ///     available = Available space
336     private Vector2 childSpace(const Node child, Vector2 available, bool stateful) const
337     in(
338         child.isHidden || child.layout.expand <= denominator,
339         format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"(
340             typeid(this), typeid(child), child.layout.expand, denominator,
341         )
342     )
343     out(
344         r; [r.tupleof].all!isFinite,
345         format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"(
346             typeid(child), r, available, child.layout.expand, denominator, reservedSpace
347         )
348     )
349     do {
350 
351         // Hidden, give it no space
352         if (child.isHidden) return Vector2();
353 
354         // Horizontal
355         if (directionHorizontal) {
356 
357             const avail = (available.x - reservedSpace);
358             const minSize = stateful
359                 ? child.minSize.x
360                 : available.x;
361 
362             return Vector2(
363                 child.layout.expand
364                     ? avail * child.layout.expand / denominator
365                     : minSize,
366                 available.y,
367             );
368 
369         }
370 
371         // Vertical
372         else {
373 
374             const avail = (available.y - reservedSpace);
375             const minSize = stateful
376                 ? child.minSize.y
377                 : available.y;
378 
379             return Vector2(
380                 available.x,
381                 child.layout.expand
382                     ? avail * child.layout.expand / denominator
383                     : minSize,
384             );
385 
386         }
387 
388     }
389 
390 }
391 
392 ///
393 unittest {
394 
395     import fluid;
396 
397     // A vspace will align all its content in a column
398     vspace(
399         label("First entry"),
400         label("Second entry"),
401         label("Third entry"),
402     );
403 
404     // hspace will lay out the nodes in a row
405     hspace(
406         label("One, "),
407         label("Two, "),
408         label("Three!"),
409     );
410 
411     // Combine them to quickly build layouts!
412     vspace(
413         label("Are you sure you want to proceed?"),
414         hspace(
415             button("Yes", delegate { }),
416             button("Cancel", delegate { }),
417         ),
418     );
419 
420 }
421 
422 unittest {
423 
424     class Square : Node {
425 
426         mixin implHoveredRect;
427 
428         Color color;
429 
430         this(Color color) {
431             this.color = color;
432         }
433 
434         override void resizeImpl(Vector2) {
435             minSize = Vector2(50, 50);
436         }
437 
438         override void drawImpl(Rectangle, Rectangle inner) {
439             io.drawRectangle(inner, this.color);
440         }
441 
442     }
443 
444     auto io = new HeadlessBackend;
445     auto root = vspace(
446         new Square(color!"000"),
447         new Square(color!"001"),
448         new Square(color!"002"),
449         hspace(
450             new Square(color!"010"),
451             new Square(color!"011"),
452             new Square(color!"012"),
453         ),
454     );
455 
456     root.io = io;
457     root.theme = nullTheme;
458     root.draw();
459 
460     // vspace
461     io.assertRectangle(Rectangle(0,   0, 50, 50), color!"000");
462     io.assertRectangle(Rectangle(0,  50, 50, 50), color!"001");
463     io.assertRectangle(Rectangle(0, 100, 50, 50), color!"002");
464 
465     // hspace
466     io.assertRectangle(Rectangle(  0, 150, 50, 50), color!"010");
467     io.assertRectangle(Rectangle( 50, 150, 50, 50), color!"011");
468     io.assertRectangle(Rectangle(100, 150, 50, 50), color!"012");
469 
470 }
471 
472 unittest {
473 
474     import fluid.frame;
475     import fluid.structs;
476 
477     auto io = new HeadlessBackend;
478     auto root = hspace(
479         layout!"fill",
480         vframe(layout!1),
481         vframe(layout!2),
482         vframe(layout!1),
483     );
484 
485     root.io = io;
486     root.theme = nullTheme.makeTheme!q{
487         Frame.styleAdd.backgroundColor = color!"7d9";
488     };
489 
490     // Frame 1
491     {
492         root.draw();
493         io.assertRectangle(Rectangle(0,   0, 0, 0), color!"7d9");
494         io.assertRectangle(Rectangle(200, 0, 0, 0), color!"7d9");
495         io.assertRectangle(Rectangle(600, 0, 0, 0), color!"7d9");
496     }
497 
498     // Fill all nodes
499     foreach (child; root.children) {
500         child.layout.nodeAlign = NodeAlign.fill;
501     }
502     root.updateSize();
503 
504     {
505         io.nextFrame;
506         root.draw();
507         io.assertRectangle(Rectangle(  0, 0, 200, 600), color!"7d9");
508         io.assertRectangle(Rectangle(200, 0, 400, 600), color!"7d9");
509         io.assertRectangle(Rectangle(600, 0, 200, 600), color!"7d9");
510     }
511 
512     const alignments = [NodeAlign.start, NodeAlign.center, NodeAlign.end];
513 
514     // Make Y alignment different across all three
515     foreach (pair; root.children.zip(alignments)) {
516         pair[0].layout.nodeAlign = pair[1];
517     }
518 
519     {
520         io.nextFrame;
521         root.draw();
522         io.assertRectangle(Rectangle(  0,   0, 0, 0), color!"7d9");
523         io.assertRectangle(Rectangle(400, 300, 0, 0), color!"7d9");
524         io.assertRectangle(Rectangle(800, 600, 0, 0), color!"7d9");
525     }
526 
527 }
528 
529 unittest {
530 
531     import fluid.frame;
532     import fluid.structs;
533 
534     auto io = new HeadlessBackend(Vector2(270, 270));
535     auto root = hframe(
536         layout!"fill",
537         vspace(layout!2),
538         vframe(
539             layout!(1, "fill"),
540             hspace(layout!2),
541             hframe(
542                 layout!(1, "fill"),
543                 vframe(
544                     layout!(1, "fill"),
545                     hframe(
546                         layout!(1, "fill")
547                     ),
548                     hspace(layout!2),
549                 ),
550                 vspace(layout!2),
551             )
552         ),
553     );
554 
555     root.theme = nullTheme.makeTheme!q{
556         Frame.styleAdd.backgroundColor = color!"0004";
557     };
558     root.io = io;
559     root.draw();
560 
561     io.assertRectangle(Rectangle(  0,   0, 270, 270), color!"0004");
562     io.assertRectangle(Rectangle(180,   0,  90, 270), color!"0004");
563     io.assertRectangle(Rectangle(180, 180,  90,  90), color!"0004");
564     io.assertRectangle(Rectangle(180, 180,  30,  90), color!"0004");
565     io.assertRectangle(Rectangle(180, 180,  30,  30), color!"0004");
566 
567 }