1 module fluid.grid;
2 
3 import std.range;
4 import std.algorithm;
5 
6 import fluid.node;
7 import fluid.tree;
8 import fluid.frame;
9 import fluid.style;
10 import fluid.utils;
11 import fluid.backend;
12 import fluid.structs;
13 
14 
15 @safe:
16 
17 
18 deprecated("`Grid` and `grid` were renamed to `GridFrame` and `gridFrame` respectively. To be removed in 0.8.0.") {
19 
20     alias grid = simpleConstructor!GridFrame;
21     alias Grid = GridFrame;
22 
23 }
24 
25 alias gridFrame = simpleConstructor!GridFrame;
26 alias gridRow = simpleConstructor!GridRow;
27 
28 // TODO rename segments to columns?
29 
30 /// Segments is used to set the number of columns spanned by a grid item. When applied to a grid, it sets the number of
31 /// columns the grid will have.
32 struct Segments {
33 
34     /// Number of columns used by a grid item.
35     uint amount = 1;
36 
37     /// Set the number of columns present in a grid.
38     void apply(GridFrame grid) {
39 
40         grid.segmentCount = amount;
41 
42     }
43 
44     /// Set the number of columns used by this node.
45     void apply(Node node) {
46 
47         node.layout.expand = amount;
48 
49     }
50 
51 }
52 
53 /// ditto
54 Segments segments(uint columns) {
55 
56     return Segments(columns);
57 
58 }
59 
60 /// ditto
61 Segments segments(uint columns)() {
62 
63     return Segments(columns);
64 
65 }
66 
67 /// The GridFrame node will align its children in a 2D grid.
68 class GridFrame : Frame {
69 
70     size_t segmentCount;
71 
72     private {
73 
74         /// Sizes for each segment.
75         int[] segmentSizes;
76 
77         /// Last grid width given.
78         float lastWidth;
79 
80     }
81 
82     this(Ts...)(Ts children) {
83 
84         this.children.length = children.length;
85 
86         // Check the other arguments
87         static foreach (i, arg; children) {{
88 
89             // Grid row (via array)
90             static if (is(typeof(arg) : U[], U)) {
91 
92                 this.children[i] = gridRow(arg);
93 
94             }
95 
96             // Other stuff
97             else {
98 
99                 this.children[i] = arg;
100 
101             }
102 
103         }}
104 
105     }
106 
107     unittest {
108 
109         import std.math;
110         import std.array;
111         import std.typecons;
112         import fluid.label;
113 
114         auto io = new HeadlessBackend;
115         auto root = gridFrame(
116             .nullTheme,
117             .layout!"fill",
118             .segments!4,
119 
120             label("You can make tables and grids with Grid"),
121             [
122                 label("This"),
123                 label("Is"),
124                 label("A"),
125                 label("Grid"),
126             ],
127             [
128                 label(.segments!2, "Multiple columns"),
129                 label(.segments!2, "For a single cell"),
130             ]
131         );
132 
133         root.io = io;
134         root.draw();
135 
136         // Check layout parameters
137 
138         assert(root.layout == .layout!"fill");
139         assert(root.segmentCount == 4);
140         assert(root.children.length == 3);
141 
142         assert(cast(Label) root.children[0]);
143 
144         auto row1 = cast(GridRow) root.children[1];
145 
146         assert(row1);
147         assert(row1.segmentCount == 4);
148         assert(row1.children.all!"a.layout.expand == 0");
149 
150         auto row2 = cast(GridRow) root.children[2];
151 
152         assert(row2);
153         assert(row2.segmentCount == 4);
154         assert(row2.children.all!"a.layout.expand == 2");
155 
156         // Current implementation requires an extra frame to settle. This shouldn't be necessary.
157         io.nextFrame;
158         root.draw();
159 
160         // Each column should be 200px wide
161         assert(root.segmentSizes == [200, 200, 200, 200]);
162 
163         const rowEnds = root.children.map!(a => a.minSize.y)
164             .cumulativeFold!"a + b"
165             .array;
166 
167         // Check if the drawing is correct
168         // Row 0
169         io.assertTexture(Rectangle(0, 0, root.children[0].minSize.tupleof), color!"fff");
170 
171         // Row 1
172         foreach (i; 0..4) {
173 
174             const start = Vector2(i * 200, rowEnds[0]);
175 
176             assert(io.textures.canFind!(tex => tex.isStartClose(start)));
177 
178         }
179 
180         // Row 2
181         foreach (i; 0..2) {
182 
183             const start = Vector2(i * 400, rowEnds[1]);
184 
185             assert(io.textures.canFind!(tex => tex.isStartClose(start)));
186 
187         }
188 
189     }
190 
191     /// Add a new row to this grid.
192     void addRow(Ts...)(Ts content) {
193 
194         children ~= gridRow(content);
195 
196     }
197 
198     /// Magic to extract return value of extractParams at compile time.
199     private struct Number(size_t num) {
200 
201         enum value = num;
202 
203     }
204 
205     /// Evaluate special parameters and get the index of the first non-special parameter (not Segments, Layout nor
206     /// Theme).
207     /// Returns: An instance of `Number` with said index as parameter.
208     private auto extractParams(Args...)(Args args) {
209 
210         enum maxInitialArgs = min(args.length, 3);
211 
212         static foreach (i, arg; args[0..maxInitialArgs]) {
213 
214             // Complete; wait to the end
215             static if (__traits(compiles, endIndex)) { }
216 
217             // Segment count
218             else static if (is(typeof(arg) : Segments)) {
219 
220                 segmentCount = arg.expand;
221 
222             }
223 
224             // Layout
225             else static if (is(typeof(arg) : Layout)) {
226 
227                 layout = arg;
228 
229             }
230 
231             // Theme
232             else static if (is(typeof(arg) : Theme)) {
233 
234                 theme = arg;
235 
236             }
237 
238             // Mark this as the end
239             else enum endIndex = i;
240 
241         }
242 
243         static if (!__traits(compiles, endIndex)) {
244 
245             enum endIndex = maxInitialArgs;
246 
247         }
248 
249         return Number!endIndex();
250 
251     }
252 
253     override protected void resizeImpl(Vector2 space) {
254 
255         import std.numeric;
256 
257         // Need to recalculate segments
258         if (segmentCount == 0) {
259 
260             // Increase segment count
261             segmentCount = 1;
262 
263             // Check children
264             foreach (child; children) {
265 
266                 // Only count rows
267                 if (auto row = cast(GridRow) child) {
268 
269                     // Set self as parent
270                     row.parent = this;
271 
272                     // Recalculate the segments needed by the row
273                     row.calculateSegments();
274 
275                     // Set the segment count to the lowest common multiple of the current segment count and the cell
276                     // count of this row
277                     segmentCount = lcm(segmentCount, row.segmentCount);
278 
279                 }
280 
281             }
282 
283         }
284 
285         else {
286 
287             foreach (child; children) {
288 
289                 // Assign self as parent to all rows
290                 if (auto row = cast(GridRow) child) {
291                     row.parent = this;
292                 }
293 
294             }
295 
296         }
297 
298         // Reserve the segments
299         segmentSizes = new int[segmentCount];
300 
301         // Resize the children
302         super.resizeImpl(space);
303 
304         // Reset width
305         lastWidth = 0;
306 
307     }
308 
309     override void drawImpl(Rectangle outer, Rectangle inner) {
310 
311         // TODO WHY is this done here and not in resizeImpl?
312         void expand(Node child) {
313 
314             // Given more grid space than we allocated
315             if (lastWidth >= inner.width + 1) return;
316 
317             // Only proceed if the given node is a row
318             if (!cast(GridRow) child) return;
319 
320             // Update the width
321             lastWidth = inner.width;
322 
323             // Get margin for the row
324             const rowMargin = child.style.totalMargin;
325             // Note: We're assuming all rows have the same margin. This might not hold true with the introduction of
326             // tags.
327 
328             // Expand the segments to match box size
329             redistributeSpace(segmentSizes, inner.width - rowMargin.sideLeft - rowMargin.sideRight);
330 
331         }
332 
333         // Draw the background
334         pickStyle.drawBackground(tree.io, outer);
335 
336         // Get the position
337         auto position = inner.y;
338 
339         // Draw the rows
340         foreach (child; filterChildren) {
341 
342             // Get params
343             const rect = Rectangle(
344                 inner.x, position,
345                 inner.width, child.minSize.y
346             );
347 
348             // Try to expand grid segments
349             expand(child);
350 
351             // Draw the child
352             child.draw(rect);
353 
354             // Offset position
355             position += child.minSize.y + style.gap.sideY;
356 
357         }
358 
359     }
360 
361     unittest {
362 
363         import fluid.label;
364 
365         // Nodes are to span segments in order:
366         // 1. One label to span 6 segments
367         // 2. Each 3 segments
368         // 3. Each 2 segments
369         auto g = gridFrame(
370             [ label("") ],
371             [ label(""), label("") ],
372             [ label(""), label(""), label("") ],
373         );
374 
375         g.backend = new HeadlessBackend;
376         g.draw();
377 
378         assert(g.segmentCount == 6);
379 
380     }
381 
382 }
383 
384 /// A single row in a `Grid`.
385 class GridRow : Frame {
386 
387     GridFrame parent;
388     size_t segmentCount;
389 
390     /// Params:
391     ///     nodes = Children to be placed in the row.
392     this(Ts...)(Ts nodes) {
393 
394         super(nodes);
395         this.layout.nodeAlign = NodeAlign.fill;
396         this.directionHorizontal = true;
397 
398     }
399 
400     void calculateSegments() {
401 
402         segmentCount = 0;
403 
404         // Count segments used by each child
405         foreach (child; children) {
406 
407             segmentCount += either(child.layout.expand, 1);
408 
409         }
410 
411     }
412 
413     override void resizeImpl(Vector2 space) {
414 
415         // Reset the size
416         minSize = Vector2();
417 
418         // Empty row; do nothing
419         if (children.length == 0) return;
420 
421         // No segments calculated, run now
422         if (segmentCount == 0) {
423 
424             calculateSegments();
425 
426         }
427 
428         size_t segment;
429 
430         // Resize the children
431         foreach (i, child; children) {
432 
433             const segments = either(child.layout.expand, 1);
434             const gapSpace = max(
435                 0, 
436                 style.gap.sideX * (cast(ptrdiff_t) children.length - 1)
437             );
438             const childSpace = Vector2(
439                 space.x * segments / segmentCount - gapSpace,
440                 minSize.y,
441             );
442 
443             scope (exit) segment += segments;
444 
445             // Resize the child
446             child.resize(tree, theme, childSpace);
447 
448             auto range = parent.segmentSizes[segment..segment+segments];
449 
450             // Include the gap for all, but the first child
451             const gap = i == 0 ? 0 : style.gap.sideX;
452 
453             // Second step: Expand the segments to give some space for the child
454             minSize.x += range.redistributeSpace(child.minSize.x + gap);
455 
456             // Increase vertical space, if needed
457             if (child.minSize.y > minSize.y) {
458 
459                 minSize.y = child.minSize.y;
460 
461             }
462 
463         }
464 
465     }
466 
467     override protected void drawImpl(Rectangle outer, Rectangle inner) {
468 
469         size_t segment;
470 
471         pickStyle.drawBackground(tree.io, outer);
472 
473         /// Child position.
474         auto position = Vector2(inner.x, inner.y);
475 
476         foreach (i, child; filterChildren) {
477 
478             const segments = either(child.layout.expand, 1);
479             const gap = i == 0 ? 0 : style.gap.sideX;
480             const width = parent.segmentSizes[segment..segment+segments].sum;
481 
482             // Draw the child
483             child.draw(Rectangle(
484                 position.x + gap, position.y,
485                 width - gap, inner.height,
486             ));
487 
488             // Proceed to the next segment
489             segment += segments;
490             position.x += width;
491 
492         }
493 
494     }
495 
496     @("Grid rows can have gaps")
497     unittest {
498 
499         auto theme = nullTheme.derive(
500             rule!GridFrame(
501                 Rule.gap = 4,
502             ),
503             rule!GridRow(
504                 Rule.gap = 6,
505             ),
506         );
507 
508         static class Warden : Frame {
509 
510             Vector2 position;
511 
512             override void resizeImpl(Vector2 space) {
513                 super.resizeImpl(space);
514                 minSize = Vector2(10, 10);
515             }
516 
517             override void drawImpl(Rectangle outer, Rectangle) {
518                 position = outer.start;
519             }
520 
521         }
522 
523         alias warden = simpleConstructor!Warden;
524 
525         Warden[3] row1;
526         Warden[6] row2;
527 
528         auto grid = gridFrame(
529             theme,
530             [
531                 row1[0] = warden(.segments!2),
532                 row1[1] = warden(.segments!2),
533                 row1[2] = warden(.segments!2),
534             ],
535             [
536                 row2[0] = warden(),
537                 row2[1] = warden(),
538                 row2[2] = warden(),
539                 row2[3] = warden(),
540                 row2[4] = warden(),
541                 row2[5] = warden(),
542             ],
543         );
544 
545         grid.draw();
546 
547         assert(row1[0].position == Vector2( 0, 0));
548         assert(row1[1].position == Vector2(32, 0));
549         assert(row1[2].position == Vector2(64, 0));
550 
551         assert(row2[0].position == Vector2( 0, 14));
552         assert(row2[1].position == Vector2(16, 14));
553         assert(row2[2].position == Vector2(32, 14));
554         assert(row2[3].position == Vector2(48, 14));
555         assert(row2[4].position == Vector2(64, 14));
556         assert(row2[5].position == Vector2(80, 14));
557 
558     }
559 
560 }
561 
562 /// Redistribute space for the given row spacing range. It will increase the size of as many cells as possible as long
563 /// as they can stay even.
564 ///
565 /// Does nothing if amount of space was reduced.
566 ///
567 /// Params:
568 ///     range = Range to work on and modify.
569 ///     space = New amount of space to apply. The resulting sum of range items will be equal or greater (if it was
570 ///         already greater) to this number.
571 /// Returns:
572 ///     Newly acquired amount of space, the resulting sum of range size.
573 private ElementType!Range redistributeSpace(Range, Numeric)(ref Range range, Numeric space, string caller = __FUNCTION__) {
574 
575     import std.math;
576 
577     alias RangeNumeric = ElementType!Range;
578 
579     // Get a sorted copy of the range
580     auto sortedCopy = range.dup.sort!"a > b";
581 
582     // Find smallest item & current size of the range
583     RangeNumeric currentSize;
584     RangeNumeric smallestItem;
585 
586     // Check current data from the range
587     foreach (item; sortedCopy.save) {
588 
589         currentSize += item;
590         smallestItem = item;
591 
592     }
593 
594     /// Extra space to give
595     auto extra = cast(double) space - currentSize;
596 
597     // Do nothing if there's no extra space
598     if (extra < 0 || extra.isClose(0)) return currentSize;
599 
600     /// Space to give per segment
601     RangeNumeric perElement;
602 
603     // Check all segments
604     foreach (i, segment; sortedCopy.enumerate) {
605 
606         // Split the available over all remaining segments
607         perElement = smallestItem + cast(RangeNumeric) ceil(extra / (range.length - i));
608 
609         // Skip segments if the resulting size isn't big enough
610         if (perElement > segment) break;
611 
612     }
613 
614     RangeNumeric total;
615 
616     // Assign the size
617     foreach (ref item; range)  {
618 
619         // Grow this one
620         item = max(item, perElement);
621         total += item;
622 
623     }
624 
625     return total;
626 
627 }