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