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