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         size_t visibleChildren;
107         foreach (child; children) {
108 
109             visibleChildren += !child.isHidden;
110 
111             // This node expands and isn't hidden
112             if (child.layout.expand && !child.isHidden) {
113 
114                 // Make it happen later
115                 expandChildren ~= child;
116 
117                 // Add to the denominator
118                 denominator += child.layout.expand;
119 
120             }
121 
122             // Check non-expand nodes now
123             else {
124 
125                 child.resize(tree, theme, childSpace(child, available, false));
126                 minSize = addSize(child.minSize, minSize);
127 
128                 // Reserve space for this node
129                 reservedSpace += directionHorizontal
130                     ? child.minSize.x
131                     : child.minSize.y;
132 
133             }
134 
135         }
136 
137         const gapSpace 
138             = visibleChildren == 0 ? 0
139             : isHorizontal ? style.gap.sideX * (visibleChildren - 1u)
140             :                style.gap.sideY * (visibleChildren - 1u);
141 
142         // Reserve space for gaps
143         reservedSpace += gapSpace;
144 
145         if (isHorizontal)
146             minSize.x += gapSpace;
147         else
148             minSize.y += gapSpace;
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 = addSize(expandSize, minSize);
175 
176     }
177 
178     unittest {
179 
180         import std.meta;
181         import fluid.frame;
182         import fluid.size_lock;
183 
184         auto theme = nullTheme.derive(
185             rule!Space(
186                 Rule.gap = 12,
187             ),
188         );
189 
190         auto root = vspace(
191             theme,
192             sizeLock!vframe(
193                 sizeLimitY = 200
194             ),
195             sizeLock!vframe(
196                 sizeLimitY = 200
197             ),
198             sizeLock!vframe(
199                 sizeLimitY = 200
200             ),
201         );
202         root.draw();
203 
204         assert(isClose(root.minSize.y, 200 * 3 + 12 * 2));
205 
206     }
207 
208     protected override void drawImpl(Rectangle, Rectangle area) {
209 
210         assertClean(children, "Children were changed without calling updateSize().");
211 
212         auto position = start(area);
213 
214         foreach (child; filterChildren) {
215 
216             // Ignore if this child is not visible
217             if (child.isHidden) continue;
218 
219             // Get params
220             const size = childSpace(child, size(area), true);
221             const rect = Rectangle(
222                 position.x, position.y,
223                 size.x, size.y
224             );
225 
226             // Draw the child
227             child.draw(rect);
228 
229             // Offset position
230             position = childOffset(position, size);
231 
232         }
233 
234     }
235 
236     /// List children in the space, removing all nodes queued for deletion beforehand.
237     protected auto filterChildren() {
238 
239         struct ChildIterator {
240 
241             Space node;
242 
243             int opApply(int delegate(Node) @safe fun) @trusted {
244 
245                 foreach (_, node; this) {
246 
247                     if (auto result = fun(node)) {
248                         return result;
249                     }
250 
251                 }
252                 return 0;
253 
254             }
255 
256             int opApply(int delegate(size_t index, Node) @safe fun) @trusted {
257 
258                 node.children.lock();
259                 scope (exit) node.children.unlock();
260 
261                 size_t destinationIndex = 0;
262                 int end = 0;
263 
264                 // Iterate through all children. When we come upon ones that are queued for deletion,
265                 foreach (sourceIndex, child; node.children) {
266 
267                     const toRemove = child.toRemove;
268                     child.toRemove = false;
269 
270                     // Ignore children that are to be removed
271                     if (toRemove) continue;
272 
273                     // Yield the child
274                     if (!end)
275                         end = fun(destinationIndex, child);
276 
277                     // Move the child if needed
278                     if (sourceIndex != destinationIndex) {
279 
280                         node.children.forceMutable[destinationIndex] = child;
281 
282                     }
283 
284                     // Stop iteration if requested — and if there's nothing to move
285                     else if (end) return end;
286 
287                     // Set space for next nodes
288                     destinationIndex++;
289 
290 
291                 }
292 
293                 // Adjust length
294                 node.children.forceMutable.length = destinationIndex;
295 
296                 return 0;
297 
298             }
299 
300         }
301 
302         return ChildIterator(this);
303 
304     }
305 
306     /// Space does not take hover; isHovered is always false.
307     protected override bool hoveredImpl(Rectangle, Vector2) {
308 
309         return false;
310 
311     }
312 
313     /// Params:
314     ///     child     = Child size to add.
315     ///     previous  = Previous position.
316     private Vector2 addSize(Vector2 child, Vector2 previous) const {
317 
318         import std.algorithm : max;
319 
320         // Horizontal
321         if (directionHorizontal) {
322 
323             return Vector2(
324                 previous.x + child.x,
325                 max(minSize.y, child.y),
326             );
327 
328         }
329 
330         // Vertical
331         else return Vector2(
332             max(minSize.x, child.x),
333             previous.y + child.y,
334         );
335 
336     }
337 
338     /// Calculate the offset for the next node, given the `childSpace` result for its previous sibling.
339     protected Vector2 childOffset(Vector2 currentOffset, Vector2 childSpace) {
340 
341         if (isHorizontal)
342             return currentOffset + Vector2(childSpace.x + style.gap.sideX, 0);
343         else
344             return currentOffset + Vector2(0, childSpace.y + style.gap.sideY);
345 
346     }
347 
348     /// Get space for a child.
349     /// Params:
350     ///     child     = Child to place
351     ///     available = Available space
352     protected Vector2 childSpace(const Node child, Vector2 available, bool stateful = true) const
353     in(
354         child.isHidden || child.layout.expand <= denominator,
355         format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"(
356             typeid(this), typeid(child), child.layout.expand, denominator,
357         )
358     )
359     out(
360         r; [r.tupleof].all!isFinite,
361         format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"(
362             typeid(child), r, available, child.layout.expand, denominator, reservedSpace
363         )
364     )
365     do {
366 
367         // Hidden, give it no space
368         if (child.isHidden) return Vector2();
369 
370         // Horizontal
371         if (directionHorizontal) {
372 
373             const avail = (available.x - reservedSpace);
374             const minSize = stateful
375                 ? child.minSize.x
376                 : available.x;
377 
378             return Vector2(
379                 child.layout.expand
380                     ? avail * child.layout.expand / denominator
381                     : minSize,
382                 available.y,
383             );
384 
385         }
386 
387         // Vertical
388         else {
389 
390             const avail = (available.y - reservedSpace);
391             const minSize = stateful
392                 ? child.minSize.y
393                 : available.y;
394 
395             return Vector2(
396                 available.x,
397                 child.layout.expand
398                     ? avail * child.layout.expand / denominator
399                     : minSize,
400             );
401 
402         }
403 
404     }
405 
406 }
407 
408 ///
409 unittest {
410 
411     import fluid;
412 
413     // A vspace will align all its content in a column
414     vspace(
415         label("First entry"),
416         label("Second entry"),
417         label("Third entry"),
418     );
419 
420     // hspace will lay out the nodes in a row
421     hspace(
422         label("One, "),
423         label("Two, "),
424         label("Three!"),
425     );
426 
427     // Combine them to quickly build layouts!
428     vspace(
429         label("Are you sure you want to proceed?"),
430         hspace(
431             button("Yes", delegate { }),
432             button("Cancel", delegate { }),
433         ),
434     );
435 
436 }
437 
438 unittest {
439 
440     class Square : Node {
441 
442         Color color;
443 
444         this(Color color) {
445             this.color = color;
446         }
447 
448         override void resizeImpl(Vector2) {
449             minSize = Vector2(50, 50);
450         }
451 
452         override void drawImpl(Rectangle, Rectangle inner) {
453             io.drawRectangle(inner, this.color);
454         }
455 
456     }
457 
458     auto io = new HeadlessBackend;
459     auto root = vspace(
460         new Square(color!"000"),
461         new Square(color!"001"),
462         new Square(color!"002"),
463         hspace(
464             new Square(color!"010"),
465             new Square(color!"011"),
466             new Square(color!"012"),
467         ),
468     );
469 
470     root.io = io;
471     root.theme = nullTheme;
472     root.draw();
473 
474     // vspace
475     io.assertRectangle(Rectangle(0,   0, 50, 50), color!"000");
476     io.assertRectangle(Rectangle(0,  50, 50, 50), color!"001");
477     io.assertRectangle(Rectangle(0, 100, 50, 50), color!"002");
478 
479     // hspace
480     io.assertRectangle(Rectangle(  0, 150, 50, 50), color!"010");
481     io.assertRectangle(Rectangle( 50, 150, 50, 50), color!"011");
482     io.assertRectangle(Rectangle(100, 150, 50, 50), color!"012");
483 
484 }
485 
486 unittest {
487 
488     import fluid.frame;
489     import fluid.structs;
490 
491     auto io = new HeadlessBackend;
492     auto root = hspace(
493         layout!"fill",
494         vframe(layout!1),
495         vframe(layout!2),
496         vframe(layout!1),
497     );
498 
499     with (Rule)
500     root.theme = nullTheme.derive(
501         rule!Frame(backgroundColor = color!"7d9"),
502     );
503     root.io = io;
504 
505     // Frame 1
506     {
507         root.draw();
508         io.assertRectangle(Rectangle(0,   0, 0, 0), color!"7d9");
509         io.assertRectangle(Rectangle(200, 0, 0, 0), color!"7d9");
510         io.assertRectangle(Rectangle(600, 0, 0, 0), color!"7d9");
511     }
512 
513     // Fill all nodes
514     foreach (child; root.children) {
515         child.layout.nodeAlign = NodeAlign.fill;
516     }
517     root.updateSize();
518 
519     {
520         io.nextFrame;
521         root.draw();
522         io.assertRectangle(Rectangle(  0, 0, 200, 600), color!"7d9");
523         io.assertRectangle(Rectangle(200, 0, 400, 600), color!"7d9");
524         io.assertRectangle(Rectangle(600, 0, 200, 600), color!"7d9");
525     }
526 
527     const alignments = [NodeAlign.start, NodeAlign.center, NodeAlign.end];
528 
529     // Make Y alignment different across all three
530     foreach (pair; root.children.zip(alignments)) {
531         pair[0].layout.nodeAlign = pair[1];
532     }
533 
534     {
535         io.nextFrame;
536         root.draw();
537         io.assertRectangle(Rectangle(  0,   0, 0, 0), color!"7d9");
538         io.assertRectangle(Rectangle(400, 300, 0, 0), color!"7d9");
539         io.assertRectangle(Rectangle(800, 600, 0, 0), color!"7d9");
540     }
541 
542 }
543 
544 unittest {
545 
546     import fluid.frame;
547     import fluid.structs;
548 
549     auto io = new HeadlessBackend(Vector2(270, 270));
550     auto root = hframe(
551         layout!"fill",
552         vspace(layout!2),
553         vframe(
554             layout!(1, "fill"),
555             hspace(layout!2),
556             hframe(
557                 layout!(1, "fill"),
558                 vframe(
559                     layout!(1, "fill"),
560                     hframe(
561                         layout!(1, "fill")
562                     ),
563                     hspace(layout!2),
564                 ),
565                 vspace(layout!2),
566             )
567         ),
568     );
569 
570     with (Rule)
571     root.theme = nullTheme.derive(
572         rule!Frame(backgroundColor = color!"0004"),
573     );
574     root.io = io;
575     root.draw();
576 
577     io.assertRectangle(Rectangle(  0,   0, 270, 270), color!"0004");
578     io.assertRectangle(Rectangle(180,   0,  90, 270), color!"0004");
579     io.assertRectangle(Rectangle(180, 180,  90,  90), color!"0004");
580     io.assertRectangle(Rectangle(180, 180,  30,  90), color!"0004");
581     io.assertRectangle(Rectangle(180, 180,  30,  30), color!"0004");
582 
583 }
584 
585 // https://git.samerion.com/Samerion/Fluid/issues/58
586 unittest {
587 
588     import fluid.frame;
589     import fluid.label;
590     import fluid.structs;
591 
592     auto fill = layout!(1, "fill");
593     auto io = new HeadlessBackend;
594     auto myTheme = nullTheme.derive(
595         rule!Frame(Rule.backgroundColor = color!"#303030"),
596         rule!Label(Rule.backgroundColor = color!"#e65bb8"),
597     );
598     auto root = hframe(
599         fill,
600         myTheme,
601         label(fill, "1"),
602         label(fill, "2"),
603         label(fill, "3"),
604         label(fill, "4"),
605         label(fill, "5"),
606         label(fill, "6"),
607         label(fill, "7"),
608         label(fill, "8"),
609         label(fill, "9"),
610         label(fill, "10"),
611         label(fill, "11"),
612         label(fill, "12"),
613     );
614 
615     root.io = io;
616     root.draw();
617 
618     io.assertRectangle(Rectangle( 0*800/12f, 0, 66.66, 600), color!"#e65bb8");
619     io.assertRectangle(Rectangle( 1*800/12f, 0, 66.66, 600), color!"#e65bb8");
620     io.assertRectangle(Rectangle( 2*800/12f, 0, 66.66, 600), color!"#e65bb8");
621     io.assertRectangle(Rectangle( 3*800/12f, 0, 66.66, 600), color!"#e65bb8");
622     io.assertRectangle(Rectangle( 4*800/12f, 0, 66.66, 600), color!"#e65bb8");
623     io.assertRectangle(Rectangle( 5*800/12f, 0, 66.66, 600), color!"#e65bb8");
624     io.assertRectangle(Rectangle( 6*800/12f, 0, 66.66, 600), color!"#e65bb8");
625     io.assertRectangle(Rectangle( 7*800/12f, 0, 66.66, 600), color!"#e65bb8");
626     io.assertRectangle(Rectangle( 8*800/12f, 0, 66.66, 600), color!"#e65bb8");
627     io.assertRectangle(Rectangle( 9*800/12f, 0, 66.66, 600), color!"#e65bb8");
628     io.assertRectangle(Rectangle(10*800/12f, 0, 66.66, 600), color!"#e65bb8");
629     io.assertRectangle(Rectangle(11*800/12f, 0, 66.66, 600), color!"#e65bb8");
630 
631 }
632 
633 unittest {
634 
635     import fluid.frame;
636     import fluid.theme;
637     import fluid.structs : layout;
638 
639     auto io = new HeadlessBackend;
640     auto theme = nullTheme.derive(
641         rule!Space(
642             gap = 4,
643         ),
644         rule!Frame(
645             backgroundColor = color("#f00"),
646         ),
647     );
648     auto root = vspace(
649         layout!"fill",
650         theme,
651         vframe(layout!(1, "fill")),
652         vframe(layout!(1, "fill")),
653         vframe(layout!(1, "fill")),
654         vframe(layout!(1, "fill")),
655     );
656 
657     root.io = io;
658     root.draw();
659 
660     io.assertRectangle(Rectangle(0,   0, 800, 147), color("#f00"));
661     io.assertRectangle(Rectangle(0, 151, 800, 147), color("#f00"));
662     io.assertRectangle(Rectangle(0, 302, 800, 147), color("#f00"));
663     io.assertRectangle(Rectangle(0, 453, 800, 147), color("#f00"));
664 
665 }
666 
667 @("Gaps do not apply to invisible children")
668 unittest {
669 
670     import fluid.theme;
671 
672     auto theme = nullTheme.derive(
673         rule!Space(gap = 4),
674     );
675 
676     auto spy = new class Space {
677 
678         Vector2 position;
679 
680         override void drawImpl(Rectangle outer, Rectangle inner) {
681 
682             position = outer.start;
683 
684         }
685         
686     };
687 
688     auto root = vspace(
689         theme,
690         hspace(),
691         hspace(),
692         hspace(),
693         spy,
694     );
695 
696     root.draw();
697 
698     assert(spy.position == Vector2(0, 12));
699 
700     // Hide one child
701     root.children[0].hide();
702     root.draw();
703 
704     assert(spy.position == Vector2(0, 8));
705     
706 
707 }
708 
709 @("applied style.gap depends on axis")
710 unittest {
711 
712     auto theme = nullTheme.derive(
713         rule!Space(
714             Rule.gap = [2, 4],
715         ),
716     );
717 
718     class Warden : Space {
719 
720         Rectangle outer;
721 
722         override void drawImpl(Rectangle outer, Rectangle inner) {
723             super.drawImpl(this.outer = outer, inner);
724         }
725 
726     }
727 
728     Warden[4] wardens;
729 
730     auto root = vspace(
731         theme,
732         hspace(
733             wardens[0] = new Warden,
734             wardens[1] = new Warden,
735         ),
736         vspace(
737             wardens[2] = new Warden,
738             wardens[3] = new Warden,
739         ),
740     );
741 
742     root.draw();
743     
744     assert(wardens[0].outer.start == Vector2(0, 0));
745     assert(wardens[1].outer.start == Vector2(2, 0));
746     assert(wardens[2].outer.start == Vector2(0, 4));
747     assert(wardens[3].outer.start == Vector2(0, 8));
748 
749 }